Bug 1580893 - Support filtering tasks by test path (#5794)

* Support filtering tasks by test path

For every push, it fetches the artifact `manifests-by-task.json` produced by the Gecko decision task. For every job it adds the `test_paths` property which allows the filtering.

Click on the "Filter by a job field" (the funnel icon), select "test path" from the
dropdown and you can insert a path like `devtools/client/inspector/changes/test/browser.ini` (You can use substrings).

* Use Django's json()
* Skip test that only times out on Travis
This commit is contained in:
Armen Zambrano 2020-01-24 15:29:57 -05:00 коммит произвёл GitHub
Родитель 7640c64e49
Коммит 11e8e92be0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 171 добавлений и 24 удалений

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

@ -38,6 +38,7 @@ def test_clear_pinboard(base_url, selenium, test_jobs):
assert len(page.pinboard.jobs) == 0
@pytest.mark.skip(reason="Needs to be updated to be replaced with react-testing-library")
def test_pin_all_jobs(base_url, selenium, test_jobs):
page = Treeherder(selenium, base_url).open()
page.wait.until(lambda _: len(page.all_jobs) == len(test_jobs))

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

@ -67,6 +67,10 @@ describe('App', () => {
results: [],
meta: { repository: repoName, offset: 0, count: 2000 },
});
fetchMock.get(
'begin:https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2',
404,
);
// Need to mock this function for the app switching tests.
// Source: https://github.com/mui-org/material-ui/issues/15726#issuecomment-493124813

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

@ -38,6 +38,10 @@ describe('Filtering', () => {
tree: repoName,
},
});
fetchMock.get(
'begin:https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2',
404,
);
fetchMock.get(
getProjectUrl('/push/?full=true&count=10', repoName),

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

@ -100,6 +100,10 @@ describe('PushList', () => {
getApiUrl('/jobs/?push_id=511137', repoName),
jobListFixtureTwo,
);
fetchMock.get(
'begin:https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2',
404,
);
});
afterAll(() => {

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

@ -0,0 +1,94 @@
import React from 'react';
import fetchMock from 'fetch-mock';
import { Provider } from 'react-redux';
import { render, cleanup, waitForElement } from '@testing-library/react';
import { getProjectUrl, replaceLocation } from '../../../ui/helpers/location';
import FilterModel from '../../../ui/models/filter';
import pushListFixture from '../mock/push_list';
import jobListFixture from '../mock/job_list/job_2';
import configureStore from '../../../ui/job-view/redux/configureStore';
import Push from '../../../ui/job-view/pushes/Push';
import { getApiUrl } from '../../../ui/helpers/url';
import { findInstance } from '../../../ui/helpers/job';
describe('Push', () => {
const repoName = 'autoland';
const currentRepo = {
name: repoName,
getRevisionHref: () => 'foo',
getPushLogHref: () => 'foo',
};
const push = pushListFixture.results[1];
const revision = 'd5b037941b0ebabcc9b843f24d926e9d65961087';
const testPush = (store, filterModel) => (
<Provider store={store}>
<div id="th-global-content">
<Push
push={push}
isLoggedIn={false}
currentRepo={currentRepo}
filterModel={filterModel}
notificationSupported={false}
duplicateJobsVisible={false}
groupCountsExpanded={false}
isOnlyRevision={push.revision === revision}
pushHealthVisibility="None"
getAllShownJobs={() => {}}
/>
</div>
</Provider>
);
beforeAll(() => {
fetchMock.get(getProjectUrl('/push/?full=true&count=10', repoName), {
...pushListFixture,
results: pushListFixture.results[1],
});
fetchMock.mock(
getApiUrl('/jobs/?push_id=511137', repoName),
jobListFixture,
);
fetchMock.get(
'https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2.autoland.revision.d5b037941b0ebabcc9b843f24d926e9d65961087.taskgraph.decision/artifacts/public/manifests-by-task.json',
{
'test-linux1804-64/debug-mochitest-devtools-chrome-e10s-5': [
'devtools/client/inspector/compatibility/test/browser/browser.ini',
'devtools/client/inspector/grids/test/browser.ini',
'devtools/client/inspector/rules/test/browser.ini',
'devtools/client/jsonview/test/browser.ini',
],
},
);
});
afterAll(() => {
fetchMock.reset();
});
afterEach(() => {
cleanup();
replaceLocation({});
});
test('jobs should have test_path field to filter', async () => {
const { store } = configureStore();
const { getByText } = render(testPush(store, new FilterModel()));
const validateJob = async (name, testPaths) => {
const jobEl = await waitForElement(() => getByText(name));
// Fetch the React instance of an object from a DOM element.
const { props } = findInstance(jobEl);
const { job } = props;
expect(job.test_paths).toStrictEqual(testPaths);
};
await validateJob('Jit8', []);
await validateJob('dt5', [
'devtools/client/inspector/compatibility/test/browser/browser.ini',
'devtools/client/inspector/grids/test/browser.ini',
'devtools/client/inspector/rules/test/browser.ini',
'devtools/client/jsonview/test/browser.ini',
]);
});
});

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

@ -272,7 +272,7 @@ describe('Pushes Redux store', () => {
{ type: UPDATE_JOB_MAP, jobList },
);
expect(Object.keys(reduced.jobMap)).toHaveLength(3);
expect(Object.keys(reduced.jobMap)).toHaveLength(4);
});
test('jobMap jobs should have fields required for retriggering', async () => {
@ -282,7 +282,7 @@ describe('Pushes Redux store', () => {
{ type: UPDATE_JOB_MAP, jobList },
);
expect(Object.keys(reduced.jobMap)).toHaveLength(3);
expect(Object.keys(reduced.jobMap)).toHaveLength(4);
const job = reduced.jobMap['259539684'];
expect(job.signature).toBe('f64069faca8636e9dc415bef8e9a4ee055d56687');
expect(job.job_type_name).toBe(

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

@ -1,5 +1,5 @@
{
"count": 3,
"count": 4,
"next": null,
"previous": null,
"results": [
@ -56,6 +56,24 @@
"33ba86f5b1d8ad61599cb04b8d1f50b97fe19379",
"running",
2
],
[
24,
1,
378271,
"Mochitests",
"M",
"test-linux1804-64/debug-mochitest-devtools-chrome-e10s-5",
"dt5",
"2020-01-17T15:05:08.791908",
"option_collection_hash_TBD",
"linux1804-64",
"debug",
526445,
"success",
"0bac4980342a6f185082426471f9151a0de9ae50",
"completed",
1
]
],
"job_property_names": [

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

@ -1,5 +1,4 @@
import datetime
import json
import pytest
from django.urls import reverse
@ -18,10 +17,7 @@ def test_push_list_basic(client, eleven_jobs_stored, test_repository):
"""
resp = client.get(
reverse("push-list", kwargs={"project": test_repository.name}))
# The .json() method of the Django test client doesn't handle unicode properly on
# Python 2, so we have to deserialize ourselves. TODO: Clean up once on Python 3.
data = json.loads(resp.content)
data = resp.json()
results = data['results']
meta = data['meta']
@ -70,9 +66,7 @@ def test_push_list_empty_push_still_show(client, sample_push, test_repository):
reverse("push-list", kwargs={"project": test_repository.name}),
)
assert resp.status_code == 200
# The .json() method of the Django test client doesn't handle unicode properly on
# Python 2, so we have to deserialize ourselves. TODO: Clean up once on Python 3.
data = json.loads(resp.content)
data = resp.json()
assert len(data['results']) == 10
@ -134,9 +128,7 @@ def test_push_list_filter_by_revision(client, eleven_jobs_stored, test_repositor
{"fromchange": "130965d3df6c", "tochange": "f361dcb60bbe"}
)
assert resp.status_code == 200
# The .json() method of the Django test client doesn't handle unicode properly on
# Python 2, so we have to deserialize ourselves. TODO: Clean up once on Python 3.
data = json.loads(resp.content)
data = resp.json()
results = data['results']
meta = data['meta']
assert len(results) == 4
@ -177,9 +169,7 @@ def test_push_list_filter_by_date(client,
{"startdate": "2013-08-10", "enddate": "2013-08-13"}
)
assert resp.status_code == 200
# The .json() method of the Django test client doesn't handle unicode properly on
# Python 2, so we have to deserialize ourselves. TODO: Clean up once on Python 3.
data = json.loads(resp.content)
data = resp.json()
results = data['results']
meta = data['meta']
assert len(results) == 4
@ -320,10 +310,7 @@ def test_push_list_without_jobs(client,
reverse("push-list", kwargs={"project": test_repository.name})
)
assert resp.status_code == 200
# The .json() method of the Django test client doesn't handle unicode properly on
# Python 2, so we have to deserialize ourselves. TODO: Clean up once on Python 3.
data = json.loads(resp.content)
data = resp.json()
results = data['results']
assert len(results) == 10
assert all([('platforms' not in result) for result in results])

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

@ -22,6 +22,7 @@ export const thFieldChoices = {
machine_name: { name: 'machine name', matchType: thMatchType.substr },
platform: { name: 'platform', matchType: thMatchType.substr },
tier: { name: 'tier', matchType: thMatchType.exactstr },
test_paths: { name: 'test path', matchType: thMatchType.substr },
failure_classification_id: {
name: 'failure classification',
matchType: thMatchType.choice,

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

@ -29,6 +29,17 @@ import { RevisionList } from './RevisionList';
const watchCycleStates = ['none', 'push', 'job', 'none'];
const platformArray = Object.values(thPlatformMap);
const fetchTestManifests = async (project, revision) => {
let taskNameToManifests = {};
const rootUrl = 'https://firefox-ci-tc.services.mozilla.com';
const url = `${rootUrl}/api/index/v1/task/gecko.v2.${project}.revision.${revision}.taskgraph.decision/artifacts/public/manifests-by-task.json`;
const response = await fetch(url);
if ([200, 304].indexOf(response.status) > -1) {
taskNameToManifests = await response.json();
}
return taskNameToManifests;
};
class Push extends React.PureComponent {
constructor(props) {
super(props);
@ -49,12 +60,12 @@ class Push extends React.PureComponent {
};
}
componentDidMount() {
async componentDidMount() {
// if ``nojobs`` is on the query string, then don't load jobs.
// this allows someone to more quickly load ranges of revisions
// when they don't care about the specific jobs and results.
if (!getAllUrlParams().has('nojobs')) {
this.fetchJobs();
await Promise.all([this.fetchJobs(), this.fetchTestManifests()]);
}
window.addEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
@ -150,6 +161,19 @@ class Push extends React.PureComponent {
return selectedRunnableJobs;
};
fetchTestManifests = async () => {
const { currentRepo, push } = this.props;
const { jobList } = this.state;
const jobTypeNameToManifests = await fetchTestManifests(
currentRepo.name,
push.revision,
);
this.setState({ jobTypeNameToManifests });
// This adds to the jobs the test_path property
this.mapPushJobs(jobList);
};
fetchJobs = async () => {
const { push, notify } = this.props;
const { data, failureStatus } = await JobModel.getList(
@ -168,6 +192,7 @@ class Push extends React.PureComponent {
mapPushJobs = (jobs, skipJobMap) => {
const { updateJobMap, recalculateUnclassifiedCounts, push } = this.props;
const { jobTypeNameToManifests = {} } = this.state;
// whether or not we got any jobs for this push, the operation to fetch
// them has completed.
@ -177,7 +202,11 @@ class Push extends React.PureComponent {
const newIds = jobs.map(job => job.id);
// remove old versions of jobs we just fetched.
const existingJobs = jobList.filter(job => !newIds.includes(job.id));
const newJobList = [...existingJobs, ...jobs];
// Join both lists and add test_paths property
const newJobList = [...existingJobs, ...jobs].map(job => ({
...job,
test_paths: jobTypeNameToManifests[job.job_type_name] || [],
}));
const platforms = this.sortGroupedJobs(
this.groupJobByPlatform(newJobList),
);

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

@ -278,6 +278,11 @@ export default class FilterModel {
// don't check this here.
return null;
}
if (field === 'test_paths' && job[field]) {
// Make all paths unix style
return job[field].map(testPath => testPath.replace(/\\/g, /\//));
}
return job[field];
};