Bug 1438555 - Fix 'n' after classifying going to top failure (#5063)

This commit is contained in:
Cameron Dawson 2019-06-14 11:41:02 -07:00 коммит произвёл GitHub
Родитель 09b5077a0d
Коммит d982fb2060
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 317 добавлений и 45 удалений

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

@ -1,4 +1,7 @@
import FilterModel from '../../../ui/models/filter';
import {
getFilterUrlParamsWithDefaults,
getNonFilterUrlParams,
} from '../../../ui/models/filter';
describe('FilterModel', () => {
const oldHash = window.location.hash;
@ -10,7 +13,7 @@ describe('FilterModel', () => {
describe('parsing an old url', () => {
it('should parse the repo with defaults', () => {
window.location.hash = '?repo=mozilla-inbound';
const urlParams = FilterModel.getUrlParamsWithDefaults();
const urlParams = getFilterUrlParamsWithDefaults();
expect(urlParams).toEqual({
repo: ['mozilla-inbound'],
@ -36,7 +39,7 @@ describe('FilterModel', () => {
'filter-resultStatus=busted&filter-resultStatus=exception&' +
'filter-resultStatus=success&filter-resultStatus=retry' +
'&filter-resultStatus=runnable';
const urlParams = FilterModel.getUrlParamsWithDefaults();
const urlParams = getFilterUrlParamsWithDefaults();
expect(urlParams).toEqual({
repo: ['mozilla-inbound'],
@ -56,7 +59,10 @@ describe('FilterModel', () => {
it('should parse searchStr params with tier and groupState intact', () => {
window.location.hash =
'?repo=mozilla-inbound&filter-searchStr=Linux%20x64%20debug%20build-linux64-base-toolchains%2Fdebug%20(Bb)&filter-tier=1&group_state=expanded';
const urlParams = FilterModel.getUrlParamsWithDefaults();
const urlParams = {
...getNonFilterUrlParams(),
...getFilterUrlParamsWithDefaults(),
};
expect(urlParams).toEqual({
repo: ['mozilla-inbound'],
@ -80,13 +86,13 @@ describe('FilterModel', () => {
'build-linux64-base-toolchains/debug',
'(bb)',
],
group_state: ['expanded'],
group_state: 'expanded',
});
});
it('should parse job field filters', () => {
window.location.hash = '?repo=mozilla-inbound&filter-job_type_name=mochi';
const urlParams = FilterModel.getUrlParamsWithDefaults();
const urlParams = getFilterUrlParamsWithDefaults();
expect(urlParams).toEqual({
repo: ['mozilla-inbound'],
@ -113,7 +119,7 @@ describe('FilterModel', () => {
window.location.hash =
'?repo=mozilla-inbound&resultStatus=testfailed,busted,exception,success,retry,runnable&' +
'searchStr=linux,x64,debug,build-linux64-base-toolchains%2Fdebug,(bb)';
const urlParams = FilterModel.getUrlParamsWithDefaults();
const urlParams = getFilterUrlParamsWithDefaults();
expect(urlParams).toEqual({
repo: ['mozilla-inbound'],
@ -140,7 +146,7 @@ describe('FilterModel', () => {
it('should preserve the case in email addresses', () => {
window.location.hash =
'?repo=mozilla-inbound&author=VYV03354@nifty.ne.jp';
const urlParams = FilterModel.getUrlParamsWithDefaults();
const urlParams = getFilterUrlParamsWithDefaults();
expect(urlParams).toEqual({
repo: ['mozilla-inbound'],

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

@ -0,0 +1,258 @@
import React from 'react';
import { Provider } from 'react-redux';
import {
render,
cleanup,
fireEvent,
waitForElement,
} from 'react-testing-library';
import PushJobs from '../../../../ui/job-view/pushes/PushJobs';
import FilterModel from '../../../../ui/models/filter';
import { store } from '../../../../ui/job-view/redux/store';
import { PinnedJobs } from '../../../../ui/job-view/context/PinnedJobs';
import { getUrlParam, setUrlParam } from '../../../../ui/helpers/location';
import JobModel from '../../../../ui/models/job';
const testPush = {
id: 494796,
revision: '1252c6014d122d48c6782310d5c3f4ae742751cb',
author: 'reviewbot',
revisions: [
{
result_set_id: 494796,
repository_id: 4,
revision: '1252c6014d122d48c6782310d5c3f4ae742751cb',
author: 'pulselistener',
comments:
'try_task_config for code-review\nDifferential Diff: PHID-DIFF-iql6zm5yinpmva7jhjln',
},
],
revision_count: 10,
push_timestamp: 1560354779,
repository_id: 4,
jobsLoaded: true,
};
const testPlatforms = [
{
name: 'Linux x64',
option: 'opt',
groups: [
{
name: 'Coverity Static Analysis',
tier: 2,
symbol: 'coverity',
mapKey: '494796coverity2linux64opt',
jobs: [
new JobModel({
build_architecture: '-',
build_os: '-',
build_platform: 'linux64',
build_platform_id: 106,
build_system_type: 'taskcluster',
end_timestamp: 1560356302,
failure_classification_id: 1,
id: 250970255,
job_group_description: '',
job_group_id: 947,
job_group_name: 'Coverity Static Analysis',
job_group_symbol: 'coverity',
job_guid: '2d180d39-8ac5-4200-995b-3f5c7b614596/0',
job_type_description: '',
job_type_id: 190421,
job_type_name: 'source-test-coverity-coverity',
job_type_symbol: 'cvsa',
last_modified: '2019-06-12T16:18:26.649628',
machine_name: 'i-0a2f82a56303c8ec2',
machine_platform_architecture: '-',
machine_platform_os: '-',
option_collection_hash: '102210fe594ee9b33d82058545b1ed14f4c8206e',
platform: 'linux64',
push_id: 494796,
reason: 'scheduled',
ref_data_name: '7542013e03efecbabf4b0bb931646f4fbff3a413',
result: 'success',
result_set_id: 494796,
signature: '7542013e03efecbabf4b0bb931646f4fbff3a413',
start_timestamp: 1560354928,
state: 'completed',
submit_timestamp: 1560354914,
tier: 2,
who: 'reviewbot@noreply.mozilla.org',
platform_option: 'opt',
visible: true,
selected: false,
}),
],
visible: true,
},
],
},
{
name: 'Gecko Decision Task',
option: 'opt',
groups: [
{
name: 'unknown',
tier: 1,
symbol: '',
mapKey: '4947961gecko-decisionopt',
jobs: [
new JobModel({
build_architecture: '-',
build_os: '-',
build_platform: 'gecko-decision',
build_platform_id: 107,
build_system_type: 'taskcluster',
end_timestamp: 1560354927,
failure_classification_id: 1,
id: 250970109,
job_group_description: '',
job_group_id: 2,
job_group_name: 'unknown',
job_group_symbol: '?',
job_guid: '7dd39d25-8990-44d2-8ba4-b2a3b319cc4d/0',
job_type_description: '',
job_type_id: 6689,
job_type_name: 'Gecko Decision Task',
job_type_symbol: 'D',
last_modified: '2019-06-12T15:55:29.549008',
machine_name: 'i-080c18493f1aa3d95',
machine_platform_architecture: '-',
machine_platform_os: '-',
option_collection_hash: '102210fe594ee9b33d82058545b1ed14f4c8206e',
platform: 'gecko-decision',
push_id: 494796,
reason: 'scheduled',
ref_data_name: '2aa083621bb989d6acf1151667288d5fe9616178',
result: 'success',
result_set_id: 494796,
signature: '2aa083621bb989d6acf1151667288d5fe9616178',
start_timestamp: 1560354846,
state: 'completed',
submit_timestamp: 1560354844,
tier: 1,
who: 'reviewbot@noreply.mozilla.org',
platform_option: 'opt',
visible: true,
selected: false,
}),
],
visible: true,
},
],
},
{
name: 'Linting',
option: 'opt',
groups: [
{
name: 'unknown',
tier: 1,
symbol: '',
mapKey: '4947961lintopt',
jobs: [
new JobModel({
build_architecture: '-',
build_os: '-',
build_platform: 'lint',
build_platform_id: 144,
build_system_type: 'taskcluster',
end_timestamp: 1560355013,
failure_classification_id: 1,
id: 250970251,
job_group_description: '',
job_group_id: 2,
job_group_name: 'unknown',
job_group_symbol: '?',
job_guid: '5df35b83-aff9-4ddf-b8c3-48eff52736f3/0',
job_type_description: '',
job_type_id: 114754,
job_type_name: 'source-test-mozlint-codespell',
job_type_symbol: 'spell',
last_modified: '2019-06-12T15:56:54.537683',
machine_name: 'i-081959e7fae55d041',
machine_platform_architecture: '-',
machine_platform_os: '-',
option_collection_hash: '102210fe594ee9b33d82058545b1ed14f4c8206e',
platform: 'lint',
push_id: 494796,
reason: 'scheduled',
ref_data_name: '6c2e8db7978ca4d5c0e38522552da4bc9b2e6b8b',
result: 'success',
result_set_id: 494796,
signature: '6c2e8db7978ca4d5c0e38522552da4bc9b2e6b8b',
start_timestamp: 1560354928,
state: 'completed',
submit_timestamp: 1560354914,
tier: 1,
who: 'reviewbot@noreply.mozilla.org',
platform_option: 'opt',
visible: true,
selected: false,
}),
],
visible: true,
},
],
},
];
afterEach(() => {
cleanup();
setUrlParam('selectedJob', null);
});
const testPushJobs = filterModel => (
<Provider store={store}>
<PinnedJobs>
<PushJobs
push={testPush}
platforms={testPlatforms}
repoName="try"
filterModel={filterModel}
pushGroupState=""
toggleSelectedRunnableJob={() => {}}
runnableVisible={false}
duplicateJobsVisible={false}
groupCountsExpanded={false}
/>
</PinnedJobs>
,
</Provider>
);
test('select a job updates url', async () => {
const { getByText } = render(testPushJobs(new FilterModel()));
const spell = getByText('spell');
expect(spell).toBeInTheDocument();
fireEvent.mouseDown(spell);
expect(spell).toHaveClass('selected-job');
const selJobId = getUrlParam('selectedJob');
expect(selJobId).toBe('250970251');
});
test('filter change keeps selected job visible', async () => {
const filterModel = new FilterModel();
const { getByText, rerender } = render(testPushJobs(filterModel));
const spell = await waitForElement(() => getByText('spell'));
expect(spell).toBeInTheDocument();
fireEvent.mouseDown(spell);
expect(spell).toHaveClass('selected-job');
filterModel.addFilter('searchStr', 'linux');
rerender(testPushJobs(new FilterModel()));
const spell2 = getByText('spell');
expect(spell2).toBeInTheDocument();
expect(spell2).toHaveClass('filter-shown');
expect(spell2).toHaveClass('selected-job');
});

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

@ -5,7 +5,7 @@ import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons';
import { faStar as faStarSolid } from '@fortawesome/free-solid-svg-icons';
import { getBtnClass, findJobInstance } from '../../helpers/job';
import { getUrlParam } from '../../helpers/location';
import { getSelectedJobId, getUrlParam } from '../../helpers/location';
export default class JobButtonComponent extends React.Component {
constructor(props) {
@ -53,7 +53,7 @@ export default class JobButtonComponent extends React.Component {
}
setSelected(isSelected) {
const { job, platform, filterPlatformCb, filterModel } = this.props;
const { job, filterPlatformCb, filterModel } = this.props;
// if a job was just classified, and we are in unclassified only mode,
// then the job no longer meets the filter criteria. However, if it
// is still selected, then it should stay visible so that next/previous
@ -64,7 +64,7 @@ export default class JobButtonComponent extends React.Component {
this.setState({ isSelected });
// filterPlatformCb will keep a job and platform visible if it contains
// the selected job, so we must pass in if this job is selected or not.
filterPlatformCb(platform, isSelected ? job.id : null);
filterPlatformCb(isSelected ? job.id : null);
}
toggleRunnableSelected() {
@ -74,9 +74,9 @@ export default class JobButtonComponent extends React.Component {
}
refilter() {
const { filterPlatformCb, platform } = this.props;
const { filterPlatformCb } = this.props;
filterPlatformCb(platform);
filterPlatformCb(getSelectedJobId());
}
render() {
@ -154,7 +154,6 @@ JobButtonComponent.propTypes = {
repoName: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired,
resultStatus: PropTypes.string.isRequired,
platform: PropTypes.object.isRequired,
filterPlatformCb: PropTypes.func.isRequired,
failureClassificationId: PropTypes.number, // runnable jobs won't have this
};

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

@ -115,7 +115,6 @@ export class JobGroupComponent extends React.Component {
const {
repoName,
filterPlatformCb,
platform,
filterModel,
group: {
name: groupName,
@ -147,7 +146,6 @@ export class JobGroupComponent extends React.Component {
failureClassificationId={job.failure_classification_id}
repoName={repoName}
filterPlatformCb={filterPlatformCb}
platform={platform}
key={job.id}
/>
))}
@ -179,7 +177,6 @@ JobGroupComponent.propTypes = {
repoName: PropTypes.string.isRequired,
filterModel: PropTypes.object.isRequired,
filterPlatformCb: PropTypes.func.isRequired,
platform: PropTypes.object.isRequired,
pushGroupState: PropTypes.string.isRequired,
duplicateJobsVisible: PropTypes.bool.isRequired,
groupCountsExpanded: PropTypes.bool.isRequired,

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

@ -11,7 +11,6 @@ export default class JobsAndGroups extends React.Component {
const {
groups,
repoName,
platform,
filterPlatformCb,
filterModel,
pushGroupState,
@ -30,7 +29,6 @@ export default class JobsAndGroups extends React.Component {
repoName={repoName}
filterModel={filterModel}
filterPlatformCb={filterPlatformCb}
platform={platform}
key={group.mapKey}
pushGroupState={pushGroupState}
duplicateJobsVisible={duplicateJobsVisible}
@ -48,7 +46,6 @@ export default class JobsAndGroups extends React.Component {
resultStatus={getStatus(job)}
failureClassificationId={job.failure_classification_id}
filterPlatformCb={filterPlatformCb}
platform={platform}
key={job.id}
/>
));
@ -63,7 +60,6 @@ JobsAndGroups.propTypes = {
repoName: PropTypes.string.isRequired,
filterModel: PropTypes.object.isRequired,
filterPlatformCb: PropTypes.func.isRequired,
platform: PropTypes.object.isRequired,
pushGroupState: PropTypes.string.isRequired,
duplicateJobsVisible: PropTypes.bool.isRequired,
groupCountsExpanded: PropTypes.bool.isRequired,

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

@ -99,7 +99,6 @@ export default class Platform extends React.PureComponent {
groups={filteredPlatform.groups}
repoName={repoName}
filterPlatformCb={this.filterCb}
platform={filteredPlatform}
filterModel={filterModel}
pushGroupState={pushGroupState}
duplicateJobsVisible={duplicateJobsVisible}

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

@ -13,39 +13,56 @@ import {
thMatchType,
thFilterDefaults,
deprecated_thFilterPrefix,
allFilterParams,
} from '../helpers/filter';
import { getAllUrlParams } from '../helpers/location';
export const getNonFilterUrlParams = () =>
[...getAllUrlParams().entries()].reduce(
(acc, [urlField, urlValue]) =>
allFilterParams.includes(urlField.replace(deprecated_thFilterPrefix, ''))
? acc
: { ...acc, [urlField]: urlValue },
{},
);
export const getFilterUrlParamsWithDefaults = () => {
// Group multiple values for the same field into an array of values.
// This handles the transition from our old url params to this newer, more
// terse version.
// Also remove usage of the 'filter-' prefix.
const groupedValues = [...getAllUrlParams().entries()].reduce(
(acc, [urlField, urlValue]) => {
const field = urlField.replace(deprecated_thFilterPrefix, '');
if (!allFilterParams.includes(field)) {
return acc;
}
const value =
field === 'author' ? [urlValue] : urlValue.toLowerCase().split(/,| /);
return field in acc
? { ...acc, [field]: [...acc[field], ...value] }
: { ...acc, [field]: value };
},
{},
);
return { ...cloneDeep(thFilterDefaults), ...groupedValues };
};
export default class FilterModel {
constructor() {
this.urlParams = FilterModel.getUrlParamsWithDefaults();
}
static getUrlParamsWithDefaults() {
// Group multiple values for the same field into an array of values.
// This handles the transition from our old url params to this newer, more
// terse version.
// Also remove usage of the 'filter-' prefix.
const groupedValues = [...getAllUrlParams().entries()].reduce(
(acc, [urlField, urlValue]) => {
const field = urlField.replace(deprecated_thFilterPrefix, '');
const value =
field === 'author' ? [urlValue] : urlValue.toLowerCase().split(/,| /);
return field in acc
? { ...acc, [field]: [...acc[field], ...value] }
: { ...acc, [field]: value };
},
{},
);
return { ...cloneDeep(thFilterDefaults), ...groupedValues };
this.urlParams = getFilterUrlParamsWithDefaults();
}
// If a param matches the defaults, then don't include it.
getUrlParamsWithoutDefaults = () => {
// ensure the repo param is always set
const params = { repo: thDefaultRepo, ...this.urlParams };
const params = {
repo: thDefaultRepo,
...getNonFilterUrlParams(),
...this.urlParams,
};
return Object.entries(params).reduce(
(acc, [field, value]) =>