Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions treeherder/webapp/api/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
logger = logging.getLogger(__name__)


def count_unique_test_failures(failures):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct. But since it is a utility function that is used at 3 places now (and may get used further), I think it would be nice to have a small unit test for it.

"""Count unique test names regardless of platform/config."""
return len(set(f["testName"] for f in failures))


class PushViewSet(viewsets.ViewSet):
"""
View for ``push`` records
Expand Down Expand Up @@ -297,7 +302,9 @@ def health_summary(self, request, project):
push
)

test_failure_count = len(push_health_test_failures["needInvestigation"])
test_failure_count = count_unique_test_failures(
push_health_test_failures["needInvestigation"]
)
build_failure_count = len(push_health_build_failures)
lint_failure_count = len(push_health_lint_failures)
test_in_progress_count = 0
Expand Down Expand Up @@ -400,7 +407,7 @@ def health(self, request, project):

status = push.get_status()
total_failures = (
len(push_health_test_failures["needInvestigation"])
count_unique_test_failures(push_health_test_failures["needInvestigation"])
+ len(build_failures)
+ len(lint_failures)
)
Expand All @@ -413,7 +420,9 @@ def health(self, request, project):
{
"revision": revision,
"repo": repository.name,
"needInvestigation": len(push_health_test_failures["needInvestigation"]),
"needInvestigation": count_unique_test_failures(
push_health_test_failures["needInvestigation"]
),
"author": push.author,
},
)
Expand Down
7 changes: 7 additions & 0 deletions ui/push-health/Action.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Action extends PureComponent {
investigateTest,
unInvestigateTest,
updatePushHealth,
decisionTaskMap,
} = this.props;
const groupedTests = this.getTestGroups(tests);

Expand All @@ -69,6 +70,7 @@ class Action extends PureComponent {
investigateTest={investigateTest}
unInvestigateTest={unInvestigateTest}
updatePushHealth={updatePushHealth}
decisionTaskMap={decisionTaskMap}
/>
</div>
))}
Expand All @@ -85,6 +87,11 @@ Action.propTypes = {
revision: PropTypes.string.isRequired,
currentRepo: PropTypes.shape({}).isRequired,
notify: PropTypes.func.isRequired,
decisionTaskMap: PropTypes.shape({}),
};

Action.defaultProps = {
decisionTaskMap: {},
};

export default Action;
82 changes: 36 additions & 46 deletions ui/push-health/ClassificationGroup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCaretDown,
faCaretRight,
faRedo,
faCheck,
} from '@fortawesome/free-solid-svg-icons';
import {
Row,
Collapse,
ButtonGroup,
DropdownButton,
Button,
Dropdown,
} from 'react-bootstrap';
import groupBy from 'lodash/groupBy';

import JobModel from '../models/job';
import { confirmFailure, canConfirmFailure } from '../helpers/job';

import Action from './Action';

Expand All @@ -40,19 +39,27 @@ class ClassificationGroup extends React.PureComponent {
}));
};

retriggerAll = (times) => {
const { tests, notify, currentRepo, jobs } = this.props;
// Reduce down to the unique jobs
confirmFailureAll = () => {
const { tests, notify, currentRepo, jobs, decisionTaskMap } = this.props;
// Reduce down to the unique jobs that can have confirm-failure run
const testJobs = tests.reduce(
(acc, test) => ({
...acc,
...jobs[test.jobName].reduce((fjAcc, job) => ({ [job.id]: job }), {}),
...jobs[test.jobName].reduce((fjAcc, job) => {
if (canConfirmFailure(job)) {
return { ...fjAcc, [job.id]: job };
}
return fjAcc;
}, {}),
}),
{},
);
const uniqueJobs = Object.values(testJobs);

JobModel.retrigger(uniqueJobs, currentRepo, notify, times);
// Call confirmFailure for each unique job
uniqueJobs.forEach((job) => {
confirmFailure(job, notify, decisionTaskMap, currentRepo);
});
};

getTestsByAction = (tests) => {
Expand Down Expand Up @@ -98,12 +105,15 @@ class ClassificationGroup extends React.PureComponent {
investigateTest,
unInvestigateTest,
updatePushHealth,
decisionTaskMap,
} = this.props;
const expandIcon = detailsShowing ? faCaretDown : faCaretRight;
const expandTitle = detailsShowing
? 'Click to collapse'
: 'Click to expand';
const groupLength = Object.keys(tests).length;
// Count unique test cases (by testName), regardless of platform/config
const uniqueTestNames = new Set(tests.map((test) => test.testName));
const groupLength = uniqueTestNames.size;
const testsByAction = this.getTestsByAction(tests);

return (
Expand Down Expand Up @@ -132,43 +142,20 @@ class ClassificationGroup extends React.PureComponent {
</span>
{hasRetriggerAll && groupLength > 0 && detailsShowing && (
<div className="mb-4 d-flex gap-2">
<ButtonGroup size="sm">
<Button
title="Retrigger all 'Need Investigation' jobs once"
onClick={() => this.retriggerAll(1)}
size="sm"
variant="secondary"
>
<FontAwesomeIcon
icon={faRedo}
title="Retrigger"
className="me-2"
alt=""
/>
Retrigger all
</Button>
<Dropdown>
<Dropdown.Toggle
split
variant="secondary"
size="sm"
title="Retrigger all multiple times"
/>
<Dropdown.Menu>
{[5, 10, 15].map((times) => (
<Dropdown.Item
key={times}
title={`Retrigger all 'Need Investigation' jobs ${times} times`}
onClick={() => this.retriggerAll(times)}
className="pointable"
tag="a"
>
Retrigger all {times} times
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
</ButtonGroup>
<Button
title="Confirm failures for all 'Need Investigation' jobs"
onClick={() => this.confirmFailureAll()}
size="sm"
variant="secondary"
>
<FontAwesomeIcon
icon={faCheck}
title="Confirm Failure"
className="me-2"
alt=""
/>
Confirm Failure all
</Button>
<DropdownButton
size="sm"
className="ms-1"
Expand Down Expand Up @@ -246,6 +233,7 @@ class ClassificationGroup extends React.PureComponent {
investigateTest={investigateTest}
unInvestigateTest={unInvestigateTest}
updatePushHealth={updatePushHealth}
decisionTaskMap={decisionTaskMap}
/>
))}
</div>
Expand All @@ -269,6 +257,7 @@ ClassificationGroup.propTypes = {
groupedBy: PropTypes.string,
setOrderedBy: PropTypes.func,
setGroupedBy: PropTypes.func,
decisionTaskMap: PropTypes.shape({}),
};

ClassificationGroup.defaultProps = {
Expand All @@ -280,6 +269,7 @@ ClassificationGroup.defaultProps = {
groupedBy: 'path',
setOrderedBy: () => {},
setGroupedBy: () => {},
decisionTaskMap: {},
};

export default ClassificationGroup;
17 changes: 17 additions & 0 deletions ui/push-health/Health.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default class Health extends React.PureComponent {
regressionsGroupBy: params.get('regressionsGroupBy') || 'path',
showIntermittentAlert:
localStorage.getItem('dismissedIntermittentAlert') !== 'true',
decisionTaskMap: {},
};
}

Expand Down Expand Up @@ -104,6 +105,7 @@ export default class Health extends React.PureComponent {

updatePushHealth = async () => {
const { repo, revision, status } = this.state;
const { notify } = this.props;

if (status) {
const { running, pending, completed } = status;
Expand All @@ -117,6 +119,19 @@ export default class Health extends React.PureComponent {
const { data, failureStatus } = await PushModel.getHealth(repo, revision);
const newState = !failureStatus ? data : { failureMessage: data };

// Fetch decision task map if we have a push ID
if (data && data.id) {
try {
const decisionTaskMap = await PushModel.getDecisionTaskMap(
[data.id],
notify,
);
newState.decisionTaskMap = decisionTaskMap;
} catch {
// Decision task map fetch failed, but we can still show the health data
}
}

this.setState(newState);
return newState;
};
Expand Down Expand Up @@ -177,6 +192,7 @@ export default class Health extends React.PureComponent {
regressionsOrderBy,
regressionsGroupBy,
showIntermittentAlert,
decisionTaskMap,
} = this.state;
const { tests, commitHistory, linting, builds } = metrics;

Expand Down Expand Up @@ -320,6 +336,7 @@ export default class Health extends React.PureComponent {
investigateTest={this.investigateTest}
unInvestigateTest={this.unInvestigateTest}
updatePushHealth={this.updatePushHealth}
decisionTaskMap={decisionTaskMap}
/>
</TabPanel>
</div>
Expand Down
25 changes: 16 additions & 9 deletions ui/push-health/PlatformConfig.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, Row, Col } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faRedo } from '@fortawesome/free-solid-svg-icons';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import sortBy from 'lodash/sortBy';

import JobModel from '../models/job';
import { addAggregateFields } from '../helpers/job';
import {
addAggregateFields,
confirmFailure,
canConfirmFailure,
} from '../helpers/job';
import { shortDateFormat } from '../helpers/display';
import SimpleTooltip from '../shared/SimpleTooltip';

Expand Down Expand Up @@ -61,10 +64,12 @@ class PlatformConfig extends React.PureComponent {
}
};

retriggerTask = async (task) => {
const { notify, currentRepo } = this.props;
confirmFailureTask = async (task) => {
const { notify, currentRepo, decisionTaskMap } = this.props;

JobModel.retrigger([task], currentRepo, notify);
if (canConfirmFailure(task)) {
confirmFailure(task, notify, decisionTaskMap, currentRepo);
}
};

render() {
Expand Down Expand Up @@ -121,13 +126,13 @@ class PlatformConfig extends React.PureComponent {
);
})}
<Button
onClick={() => this.retriggerTask(taskList[0])}
onClick={() => this.confirmFailureTask(taskList[0])}
variant="outline"
className="me-2 border-0"
title="Retrigger task"
title="Confirm failure"
style={{ lineHeight: '10px' }}
>
<FontAwesomeIcon icon={faRedo} />
<FontAwesomeIcon icon={faCheck} />
</Button>
</Col>
</Row>
Expand All @@ -151,10 +156,12 @@ PlatformConfig.propTypes = {
currentRepo: PropTypes.shape({}).isRequired,
notify: PropTypes.func.isRequired,
updateParamsAndState: PropTypes.func.isRequired,
decisionTaskMap: PropTypes.shape({}),
};

PlatformConfig.defaultProps = {
testName: '',
decisionTaskMap: {},
};

export default PlatformConfig;
Loading