diff --git a/treeherder/webapp/api/push.py b/treeherder/webapp/api/push.py
index 2a780282602..b8ecebb5443 100644
--- a/treeherder/webapp/api/push.py
+++ b/treeherder/webapp/api/push.py
@@ -28,6 +28,11 @@
logger = logging.getLogger(__name__)
+def count_unique_test_failures(failures):
+ """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
@@ -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
@@ -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)
)
@@ -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,
},
)
diff --git a/ui/push-health/Action.jsx b/ui/push-health/Action.jsx
index 7f15c01b753..b465551862f 100644
--- a/ui/push-health/Action.jsx
+++ b/ui/push-health/Action.jsx
@@ -46,6 +46,7 @@ class Action extends PureComponent {
investigateTest,
unInvestigateTest,
updatePushHealth,
+ decisionTaskMap,
} = this.props;
const groupedTests = this.getTestGroups(tests);
@@ -69,6 +70,7 @@ class Action extends PureComponent {
investigateTest={investigateTest}
unInvestigateTest={unInvestigateTest}
updatePushHealth={updatePushHealth}
+ decisionTaskMap={decisionTaskMap}
/>
))}
@@ -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;
diff --git a/ui/push-health/ClassificationGroup.jsx b/ui/push-health/ClassificationGroup.jsx
index 4223937c04f..624cdc4e93b 100644
--- a/ui/push-health/ClassificationGroup.jsx
+++ b/ui/push-health/ClassificationGroup.jsx
@@ -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';
@@ -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) => {
@@ -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 (
@@ -132,43 +142,20 @@ class ClassificationGroup extends React.PureComponent {
{hasRetriggerAll && groupLength > 0 && detailsShowing && (
-
-
-
-
-
- {[5, 10, 15].map((times) => (
- this.retriggerAll(times)}
- className="pointable"
- tag="a"
- >
- Retrigger all {times} times
-
- ))}
-
-
-
+
))}
@@ -269,6 +257,7 @@ ClassificationGroup.propTypes = {
groupedBy: PropTypes.string,
setOrderedBy: PropTypes.func,
setGroupedBy: PropTypes.func,
+ decisionTaskMap: PropTypes.shape({}),
};
ClassificationGroup.defaultProps = {
@@ -280,6 +269,7 @@ ClassificationGroup.defaultProps = {
groupedBy: 'path',
setOrderedBy: () => {},
setGroupedBy: () => {},
+ decisionTaskMap: {},
};
export default ClassificationGroup;
diff --git a/ui/push-health/Health.jsx b/ui/push-health/Health.jsx
index 03ba7b0701a..2df3362d556 100644
--- a/ui/push-health/Health.jsx
+++ b/ui/push-health/Health.jsx
@@ -49,6 +49,7 @@ export default class Health extends React.PureComponent {
regressionsGroupBy: params.get('regressionsGroupBy') || 'path',
showIntermittentAlert:
localStorage.getItem('dismissedIntermittentAlert') !== 'true',
+ decisionTaskMap: {},
};
}
@@ -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;
@@ -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;
};
@@ -177,6 +192,7 @@ export default class Health extends React.PureComponent {
regressionsOrderBy,
regressionsGroupBy,
showIntermittentAlert,
+ decisionTaskMap,
} = this.state;
const { tests, commitHistory, linting, builds } = metrics;
@@ -320,6 +336,7 @@ export default class Health extends React.PureComponent {
investigateTest={this.investigateTest}
unInvestigateTest={this.unInvestigateTest}
updatePushHealth={this.updatePushHealth}
+ decisionTaskMap={decisionTaskMap}
/>
diff --git a/ui/push-health/PlatformConfig.jsx b/ui/push-health/PlatformConfig.jsx
index c43b7dfc750..e187d8e0c9b 100644
--- a/ui/push-health/PlatformConfig.jsx
+++ b/ui/push-health/PlatformConfig.jsx
@@ -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';
@@ -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() {
@@ -121,13 +126,13 @@ class PlatformConfig extends React.PureComponent {
);
})}
@@ -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;
diff --git a/ui/push-health/Test.jsx b/ui/push-health/Test.jsx
index de4b448298c..50e3268f8e6 100644
--- a/ui/push-health/Test.jsx
+++ b/ui/push-health/Test.jsx
@@ -1,25 +1,17 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import {
- Button,
- Collapse,
- Nav,
- Navbar,
- ButtonGroup,
- Dropdown,
- Form,
-} from 'react-bootstrap';
+import { Button, Collapse, Nav, Navbar, Form } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCaretDown,
faCaretRight,
- faRedo,
+ faCheck,
} from '@fortawesome/free-solid-svg-icons';
import { create, destroy } from '../helpers/http';
import { getProjectUrl } from '../helpers/location';
import { investigatedTestsEndPoint } from '../helpers/url';
-import JobModel from '../models/job';
+import { confirmFailure, canConfirmFailure } from '../helpers/job';
import Clipboard from '../shared/Clipboard';
import PlatformConfig from './PlatformConfig';
@@ -59,23 +51,31 @@ class Test extends PureComponent {
});
};
- retriggerSelected = (times) => {
- const { notify, currentRepo, jobs } = this.props;
+ confirmFailureSelected = () => {
+ const { notify, currentRepo, jobs, decisionTaskMap } = this.props;
const { selectedTests } = this.state;
- // Reduce down to the unique jobs
+ // Reduce down to the unique jobs that can have confirm-failure run
const testJobs = Array.from(selectedTests)
.filter((test) => test.isInvestigated)
.reduce(
(acc, test) => ({
...acc,
- ...jobs[test.jobName].reduce((_, 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);
+ });
};
markAsInvestigated = async () => {
@@ -218,6 +218,7 @@ class Test extends PureComponent {
selectedJobName,
selectedTaskId,
updateParamsAndState,
+ decisionTaskMap,
} = this.props;
const {
clipboardVisible,
@@ -267,42 +268,21 @@ class Test extends PureComponent {