Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 123 additions & 2 deletions src/__tests__/Search/fetchRevisionFromLando.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fetchMock from '@fetch-mock/jest';

import App, { router } from '../../components/App';
import { Strings } from '../../resources/Strings';
import { render } from '../utils/test-utils';

describe('Lando to commit validating', () => {
Expand Down Expand Up @@ -74,7 +75,7 @@ describe('Lando to commit validating', () => {
(console.error as jest.Mock).mockClear();
});

it('should reject', async () => {
it('should explain when Lando is still creating the try push', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
fetchMock.get(
'glob:https://api.lando.services.mozilla.com/*',
Expand All @@ -83,6 +84,118 @@ describe('Lando to commit validating', () => {
? {
commit_id: null,
id: 108,
status: 'SUBMITTED',
}
: {
commit_id: '6331cb86f104e2587160208d8e47d8bef8b38ffc',
id: 96,
status: 'LANDED',
};
},
);
await router.navigate(
'/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
);
render(<App />);
expect(console.error).toHaveBeenCalledWith(
new Error(Strings.errors.lando.pending('123', 'SUBMITTED')),
);
expect(console.error).toHaveBeenCalledTimes(1);
(console.error as jest.Mock).mockClear();
});

it('should explain when Lando failed to create the try push', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
fetchMock.get(
'glob:https://api.lando.services.mozilla.com/*',
({ url }) => {
return url.includes('123')
? {
commit_id: '096aa2c25fb2f031021de8c58baf9c46c052ab2e',
id: 108,
status: 'LANDED',
}
: {
commit_id: null,
error: 'Tree is closed',
id: 96,
status: 'FAILED',
};
},
);
await router.navigate(
'/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
);
render(<App />);
expect(console.error).toHaveBeenCalledWith(
new Error(Strings.errors.lando.failed('456', 'FAILED', 'Tree is closed')),
);
expect(console.error).toHaveBeenCalledTimes(1);
(console.error as jest.Mock).mockClear();
});

it('should explain a failed Lando try push that returned an empty commit_id', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
fetchMock.get('glob:https://lando.moz.tools/*', ({ url }) => {
return url.includes('91073')
? {
commit_id: '',
error:
'Unexpected error while pushing to try.\nhg error in cmd: hg push -r tip ssh://hg.mozilla.org/try -f: pushing to ssh://hg.mozilla.org/try\n\nremote: Connection closed by 63.245.208.203 port 22\nabort: no suitable response from remote hg',
id: 91073,
status: 'FAILED',
}
: {
commit_id: '8920f830aab97b1912099621e73bd4cd1ee5fa23',
error: '',
id: 91086,
status: 'LANDED',
};
});
await router.navigate(
'/compare-lando-results?landoInstance=lando-prod-2025&baseLando=91073&newLando=91086&baseRepo=try&newRepo=try&framework=13',
);
render(<App />);
expect(console.error).toHaveBeenCalledWith(
new Error(
Strings.errors.lando.failed(
'91073',
'FAILED',
'Unexpected error while pushing to try.',
),
),
);
expect(console.error).toHaveBeenCalledTimes(1);
(console.error as jest.Mock).mockClear();
});

it('should explain when Lando landed but still has no revision', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
fetchMock.get('glob:https://api.lando.services.mozilla.com/*', {
commit_id: null,
id: 108,
status: 'LANDED',
});
await router.navigate(
'/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
);
render(<App />);
expect(console.error).toHaveBeenCalledWith(
new Error(Strings.errors.lando.landedWithoutRevision('123', 'LANDED')),
);
expect(console.error).toHaveBeenCalledTimes(1);
(console.error as jest.Mock).mockClear();
});

it('should explain when Treeherder has not ingested the try push yet', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
fetchMock.get(
'glob:https://api.lando.services.mozilla.com/*',
({ url }) => {
return url.includes('123')
? {
commit_id: '096aa2c25fb2f031021de8c58baf9c46c052ab2e',
id: 108,
status: 'LANDED',
}
: {
Expand All @@ -92,12 +205,20 @@ describe('Lando to commit validating', () => {
};
},
);
fetchMock.get('glob:https://treeherder.mozilla.org/api/project/*/push/*', {
results: [],
});
await router.navigate(
'/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
);
render(<App />);
expect(console.error).toHaveBeenCalledWith(
new Error('The parameter baseRev is missing.'),
new Error(
Strings.errors.lando.notInTreeherder(
'123',
'096aa2c25fb2f031021de8c58baf9c46c052ab2e',
),
),
);
expect(console.error).toHaveBeenCalledTimes(1);
(console.error as jest.Mock).mockClear();
Expand Down
30 changes: 30 additions & 0 deletions src/components/CompareResults/landoToCommitLoader.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import { checkValues, getComparisonInformation } from './loader';
import { compareView } from '../../common/constants';
import { fetchRevisionFromLandoId, LandoInstance } from '../../logic/lando';
import { fetchRecentRevisions } from '../../logic/treeherder';
import { Strings } from '../../resources/Strings';
import {
Changeset,
CombinedResultsItemType,
Repository,
} from '../../types/state';
import { Framework, TestVersion } from '../../types/types';

async function ensureTryPushExists({
landoId,
commitId,
repo,
}: {
landoId: string;
commitId: string;
repo: Repository['name'];
}) {
const pushes = await fetchRecentRevisions({
repository: repo,
hash: commitId,
});
if (!pushes.length) {
throw new Error(Strings.errors.lando.notInTreeherder(landoId, commitId));
}
}

// This function is responsible for fetching the data from the URL. It's called
// by React Router DOM when the compare-lando-results route is requested.
// This loader is used by ./mach try perf, and due to recent changes in
Expand Down Expand Up @@ -63,6 +83,16 @@ export async function loader({ request }: { request: Request }) {
replicates: replicatesFromUrl,
testVersion: testVersionFromUrl,
});
await ensureTryPushExists({
landoId: baseLandoIDFromUrl,
commitId: baseRev,
repo: baseRepo,
});
await ensureTryPushExists({
landoId: newLandoIDFromUrl,
commitId: newRevs[0],
repo: newRepos[0],
});
return await getComparisonInformation(
baseRev,
baseRepo,
Expand Down
63 changes: 59 additions & 4 deletions src/logic/lando.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Strings } from '../resources/Strings';
import { LandoToCommit } from '../types/state';

const landoInstances = {
Expand All @@ -9,6 +10,54 @@ const landoInstances = {

export type LandoInstance = keyof typeof landoInstances;

export type LandoRevision = LandoToCommit & { commit_id: string };

const PENDING_STATUSES = new Set([
'submitted',
'in_progress',
'deferred',
'created',
'unknown',
]);

const FAILED_STATUSES = new Set(['failed', 'aborted', 'cancelled', 'canceled']);

function hasCommitId(job: LandoToCommit): job is LandoRevision {
// New Lando returns "" rather than null when a job has not produced a revision.
return typeof job.commit_id === 'string' && job.commit_id.trim().length > 0;
}

function firstLine(text: string | undefined): string | undefined {
if (!text) {
return undefined;
}
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed) {
return trimmed;
}
}
return undefined;
}

export function messageForMissingLandoRevision(
landoId: string,
job: LandoToCommit,
) {
const status = job.status?.trim() ? job.status : 'unknown';
const normalizedStatus = status.toLowerCase();

if (FAILED_STATUSES.has(normalizedStatus)) {
return Strings.errors.lando.failed(landoId, status, firstLine(job.error));
}

if (PENDING_STATUSES.has(normalizedStatus)) {
return Strings.errors.lando.pending(landoId, status);
}

return Strings.errors.lando.landedWithoutRevision(landoId, status);
}

async function fetchFromLando(url: string) {
const response = await fetch(url);
if (!response.ok) {
Expand All @@ -20,11 +69,17 @@ async function fetchFromLando(url: string) {
}

export async function fetchRevisionFromLandoId(
landoid: string,
landoId: string,
instance: LandoInstance = 'lando-prod',
) {
): Promise<LandoRevision> {
const host = landoInstances[instance] ?? landoInstances['lando-prod'];
const url = `https://${host}/landing_jobs/${landoid}`;
const url = `https://${host}/landing_jobs/${landoId}`;
const response = await fetchFromLando(url);
return response.json() as Promise<LandoToCommit>;
const job = (await response.json()) as LandoToCommit;

if (!hasCommitId(job)) {
throw new Error(messageForMissingLandoRevision(landoId, job));
}

return job;
}
12 changes: 12 additions & 0 deletions src/resources/Strings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,17 @@ export const Strings = {
},
errors: {
warningText: 'The search input must be at least three characters.',
lando: {
pending: (landoId: string, status: string) =>
`Lando has not finished creating the try push for job ${landoId} yet (status: ${status}). Please wait a few moments for the push to complete and then refresh the page.`,
failed: (landoId: string, status: string, detail?: string) =>
detail
? `Lando could not create the try push for job ${landoId} (status: ${status}): ${detail}`
: `Lando could not create the try push for job ${landoId} (status: ${status}).`,
landedWithoutRevision: (landoId: string, status: string) =>
`Lando reports job ${landoId} as ${status}, but no Treeherder revision is available yet. Please wait a few moments and then refresh the page.`,
notInTreeherder: (landoId: string, revision: string) =>
`Lando job ${landoId} has revision ${revision}, but Treeherder does not have this try push yet. If the push exists, it will appear in a few minutes once Treeherder has processed it. If this Lando ID is from lando.moz.tools, add landoInstance=lando-prod-2025 to the URL.`,
},
},
};
5 changes: 3 additions & 2 deletions src/types/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,10 @@ export type HashToCommit = {
};

export type LandoToCommit = {
commit_id: string;
id: string;
commit_id: string | null;
id: string | number;
status: string;
error?: string;
};

export type InputType = 'base' | 'new';
Expand Down