зеркало из https://github.com/mozilla/treeherder.git
Bug 1602833 - Group failures by platform and config (#5831)
This commit is contained in:
Родитель
9c075730ea
Коммит
3ebf7b7772
|
@ -41,6 +41,7 @@ describe('TestFailure', () => {
|
|||
user={{ email: 'foo' }}
|
||||
revision="abc"
|
||||
currentRepo={{ name: repoName }}
|
||||
groupedBy="platform"
|
||||
notify={() => {}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -16,11 +16,15 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
DropdownItem,
|
||||
Navbar,
|
||||
Nav,
|
||||
NavItem,
|
||||
UncontrolledButtonDropdown,
|
||||
} from 'reactstrap';
|
||||
|
||||
import JobModel from '../models/job';
|
||||
|
||||
import TestFailure from './TestFailure';
|
||||
import GroupedTests from './GroupedTests';
|
||||
|
||||
class ClassificationGroup extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -29,6 +33,8 @@ class ClassificationGroup extends React.PureComponent {
|
|||
this.state = {
|
||||
detailsShowing: props.expanded,
|
||||
retriggerDropdownOpen: false,
|
||||
groupedBy: 'path',
|
||||
orderedBy: 'count',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -57,8 +63,21 @@ class ClassificationGroup extends React.PureComponent {
|
|||
JobModel.retrigger(uniqueJobs, currentRepo, notify, times);
|
||||
};
|
||||
|
||||
setGroupedBy = groupedBy => {
|
||||
this.setState({ groupedBy });
|
||||
};
|
||||
|
||||
setOrderedBy = orderedBy => {
|
||||
this.setState({ orderedBy });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { detailsShowing, retriggerDropdownOpen } = this.state;
|
||||
const {
|
||||
detailsShowing,
|
||||
retriggerDropdownOpen,
|
||||
groupedBy,
|
||||
orderedBy,
|
||||
} = this.state;
|
||||
const {
|
||||
group,
|
||||
name,
|
||||
|
@ -97,50 +116,113 @@ class ClassificationGroup extends React.PureComponent {
|
|||
</h4>
|
||||
<Collapse isOpen={detailsShowing} className="w-100">
|
||||
{hasRetriggerAll && Object.keys(group).length > 0 && (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
title="Retrigger all 'Need Investigation' jobs once"
|
||||
onClick={() => this.retriggerAll(1)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faRedo}
|
||||
title="Retrigger"
|
||||
className="mr-2"
|
||||
/>
|
||||
Retrigger all
|
||||
</Button>
|
||||
<ButtonDropdown
|
||||
isOpen={retriggerDropdownOpen}
|
||||
toggle={this.toggleRetrigger}
|
||||
>
|
||||
<DropdownToggle caret />
|
||||
<DropdownMenu>
|
||||
{[5, 10, 15].map(times => (
|
||||
<DropdownItem
|
||||
key={times}
|
||||
title={`Retrigger all 'Need Investigation' jobs ${times} times`}
|
||||
onClick={() => this.retriggerAll(times)}
|
||||
<Navbar className="mb-4">
|
||||
<Nav>
|
||||
<NavItem>
|
||||
<ButtonGroup size="sm">
|
||||
<Button
|
||||
title="Retrigger all 'Need Investigation' jobs once"
|
||||
onClick={() => this.retriggerAll(1)}
|
||||
size="sm"
|
||||
>
|
||||
Retrigger all {times} times
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
</ButtonGroup>
|
||||
<FontAwesomeIcon
|
||||
icon={faRedo}
|
||||
title="Retrigger"
|
||||
className="mr-2"
|
||||
/>
|
||||
Retrigger all
|
||||
</Button>
|
||||
<ButtonDropdown
|
||||
isOpen={retriggerDropdownOpen}
|
||||
toggle={this.toggleRetrigger}
|
||||
size="sm"
|
||||
>
|
||||
<DropdownToggle caret />
|
||||
<DropdownMenu>
|
||||
{[5, 10, 15].map(times => (
|
||||
<DropdownItem
|
||||
key={times}
|
||||
title={`Retrigger all 'Need Investigation' jobs ${times} times`}
|
||||
onClick={() => this.retriggerAll(times)}
|
||||
tag="a"
|
||||
>
|
||||
Retrigger all {times} times
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
</ButtonGroup>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<UncontrolledButtonDropdown size="sm" className="ml-1">
|
||||
<DropdownToggle
|
||||
className="btn-sm ml-1 text-capitalize"
|
||||
id="groupTestsDropdown"
|
||||
caret
|
||||
>
|
||||
Group By: {groupedBy}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu toggler="groupTestsDropdown">
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onClick={() => this.setGroupedBy('none')}
|
||||
>
|
||||
None
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onClick={() => this.setGroupedBy('path')}
|
||||
>
|
||||
Path
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onClick={() => this.setGroupedBy('platform')}
|
||||
>
|
||||
Platform
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledButtonDropdown>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<UncontrolledButtonDropdown size="sm" className="ml-1">
|
||||
<DropdownToggle
|
||||
className="btn-sm ml-1 text-capitalize"
|
||||
id="groupTestsDropdown"
|
||||
caret
|
||||
>
|
||||
Order By: {orderedBy}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu toggler="groupTestsDropdown">
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onClick={() => this.setOrderedBy('count')}
|
||||
>
|
||||
Count
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
onClick={() => this.setOrderedBy('text')}
|
||||
>
|
||||
Text
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledButtonDropdown>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Navbar>
|
||||
)}
|
||||
<div>
|
||||
{group &&
|
||||
group.map(failure => (
|
||||
<TestFailure
|
||||
key={failure.key}
|
||||
failure={failure}
|
||||
repo={repo}
|
||||
currentRepo={currentRepo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
notify={notify}
|
||||
/>
|
||||
))}
|
||||
<GroupedTests
|
||||
group={group}
|
||||
repo={repo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
groupedBy={groupedBy}
|
||||
orderedBy={orderedBy}
|
||||
currentRepo={currentRepo}
|
||||
notify={notify}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Row>
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, UncontrolledCollapse } from 'reactstrap';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import TestFailure from './TestFailure';
|
||||
|
||||
class GroupedTests extends Component {
|
||||
getGroupedTests = tests => {
|
||||
const { groupedBy } = this.props;
|
||||
|
||||
const grouped = groupBy(tests, test => {
|
||||
switch (groupedBy) {
|
||||
case 'none':
|
||||
return 'none';
|
||||
case 'path':
|
||||
return test.testName;
|
||||
case 'platform':
|
||||
return `${test.platform} ${test.config}`;
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
group,
|
||||
repo,
|
||||
revision,
|
||||
user,
|
||||
notify,
|
||||
currentRepo,
|
||||
orderedBy,
|
||||
groupedBy,
|
||||
} = this.props;
|
||||
|
||||
const groupedTests = this.getGroupedTests(group);
|
||||
const groupedArray = Object.entries(groupedTests).map(([key, tests]) => ({
|
||||
key,
|
||||
id: key.replace(/[^a-z0-9-]+/gi, ''), // make this a valid selector
|
||||
tests,
|
||||
}));
|
||||
const sortedGroups =
|
||||
orderedBy === 'count'
|
||||
? orderBy(groupedArray, ['tests.length'], ['desc'])
|
||||
: orderBy(groupedArray, ['key'], ['asc']);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{groupedTests &&
|
||||
sortedGroups.map(group => (
|
||||
<div key={group.id}>
|
||||
<Button
|
||||
id={`${group.id}-group`}
|
||||
color="secondary"
|
||||
outline
|
||||
className="p-3 bg-light text-center text-monospace border-bottom-0 border-right-0 border-left-0 border-secondary w-100"
|
||||
title="Click to expand for test detail"
|
||||
>
|
||||
{group.key === 'none' ? 'All' : group.key} -
|
||||
<span className="ml-2 font-italic">
|
||||
{group.tests.length} test{group.tests.length > 1 && 's'}
|
||||
</span>
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-1" />
|
||||
</Button>
|
||||
<UncontrolledCollapse toggler={`${group.id}-group`}>
|
||||
{group.tests.map(failure => (
|
||||
<TestFailure
|
||||
key={failure.key}
|
||||
failure={failure}
|
||||
repo={repo}
|
||||
currentRepo={currentRepo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
notify={notify}
|
||||
groupedBy={groupedBy}
|
||||
className="ml-3"
|
||||
/>
|
||||
))}
|
||||
</UncontrolledCollapse>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupedTests.propTypes = {
|
||||
group: PropTypes.array.isRequired,
|
||||
groupedBy: PropTypes.string.isRequired,
|
||||
orderedBy: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
repo: PropTypes.string.isRequired,
|
||||
currentRepo: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GroupedTests;
|
|
@ -1,6 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Badge, Button, Row, Col, UncontrolledTooltip } from 'reactstrap';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
UncontrolledTooltip,
|
||||
UncontrolledCollapse,
|
||||
} from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faRedo } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
@ -40,7 +47,7 @@ class TestFailure extends React.PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { failure, repo, revision } = this.props;
|
||||
const { failure, repo, revision, groupedBy } = this.props;
|
||||
const {
|
||||
testName,
|
||||
action,
|
||||
|
@ -62,42 +69,9 @@ class TestFailure extends React.PureComponent {
|
|||
const { detailsShowing } = this.state;
|
||||
|
||||
return (
|
||||
<Col className="mt-2 mb-3 ml-2" key={key}>
|
||||
<Row className="border-top border-secondary justify-content-between">
|
||||
<Row className="ml-1 w-100">
|
||||
<span
|
||||
color="secondary"
|
||||
className="font-weight-bold text-uppercase mr-1"
|
||||
>
|
||||
{action} :
|
||||
</span>
|
||||
{testName}
|
||||
{tier > 1 && (
|
||||
<span className="ml-1 small text-muted">[tier-{tier}]</span>
|
||||
)}
|
||||
<span id={key} className="ml-auto mr-3">
|
||||
<strong>Pass/Fail Ratio:</strong>{' '}
|
||||
{Math.round(passFailRatio * 100)}%
|
||||
</span>
|
||||
<UncontrolledTooltip target={key} placement="left">
|
||||
Greater than 50% (and/or classification history) will make this an
|
||||
intermittent
|
||||
</UncontrolledTooltip>
|
||||
</Row>
|
||||
{!!confidence && (
|
||||
<span title="Best guess at a classification" className="ml-auto">
|
||||
{classificationMap[suggestedClassification]}
|
||||
<Badge
|
||||
color="secondary"
|
||||
className="ml-2 mr-3"
|
||||
title="Confidence in this classification guess"
|
||||
>
|
||||
{confidence}
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
</Row>
|
||||
<div className="small">
|
||||
<Row className="border-top m-3" key={key}>
|
||||
<Col>
|
||||
<Row>{groupedBy !== 'path' && <span>{testName}</span>}</Row>
|
||||
<Button
|
||||
onClick={() => this.retriggerJob(failJobs[0])}
|
||||
outline
|
||||
|
@ -107,8 +81,16 @@ class TestFailure extends React.PureComponent {
|
|||
>
|
||||
<FontAwesomeIcon icon={faRedo} title="Retrigger" />
|
||||
</Button>
|
||||
<span>
|
||||
{platform} {config}:
|
||||
{groupedBy !== 'platform' && (
|
||||
<span>
|
||||
{platform} {config}:
|
||||
</span>
|
||||
)}
|
||||
{tier > 1 && (
|
||||
<span className="ml-1 small text-muted">[tier-{tier}]</span>
|
||||
)}
|
||||
<span color="secondary" className="text-uppercase ml-1 mr-1">
|
||||
{action} :
|
||||
</span>
|
||||
{failJobs.map(failJob => (
|
||||
<Job
|
||||
|
@ -158,39 +140,63 @@ class TestFailure extends React.PureComponent {
|
|||
key={inProgressJob.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!!logLines.length && (
|
||||
<div>
|
||||
<Button
|
||||
className="border-0 text-info bg-transparent p-1"
|
||||
onClick={this.toggleDetails}
|
||||
>
|
||||
{detailsShowing ? 'less...' : 'more...'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{logLines.map(logLine => (
|
||||
<Row
|
||||
className="small text-monospace mt-2 ml-3"
|
||||
key={logLine.line_number}
|
||||
>
|
||||
{detailsShowing ? (
|
||||
<div className="pre-wrap text-break">
|
||||
{logLine.subtest}
|
||||
<Row className="ml-3">
|
||||
<div>{logLine.message}</div>
|
||||
<div>{logLine.signature}</div>
|
||||
<div>{logLine.stackwalk_stdout}</div>
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pre-wrap text-break">
|
||||
{!!logLine.subtest && logLine.subtest.substr(0, 200)}
|
||||
</div>
|
||||
{!!logLines.length && (
|
||||
<span>
|
||||
<Button
|
||||
id={key}
|
||||
className="border-0 text-info btn-sm p-1"
|
||||
outline
|
||||
onClick={this.toggleDetails}
|
||||
>
|
||||
{detailsShowing ? 'less...' : 'more...'}
|
||||
</Button>
|
||||
<UncontrolledCollapse toggler={key}>
|
||||
{logLines.map(logLine => (
|
||||
<Row
|
||||
className="small text-monospace mt-2 ml-3"
|
||||
key={logLine.line_number}
|
||||
>
|
||||
<div className="pre-wrap text-break">
|
||||
{logLine.subtest}
|
||||
<Row className="ml-3">
|
||||
<div>{logLine.message}</div>
|
||||
<div>{logLine.signature}</div>
|
||||
<div>{logLine.stackwalk_stdout}</div>
|
||||
</Row>
|
||||
</div>
|
||||
</Row>
|
||||
))}
|
||||
</UncontrolledCollapse>
|
||||
</span>
|
||||
)}
|
||||
</Col>
|
||||
<span className="ml-1">
|
||||
<Row className="justify-content-between mr-2">
|
||||
{!!confidence && (
|
||||
<span title="Best guess at a classification" className="ml-auto">
|
||||
{classificationMap[suggestedClassification]}
|
||||
<Badge
|
||||
color="secondary"
|
||||
className="ml-2 mr-3"
|
||||
title="Confidence in this classification guess"
|
||||
>
|
||||
{confidence}
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
<Row>
|
||||
<span id={`${key}-ratio`} className="mr-3">
|
||||
<strong>Pass/Fail Ratio:</strong>{' '}
|
||||
{Math.round(passFailRatio * 100)}%
|
||||
</span>
|
||||
<UncontrolledTooltip target={`${key}-ratio`} placement="left">
|
||||
Greater than 50% (and/or classification history) will make this an
|
||||
intermittent
|
||||
</UncontrolledTooltip>
|
||||
</Row>
|
||||
</span>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -214,6 +220,7 @@ TestFailure.propTypes = {
|
|||
user: PropTypes.object.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
groupedBy: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default TestFailure;
|
||||
|
|
Загрузка…
Ссылка в новой задаче