ui/helpers/job.js (251 lines of code) (raw):
import TaskclusterModel from '../models/taskcluster';
import { thFailureResults, thPlatformMap } from './constants';
import { getGroupMapKey } from './aggregateId';
import { getAllUrlParams, getRepo } from './location';
import { getAction } from './taskcluster';
import { formatTaskclusterError } from './errorMessage';
const btnClasses = {
busted: 'btn-red',
exception: 'btn-purple',
testfailed: 'btn-orange',
usercancel: 'btn-pink',
retry: 'btn-dkblue',
success: 'btn-green',
running: 'btn-dkgray',
pending: 'btn-ltgray',
superseded: 'btn-ltblue',
failures: 'btn-red',
'in progress': 'btn-dkgray',
};
// failure classification ids that should be shown in "unclassified" mode
export const thUnclassifiedIds = [1, 6, 7];
// Get the CSS class for job buttons as well as jobs that show in the pinboard.
// These also apply to result "groupings" like ``failures`` and ``in progress``
// for the colored filter chicklets on the nav bar.
export const getBtnClass = function getBtnClass(
resultStatus,
failureClassificationId,
) {
let btnClass = btnClasses[resultStatus] || 'btn-default';
// handle if a job is classified > 1
// and not "NEW failure", classification == 6
if (failureClassificationId > 1 && failureClassificationId !== 6) {
btnClass += '-classified';
}
return btnClass;
};
export const isReftest = function isReftest(job) {
const {
job_group_name: gName,
job_type_name: jName,
job_type_symbol: jSymbol,
} = job;
return (
[gName, jName].some((name) => name.toLowerCase().includes('reftest')) ||
jSymbol.includes('wrench') ||
jName.includes('test-verify')
);
};
export const isPerfTest = function isPerfTest(job) {
return [job.job_group_name, job.job_type_name].some(
(name) =>
name.toLowerCase().includes('talos') ||
name.toLowerCase().includes('raptor') ||
name.toLowerCase().includes('browsertime') ||
name.toLowerCase().includes('perftest'),
);
};
export const canConfirmFailure = function canConfirmFailure(job) {
const confirmRepos = ['autoland', 'mozilla-central', 'try'];
const repoName = getRepo();
if (!confirmRepos.includes(repoName)) {
return false;
}
if (job.job_type_name.toLowerCase().includes('jsreftest')) {
return false;
}
return [job.job_group_name, job.job_type_name].some(
(name) =>
!name.toLowerCase().includes('source-test') &&
(name.toLowerCase().includes('crashtest') ||
name.toLowerCase().includes('mochitest') ||
name.toLowerCase().includes('reftest') ||
name.toLowerCase().includes('web-platform') ||
name.toLowerCase().includes('xpcshell')),
);
};
export const confirmFailure = async function confirmFailure(
job,
notify,
decisionTaskMap,
currentRepo,
) {
const { id: decisionTaskId } = decisionTaskMap[job.push_id];
if (!canConfirmFailure(job)) {
return;
}
if (!job.id) {
notify('Job not yet loaded for failure confirmation', 'warning');
return;
}
if (job.state !== 'completed') {
notify('Job not yet completed. Try again later.', 'warning');
return;
}
TaskclusterModel.load(decisionTaskId, job, currentRepo).then((results) => {
try {
const confirmFailureAction = getAction(
results.actions,
'confirm-failures',
);
if (!confirmFailureAction) {
notify(
'Request to confirm failure via actions.json failed could not find action.',
'danger',
{ sticky: true },
);
return;
}
return TaskclusterModel.submit({
action: confirmFailureAction,
decisionTaskId,
taskId: results.originalTaskId,
input: {},
staticActionVariables: results.staticActionVariables,
currentRepo,
}).then(
() => {
notify(
'Request sent to confirm-failures job via actions.json',
'success',
);
},
(e) => {
// The full message is too large to fit in a Treeherder
// notification box.
notify(formatTaskclusterError(e), 'danger', { sticky: true });
},
);
} catch (e) {
notify(formatTaskclusterError(e), 'danger', { sticky: true });
}
});
};
export const isClassified = function isClassified(job) {
return !thUnclassifiedIds.includes(job.failure_classification_id);
};
export const isUnclassifiedFailure = function isUnclassifiedFailure(job) {
return thFailureResults.includes(job.result) && !isClassified(job);
};
// Fetch the React instance of an object from a DOM element.
// Credit for this approach goes to SO: https://stackoverflow.com/a/48335220/333614
export const findInstance = function findInstance(el) {
const key = Object.keys(el).find((key) => key.startsWith('__reactFiber$'));
if (key) {
const fiberNode = el[key];
return fiberNode && fiberNode.return && fiberNode.return.stateNode;
}
return null;
};
// Fetch the React instance of the currently selected job.
export const findSelectedInstance = function findSelectedInstance() {
const selectedEl = document.querySelector('#push-list .job-btn.selected-job');
if (selectedEl) {
return findInstance(selectedEl);
}
};
// Check if the element is visible on screen or not.
const isOnScreen = function isOnScreen(el) {
const bounding = el.getBoundingClientRect();
const offset = el.getBoundingClientRect();
const top = offset.top + document.body.scrollTop;
const bottom = top + el.offsetHeight;
return top >= bounding.bottom && bottom <= bounding.top;
};
// Scroll the element into view.
export const scrollToElement = function scrollToElement(el) {
if (!isOnScreen(el)) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
export const findGroupElement = function findGroupElement(job) {
const {
push_id: pushId,
job_group_symbol: jobGroupSymbol,
tier,
platform,
platform_option: platformOption,
} = job;
const groupMapKey = getGroupMapKey(
pushId,
jobGroupSymbol,
tier,
platform,
platformOption,
);
return document.querySelector(
`#push-list span[data-group-key='${groupMapKey}']`,
);
};
export const findGroupInstance = function findGroupInstance(job) {
const groupEl = findGroupElement(job);
if (groupEl) {
return findInstance(groupEl);
}
};
// Fetch the React instance based on the jobId, and if scrollTo
// is true, then scroll it into view.
export const findJobInstance = function findJobInstance(jobId, scrollTo) {
const jobEl = document.querySelector(
`#push-list button[data-job-id='${jobId}']`,
);
if (jobEl) {
if (scrollTo) {
scrollToElement(jobEl);
}
return findInstance(jobEl);
}
};
export const getResultState = function getResultState(job) {
const { result, state } = job;
return state === 'completed' ? result : state;
};
export const addAggregateFields = function addAggregateFields(job) {
const {
job_group_name: jobGroupName,
platform,
platform_option: platformOption,
submit_timestamp: submitTimestamp,
start_timestamp: startTimestamp,
end_timestamp: endTimestamp,
} = job;
let { job_type_name: jobTypeName, job_type_symbol: jobTypeSymbol } = job;
job.resultStatus = getResultState(job);
// The current modification is to support backfilling of manifest based scheduling.
// A backfilled task (e.g. bc2) ends up being named bc2-<revision>-bk
// The label can also be different than the original task selected to be backfilled
// For instance 'test-linux1804-64/debug-mochitest-browser-chrome-e10s-4' can be
// 'test-linux1804-64/debug-mochitest-browser-chrome-e10s-1' yet the symbol be bc4
const parts = jobTypeName.split('-');
// This makes backfilled tasks have the same symbol as the original task
if (jobTypeSymbol.endsWith('-bk')) {
[jobTypeSymbol] = jobTypeSymbol.split('-');
}
const chunk = Number(parts.pop());
if (Number.isInteger(chunk)) {
jobTypeName = parts.join('-');
jobTypeSymbol = jobTypeSymbol.split('-').shift();
}
job.searchStr = [
thPlatformMap[platform] || platform,
platformOption,
jobGroupName === 'unknown' ? undefined : jobGroupName,
jobTypeName,
jobTypeSymbol,
]
.filter((item) => typeof item !== 'undefined')
.join(' ');
if (!('duration' in job)) {
// If start time is 0, then duration should be from requesttime to now
// If we have starttime and no endtime, then duration should be starttime to now
// If we have both starttime and endtime, then duration will be between those two
const endtime = endTimestamp || Date.now() / 1000;
const starttime = startTimestamp || submitTimestamp;
const diff = Math.max(endtime - starttime, 60);
job.duration = Math.round(diff / 60, 0);
}
job.hoverText = `${jobTypeName} - ${job.resultStatus} - ${job.duration} min${
job.duration > 1 ? 's' : ''
}`;
return job;
};
export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
const params = getAllUrlParams();
params.set('searchStr', jobSearchStr.split(' '));
return `?${params.toString()}`;
};
export const getTaskRunStr = (job) => `${job.task_id}.${job.retry_id}`;
// This matches as taskId, optionally followed by `.` or`-` and a runId.
// We support `-` for backwards compatability with the original format used.
const taskRunPattern = /^([A-Za-z0-9_-]{8}[Q-T][A-Za-z0-9_-][CGKOSWaeimquy26-][A-Za-z0-9_-]{10}[AQgw])(?:[-.]([0-9]+))?$/;
export const getTaskRun = function getTaskRun(taskRunStr) {
const match = taskRunPattern.exec(taskRunStr);
if (!match) {
return {};
}
return { taskId: match[1], runId: match[2] };
};