Bug 1597835 - Refactor Push Health to ease adding new metric support (#5661)

The push_health.py file is now 'tests.py' where we will add 'linting.py', etc.
Changed a couple function names to make them a little more explicit.

* Change the 'metrics' field in the result to an object instead of
an array.  This way the UI can ask for each metric more easily.
It'd rather have the UI decide what order to display the metrics
than the API.

This also makes the 'Metric.jsx' component simpler and just control
expand / collapse and display of basic pass / fail.
This commit is contained in:
Cameron Dawson 2019-11-22 10:19:04 -08:00 коммит произвёл GitHub
Родитель 667efe0d04
Коммит eeed1d759f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 103 добавлений и 129 удалений

Просмотреть файл

@ -3,9 +3,9 @@ import datetime
from django.core.management.base import BaseCommand
from treeherder.model.models import OptionCollection
from treeherder.push_health.push_health import (fixed_by_commit_history_days,
get_history,
intermittent_history_days)
from treeherder.push_health.tests import (fixed_by_commit_history_days,
get_history,
intermittent_history_days)
from treeherder.webapp.api.utils import REPO_GROUPS

Просмотреть файл

@ -79,7 +79,7 @@ def get_history(failure_classification_id, push_date, num_days, option_map, repo
# though the job failed, we count it as a 'pass' for the test in question. This 'pass' is
# used for the pass/fail ratio on the test to help determine if it is intermittent.
#
def get_push_failures(push, option_map):
def get_current_test_failures(push, option_map):
all_testfailed = Job.objects.filter(
push=push,
tier__lte=2,
@ -168,7 +168,7 @@ def has_line(failure_line, log_line_list):
return next((find_line for find_line in log_line_list if find_line['line_number'] == failure_line.line), False)
def get_push_health_test_failures(push, repository_ids):
def get_test_failures(push, repository_ids):
# query for jobs for the last two weeks excluding today
# find tests that have failed in the last 14 days
# this is very cache-able for reuse on other pushes.
@ -186,7 +186,7 @@ def get_push_health_test_failures(push, repository_ids):
fixed_by_commit_history_days,
option_map,
repository_ids)
push_failures, unsupported_jobs = get_push_failures(push, option_map)
push_failures, unsupported_jobs = get_current_test_failures(push, option_map)
filtered_push_failures = [
failure for failure in push_failures if filter_failure(failure)
]

Просмотреть файл

@ -13,7 +13,7 @@ from treeherder.model.models import (Job,
JobType,
Push,
Repository)
from treeherder.push_health.push_health import get_push_health_test_failures
from treeherder.push_health.tests import get_test_failures
from treeherder.webapp.api.serializers import PushSerializer
from treeherder.webapp.api.utils import (REPO_GROUPS,
to_datetime,
@ -211,7 +211,7 @@ class PushViewSet(viewsets.ViewSet):
except Push.DoesNotExist:
return Response("No push with id: {0}".format(pk),
status=HTTP_404_NOT_FOUND)
push_health_test_failures = get_push_health_test_failures(push, REPO_GROUPS['trunk'])
push_health_test_failures = get_test_failures(push, REPO_GROUPS['trunk'])
return Response({'needInvestigation': len(push_health_test_failures['needInvestigation'])})
@ -227,7 +227,7 @@ class PushViewSet(viewsets.ViewSet):
except Push.DoesNotExist:
return Response("No push with revision: {0}".format(revision),
status=HTTP_404_NOT_FOUND)
push_health_test_failures = get_push_health_test_failures(push, REPO_GROUPS['trunk'])
push_health_test_failures = get_test_failures(push, REPO_GROUPS['trunk'])
test_result = 'pass'
if len(push_health_test_failures['unsupported']):
test_result = 'indeterminate'
@ -238,37 +238,37 @@ class PushViewSet(viewsets.ViewSet):
'revision': revision,
'id': push.id,
'result': test_result,
'metrics': [
{
'metrics': {
'tests': {
'name': 'Tests',
'result': test_result,
'failures': push_health_test_failures,
'details': push_health_test_failures,
},
{
'linting': {
'name': 'Linting (Not yet implemented)',
'result': 'none',
'details': ['lint stuff would be good'],
},
'builds': {
'name': 'Builds (Not yet implemented)',
'result': 'pass',
'result': 'none',
'details': ['Wow, everything passed!'],
},
{
'name': 'Linting (Not yet implemented)',
'result': 'pass',
'details': ['Gosh, this code is really nicely formatted.'],
},
{
'coverage': {
'name': 'Coverage (Not yet implemented)',
'result': 'pass',
'result': 'none',
'details': [
'Covered 42% of the tests that are needed for feature ``foo``.',
'Covered 100% of the tests that are needed for feature ``bar``.',
'The ratio of people to cake is too many...',
],
},
{
'performance': {
'name': 'Performance (Not yet implemented)',
'result': 'pass',
'result': 'none',
'details': ['Ludicrous Speed'],
},
],
},
})
@cache_memoize(60 * 60)

Просмотреть файл

@ -14,6 +14,7 @@ import PushModel from '../models/push';
import { resultColorMap } from './helpers';
import Metric from './Metric';
import Navigation from './Navigation';
import TestFailures from './TestFailures';
export default class Health extends React.PureComponent {
constructor(props) {
@ -25,7 +26,8 @@ export default class Health extends React.PureComponent {
user: { isLoggedIn: false },
revision: params.get('revision'),
repo: params.get('repo'),
healthData: null,
metrics: {},
result: null,
failureMessage: null,
notifications: [],
};
@ -55,9 +57,7 @@ export default class Health extends React.PureComponent {
updatePushHealth = async () => {
const { repo, revision } = this.state;
const { data, failureStatus } = await PushModel.getHealth(repo, revision);
const newState = !failureStatus
? { healthData: data }
: { failureMessage: data };
const newState = !failureStatus ? data : { failureMessage: data };
this.setState(newState);
};
@ -85,17 +85,17 @@ export default class Health extends React.PureComponent {
render() {
const {
healthData,
metrics,
result,
user,
repo,
revision,
failureMessage,
notifications,
} = this.state;
const { tests, linting, builds, coverage, performance } = metrics;
const { currentRepo } = this.props;
const overallResult = healthData
? resultColorMap[healthData.result]
: 'none';
const overallResult = result ? resultColorMap[result] : 'none';
return (
<React.Fragment>
@ -105,7 +105,7 @@ export default class Health extends React.PureComponent {
notifications={notifications}
clearNotification={this.clearNotification}
/>
{healthData && (
{!!tests && !!currentRepo && (
<div className="d-flex flex-column">
<h3 className="text-center">
<span className={`badge badge-xl mb-3 badge-${overallResult}`}>
@ -121,20 +121,25 @@ export default class Health extends React.PureComponent {
</h3>
<Table size="sm" className="table-fixed">
<tbody>
{healthData.metrics.map(metric => (
<tr key={metric.name}>
<Metric
name={metric.name}
result={metric.result}
value={metric.value}
details={metric.details}
failures={metric.failures}
repo={repo}
currentRepo={currentRepo}
revision={revision}
user={user}
notify={this.notify}
/>
<tr>
<TestFailures
data={tests}
repo={repo}
currentRepo={currentRepo}
revision={revision}
user={user}
notify={this.notify}
/>
</tr>
{[linting, builds, coverage, performance].map(metric => (
<tr>
<Metric result={metric.result} name={metric.name}>
<div>
{metric.details.map(detail => (
<div>{detail}</div>
))}
</div>
</Metric>
</tr>
))}
</tbody>
@ -142,7 +147,7 @@ export default class Health extends React.PureComponent {
</div>
)}
{failureMessage && <ErrorMessages failureMessage={failureMessage} />}
{!failureMessage && !healthData && <Spinner />}
{!failureMessage && !tests && <Spinner />}
</Container>
</React.Fragment>
);
@ -151,5 +156,9 @@ export default class Health extends React.PureComponent {
Health.propTypes = {
location: PropTypes.object.isRequired,
currentRepo: PropTypes.object.isRequired,
currentRepo: PropTypes.object,
};
Health.defaultProps = {
currentRepo: null,
};

Просмотреть файл

@ -8,7 +8,6 @@ import {
import { Button, Badge, Row, Col, Collapse, Card, CardBody } from 'reactstrap';
import { resultColorMap } from './helpers';
import TestFailures from './TestFailures';
export default class Metric extends React.PureComponent {
constructor(props) {
@ -17,7 +16,7 @@ export default class Metric extends React.PureComponent {
const { result } = this.props;
this.state = {
detailsShowing: result !== 'pass',
detailsShowing: !['pass', 'none'].includes(result),
};
}
@ -27,17 +26,7 @@ export default class Metric extends React.PureComponent {
render() {
const { detailsShowing } = this.state;
const {
result,
name,
details,
failures,
repo,
revision,
user,
notify,
currentRepo,
} = this.props;
const { result, name, children } = this.props;
const resultColor = resultColorMap[result];
const expandIcon = detailsShowing ? faMinusSquare : faPlusSquare;
@ -63,24 +52,7 @@ export default class Metric extends React.PureComponent {
</Row>
<Collapse isOpen={detailsShowing}>
<Card>
<CardBody>
{name === 'Tests' && (
<TestFailures
failures={failures}
repo={repo}
currentRepo={currentRepo}
revision={revision}
user={user}
notify={notify}
/>
)}
{details &&
details.map(detail => (
<div key={detail} className="ml-3">
{detail}
</div>
))}
</CardBody>
<CardBody>{children}</CardBody>
</Card>
</Collapse>
</Col>
@ -91,18 +63,7 @@ export default class Metric extends React.PureComponent {
}
Metric.propTypes = {
repo: PropTypes.string.isRequired,
currentRepo: PropTypes.object.isRequired,
revision: PropTypes.string.isRequired,
result: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
notify: PropTypes.func.isRequired,
details: PropTypes.array,
failures: PropTypes.object,
};
Metric.defaultProps = {
details: null,
failures: null,
children: PropTypes.object.isRequired,
};

Просмотреть файл

@ -3,54 +3,58 @@ import PropTypes from 'prop-types';
import ClassificationGroup from './ClassificationGroup';
import UnsupportedGroup from './UnsupportedGroup';
import Metric from './Metric';
export default class TestFailures extends React.PureComponent {
render() {
const { failures, repo, revision, user, notify, currentRepo } = this.props;
const { needInvestigation, intermittent, unsupported } = failures;
const { data, repo, revision, user, notify, currentRepo } = this.props;
const { name, result, details } = data;
const { needInvestigation, intermittent, unsupported } = details;
const needInvestigationLength = Object.keys(needInvestigation).length;
return (
<div className="border-bottom border-secondary">
<ClassificationGroup
group={needInvestigation}
name="Need Investigation"
repo={repo}
currentRepo={currentRepo}
revision={revision}
className="mb-5"
headerColor={needInvestigationLength ? 'danger' : 'secondary'}
user={user}
hasRetriggerAll
notify={notify}
/>
<ClassificationGroup
group={intermittent}
name="Known Intermittent"
repo={repo}
currentRepo={currentRepo}
revision={revision}
className="mb-5"
headerColor="secondary"
expanded={false}
user={user}
notify={notify}
/>
<UnsupportedGroup
group={unsupported}
name="Unsupported"
repo={repo}
revision={revision}
className="mb-5"
headerColor="warning"
/>
</div>
<Metric name={name} result={result}>
<div className="border-bottom border-secondary">
<ClassificationGroup
group={needInvestigation}
name="Need Investigation"
repo={repo}
currentRepo={currentRepo}
revision={revision}
className="mb-5"
headerColor={needInvestigationLength ? 'danger' : 'secondary'}
user={user}
hasRetriggerAll
notify={notify}
/>
<ClassificationGroup
group={intermittent}
name="Known Intermittent"
repo={repo}
currentRepo={currentRepo}
revision={revision}
className="mb-5"
headerColor="secondary"
expanded={false}
user={user}
notify={notify}
/>
<UnsupportedGroup
group={unsupported}
name="Unsupported"
repo={repo}
revision={revision}
className="mb-5"
headerColor="warning"
/>
</div>
</Metric>
);
}
}
TestFailures.propTypes = {
failures: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
repo: PropTypes.string.isRequired,
currentRepo: PropTypes.object.isRequired,