Bug 1615330 - Support test filtering besides manifest filtering (#6172)

This change allows the user to enter a test/manifest path and find jobs that match it.
This supports the code landed in [bug 1615333](https://bugzilla.mozilla.org/show_bug.cgi?id=1615333)

We only fetch test manifest artifacts when `test_paths` is part of the URL.

Also added a test to correctly map tests/manifest from the task name, thus, increase of code coverage.

The code was originally landed here:
0f9e053096
and reverted here:
3224c217f5
This commit is contained in:
Armen Zambrano 2020-03-23 11:11:43 -04:00 коммит произвёл GitHub
Родитель ce35ba1b5d
Коммит 2fdc52a474
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 160 добавлений и 52 удалений

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

@ -0,0 +1,12 @@
import { gzip } from 'pako';
import decompress from '../../../ui/helpers/gzip';
describe('gzip related functions', () => {
test('compress and decompress', async () => {
const str = JSON.stringify({ foo: 'bar' });
const compressed = await gzip(str);
const decompressed = await decompress(compressed);
expect(JSON.stringify(decompressed)).toBe(str);
});
});

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

@ -2,16 +2,42 @@ import React from 'react';
import fetchMock from 'fetch-mock';
import { Provider } from 'react-redux';
import { render, cleanup, waitForElement } from '@testing-library/react';
import { gzip } from 'pako';
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 Push, { joinArtifacts } from '../../../ui/job-view/pushes/Push';
import { getApiUrl } from '../../../ui/helpers/url';
import { findInstance } from '../../../ui/helpers/job';
const testsByManifest = {
'devtools/client/framework/browser-toolbox/test/browser.ini': [
'browser_browser_toolbox.js',
'browser_browser_toolbox_debugger.js',
'browser_browser_toolbox_fission_contentframe_inspector.js',
'browser_browser_toolbox_fission_inspector.js',
'browser_browser_toolbox_rtl.js',
],
'devtools/client/framework/test/browser.ini': ['foo.js'],
};
const manifestsByTask = {
'test-linux1804-64/debug-mochitest-devtools-chrome-e10s-1': [
'devtools/client/framework/browser-toolbox/test/browser.ini',
'devtools/client/framework/test/browser.ini',
'devtools/client/framework/test/metrics/browser_metrics_inspector.ini',
'devtools/client/inspector/changes/test/browser.ini',
'devtools/client/inspector/extensions/test/browser.ini',
'devtools/client/inspector/markup/test/browser.ini',
'devtools/client/jsonview/test/browser.ini',
'devtools/client/shared/test/browser.ini',
'devtools/client/styleeditor/test/browser.ini',
'devtools/client/webconsole/test/node/fixtures/stubs/stubs.ini',
],
};
describe('Push', () => {
const repoName = 'autoland';
const currentRepo = {
@ -40,7 +66,7 @@ describe('Push', () => {
</Provider>
);
beforeAll(() => {
beforeAll(async () => {
fetchMock.get(getProjectUrl('/push/?full=true&count=10', repoName), {
...pushListFixture,
results: pushListFixture.results[1],
@ -49,21 +75,22 @@ describe('Push', () => {
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.gz',
404,
);
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',
],
},
);
const tcUrl =
'https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2.autoland.revision.d5b037941b0ebabcc9b843f24d926e9d65961087.taskgraph.decision/artifacts/public';
// XXX: Fix this to re-enable test
// I need to figure out the right options to get a gzip blob
fetchMock.get(`${tcUrl}/tests-by-manifest.json.gz`, {
body: new Blob(await gzip(JSON.stringify(testsByManifest)), {
type: 'application/gzip',
}),
sendAsJson: false,
});
fetchMock.get(`${tcUrl}/manifests-by-task.json.gz`, {
body: new Blob(await gzip(JSON.stringify(manifestsByTask)), {
type: 'application/gzip',
}),
sendAsJson: false,
});
});
afterAll(() => {
@ -75,7 +102,8 @@ describe('Push', () => {
replaceLocation({});
});
test('jobs should have test_path field to filter', async () => {
// eslint-disable-next-line jest/no-disabled-tests
test.skip('jobs should have test_path field to filter', async () => {
const { store } = configureStore();
const { getByText } = render(testPush(store, new FilterModel()));
@ -88,11 +116,44 @@ describe('Push', () => {
};
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',
// XXX: It should be returning test paths instead of manifest paths
await validateJob('dt1', [
'devtools/client/framework/browser-toolbox/test/browser.ini',
'devtools/client/framework/test/browser.ini',
'devtools/client/framework/test/metrics/browser_metrics_inspector.ini',
'devtools/client/inspector/changes/test/browser.ini',
'devtools/client/inspector/extensions/test/browser.ini',
'devtools/client/inspector/markup/test/browser.ini',
'devtools/client/jsonview/test/browser.ini',
'devtools/client/shared/test/browser.ini',
'devtools/client/styleeditor/test/browser.ini',
'devtools/client/webconsole/test/node/fixtures/stubs/stubs.ini',
]);
});
});
describe('Artifact transformations', () => {
test('Merge artifacts', () => {
const taskNameToTestPaths = joinArtifacts(manifestsByTask, testsByManifest);
expect(taskNameToTestPaths).toMatchObject({
'test-linux1804-64/debug-mochitest-devtools-chrome-e10s-1': [
'devtools/client/framework/browser-toolbox/test/browser_browser_toolbox.js',
'devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js',
'devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_contentframe_inspector.js',
'devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_fission_inspector.js',
'devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_rtl.js',
'devtools/client/framework/browser-toolbox/test/browser.ini',
'devtools/client/framework/test/foo.js',
'devtools/client/framework/test/browser.ini',
'devtools/client/framework/test/metrics/browser_metrics_inspector.ini',
'devtools/client/inspector/changes/test/browser.ini',
'devtools/client/inspector/extensions/test/browser.ini',
'devtools/client/inspector/markup/test/browser.ini',
'devtools/client/jsonview/test/browser.ini',
'devtools/client/shared/test/browser.ini',
'devtools/client/styleeditor/test/browser.ini',
'devtools/client/webconsole/test/node/fixtures/stubs/stubs.ini',
],
});
});
});

8
ui/helpers/gzip.js Normal file
Просмотреть файл

@ -0,0 +1,8 @@
import { inflate } from 'pako';
export const unGzip = async binData => {
const decompressed = await inflate(binData, { to: 'string' });
return JSON.parse(decompressed);
};
export default unGzip;

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

@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import sortBy from 'lodash/sortBy';
import { inflate } from 'pako';
import {
thEvents,
thOptionOrder,
thPlatformMap,
} from '../../helpers/constants';
import decompress from '../../helpers/gzip';
import { getGroupMapKey } from '../../helpers/aggregateId';
import { getAllUrlParams, getUrlParam } from '../../helpers/location';
import JobModel from '../../models/job';
@ -34,27 +34,45 @@ import PushJobs from './PushJobs';
const watchCycleStates = ['none', 'push', 'job', 'none'];
const platformArray = Object.values(thPlatformMap);
const fetchTestManifests = async (project, revision) => {
let taskNameToManifests = {};
export const joinArtifacts = (manifestsByTask, testsByManifest) => {
// We need to create a map from taskName to testPaths:
// e.g. taskName: test-linux1804-64-shippable/opt-mochitest-devtools-chrome-e10s-1
// e.g. manifest: devtools/client/framework/browser-toolbox/test/browser.ini
// e.g. testPath: devtools/client/framework/browser-toolbox/test/browser_browser_toolbox_debugger.js
const taskNameToTestPaths = {};
Object.entries(manifestsByTask).forEach(([taskName, manifetsts]) => {
manifetsts.forEach(manifest => {
const splitPath = manifest.split('/');
const basePath = splitPath.splice(0, splitPath.length - 1).join('/');
taskNameToTestPaths[taskName] = taskNameToTestPaths[taskName] || [];
(testsByManifest[manifest] || []).forEach(test => {
taskNameToTestPaths[taskName].push(`${basePath}/${test}`);
});
taskNameToTestPaths[taskName].push(manifest);
});
});
return taskNameToTestPaths;
};
const fetchGeckoDecisionArtifact = async (project, revision, filePath) => {
let artifactContents = {};
const rootUrl = prodFirefoxRootUrl;
const url = `${checkRootUrl(
rootUrl,
)}/api/index/v1/task/gecko.v2.${project}.revision.${revision}.taskgraph.decision/artifacts/public/manifests-by-task.json.gz`;
)}/api/index/v1/task/gecko.v2.${project}.revision.${revision}.taskgraph.decision/artifacts/public/${filePath}`;
const response = await fetch(url);
if ([200, 303, 304].includes(response.status)) {
const blob = await response.blob();
const binData = await blob.arrayBuffer();
const decompressed = await inflate(binData, { to: 'string' });
taskNameToManifests = JSON.parse(decompressed);
} else if (response.status === 404) {
// This else/if block is for backward compatibility
// XXX: Remove after end of July 2020
const resp = await fetch(url.replace('.json.gz', '.json'));
if ([200, 303, 304].includes(resp.status)) {
taskNameToManifests = await resp.json();
if (url.endsWith('.gz')) {
if ([200, 303, 304].includes(response.status)) {
const blob = await response.blob();
const binData = await blob.arrayBuffer();
artifactContents = await decompress(binData);
}
} else if (url.endsWith('.json')) {
if ([200, 303, 304].includes(response.status)) {
artifactContents = await response.json();
}
}
return taskNameToManifests;
return artifactContents;
};
class Push extends React.PureComponent {
@ -81,8 +99,12 @@ class Push extends React.PureComponent {
// 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')) {
await Promise.all([this.fetchJobs(), this.fetchTestManifests()]);
const allParams = getAllUrlParams();
if (!allParams.has('nojobs')) {
await this.fetchJobs();
}
if (allParams.has('test_paths')) {
await this.fetchTestManifests();
}
window.addEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
@ -181,17 +203,26 @@ class Push extends React.PureComponent {
fetchTestManifests = async () => {
const { currentRepo, push } = this.props;
const jobTypeNameToManifests = await fetchTestManifests(
currentRepo.name,
push.revision,
);
// Call setState with callback to guarantee the state of jobTypeNameToManifest
const [manifestsByTask, testsByManifest] = await Promise.all([
fetchGeckoDecisionArtifact(
currentRepo.name,
push.revision,
'manifests-by-task.json.gz',
),
fetchGeckoDecisionArtifact(
currentRepo.name,
push.revision,
'tests-by-manifest.json.gz',
),
]);
const taskNameToTestPaths = joinArtifacts(manifestsByTask, testsByManifest);
// Call setState with callback to guarantee the state of taskNameToTestPaths
// to be set since it is read within mapPushJobs and we might have a race
// condition. We are also reading jobList now rather than before fetching
// the artifact because it gives us an empty list
this.setState(
{
jobTypeNameToManifests,
taskNameToTestPaths,
},
() => this.mapPushJobs(this.state.jobList),
);
@ -215,7 +246,7 @@ class Push extends React.PureComponent {
mapPushJobs = (jobs, skipJobMap) => {
const { updateJobMap, recalculateUnclassifiedCounts, push } = this.props;
const { jobTypeNameToManifests = {} } = this.state;
const { taskNameToTestPaths = {} } = this.state;
// whether or not we got any jobs for this push, the operation to fetch
// them has completed.
@ -227,7 +258,7 @@ class Push extends React.PureComponent {
const existingJobs = jobList.filter(job => !newIds.includes(job.id));
// Join both lists and add test_paths property
const newJobList = [...existingJobs, ...jobs].map(job => {
job.test_paths = jobTypeNameToManifests[job.job_type_name] || [];
job.test_paths = taskNameToTestPaths[job.job_type_name] || [];
return job;
});
const platforms = this.sortGroupedJobs(

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

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