зеркало из https://github.com/mozilla/treeherder.git
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:
Родитель
7640c64e49
Коммит
11e8e92be0
|
@ -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];
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче