Skip to content
Draft
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
5 changes: 0 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,19 +410,15 @@ const RESTRICTED_IMPORTS = [
const AXIOS_LEGACY_CONSUMERS = [
'.buildkite/**/*.{js,mjs,ts,tsx,jsx}',
'packages/kbn-ci-stats-performance-metrics/**/*.{js,mjs,ts,tsx}',
'packages/kbn-failed-test-reporter-cli/**/*.{js,mjs,ts,tsx}',
'packages/kbn-generate/**/*.{js,mjs,ts,tsx}',
'src/dev/build/lib/**/*.{js,mjs,ts,tsx}',
'src/dev/build/tasks/**/*.{js,mjs,ts,tsx}',
'src/dev/prs/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/private/kbn-ci-stats-reporter/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/private/kbn-journeys/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/shared/kbn-connector-specs/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/shared/kbn-cypress-test-helper/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/shared/kbn-dev-utils/src/axios/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/shared/kbn-kbn-client/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/shared/kbn-mcp-dev-server/**/*.{js,mjs,ts,tsx}',
'src/platform/packages/shared/kbn-test-saml-auth/**/*.{js,mjs,ts,tsx}',
'src/platform/test/api_integration/apis/telemetry/**/*.{js,mjs,ts,tsx}',
'x-pack/examples/alerting_example/server/rule_types/**/*.{js,mjs,ts,tsx}',
'x-pack/packages/kbn-synthetics-private-location/**/*.{js,mjs,ts,tsx}',
Expand All @@ -447,7 +443,6 @@ const AXIOS_LEGACY_CONSUMERS = [
'x-pack/platform/test/alerting_api_integration/common/plugins/alerts/server/sub_action_connector.ts',
'x-pack/platform/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/mustache_templates.ts',
'x-pack/platform/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts',
'x-pack/platform/test/api_integration/services/**/*.{js,mjs,ts,tsx}',
'x-pack/platform/test/fleet_api_integration/**/*.{js,mjs,ts,tsx}',
'x-pack/platform/test/fleet_cypress/agent.ts',
'x-pack/platform/test/fleet_cypress/artifact_manager.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ const log = new ToolingLog();
const writer = new ToolingLogCollectingWriter();
log.setWriters([writer]);

const fetchMock = jest.spyOn(global, 'fetch');

afterEach(() => {
writer.messages.length = 0;
jest.clearAllMocks();
});

jest.mock('axios', () => ({
request: jest.fn(),
}));
const Axios = jest.requireMock('axios');
const jsonResponse = (data: unknown) => new Response(JSON.stringify(data), { status: 200 });

const mockTestFailure: Omit<TestFailure, 'classname' | 'name'> = {
failure: '',
Expand All @@ -41,12 +40,15 @@ const mockTestFailure: Omit<TestFailure, 'classname' | 'name'> = {
it('captures a list of failed test issue, loads the bodies for each issue, and only fetches what is needed', async () => {
const existing = new ExistingFailedTestIssues(log);

Axios.request.mockImplementation(({ data }: any) => ({
data: {
existingIssues: data.failures
.filter((t: any) => t.classname.includes('foo'))
fetchMock.mockImplementation(async (_input, init) => {
const body = JSON.parse(init?.body as string) as {
failures: Array<{ classname: string; name: string }>;
};
return jsonResponse({
existingIssues: body.failures
.filter((t) => t.classname.includes('foo'))
.map(
(t: any, i: any): FailedTestIssue => ({
(t, i): FailedTestIssue => ({
classname: t.classname,
name: t.name,
github: {
Expand All @@ -57,8 +59,8 @@ it('captures a list of failed test issue, loads the bodies for each issue, and o
},
})
),
},
}));
});
});

const fooFailure: TestFailure = {
...mockTestFailure,
Expand Down Expand Up @@ -98,72 +100,42 @@ it('captures a list of failed test issue, loads the bodies for each issue, and o
" debg loaded 1 existing test issues",
]
`);
expect(Axios.request).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Object {
"allowAbsoluteUrls": false,
"baseURL": "https://ci-stats.kibana.dev",
"data": Object {
"failures": Array [
Object {
"classname": "foo classname",
"name": "foo test",
},
],
},
"method": "POST",
"url": "/v1/find_failed_test_issues",
},
],
Array [
Object {
"allowAbsoluteUrls": false,
"baseURL": "https://ci-stats.kibana.dev",
"data": Object {
"failures": Array [
Object {
"classname": "bar classname",
"name": "bar test",
},
],
},
"method": "POST",
"url": "/v1/find_failed_test_issues",
},
],
],
"results": Array [
Object {
"type": "return",
"value": Object {
"data": Object {
"existingIssues": Array [
Object {
"classname": "foo classname",
"github": Object {
"body": "FAILURE: foo classname/foo test",
"htmlUrl": "htmlurl(foo classname/foo test)",
"nodeId": "nodeid(foo classname/foo test)",
"number": 21,
},
"name": "foo test",
},
],

expect(fetchMock).toHaveBeenCalledTimes(2);
// Argument shape: each call should be (url, init) with method POST and a JSON body
// describing the failure to look up. Snapshot the call args after parsing the body.
const calls = fetchMock.mock.calls.map(([url, init]) => ({
url: typeof url === 'string' ? url : url.toString(),
method: init?.method,
body: JSON.parse(init?.body as string),
}));
expect(calls).toMatchInlineSnapshot(`
Array [
Object {
"body": Object {
"failures": Array [
Object {
"classname": "foo classname",
"name": "foo test",
},
},
],
},
Object {
"type": "return",
"value": Object {
"data": Object {
"existingIssues": Array [],
"method": "POST",
"url": "https://ci-stats.kibana.dev/v1/find_failed_test_issues",
},
Object {
"body": Object {
"failures": Array [
Object {
"classname": "bar classname",
"name": "bar test",
},
},
],
},
],
}
"method": "POST",
"url": "https://ci-stats.kibana.dev/v1/find_failed_test_issues",
},
]
`);
});

Expand Down Expand Up @@ -193,12 +165,15 @@ describe('Scout failures', () => {
it('matches Scout failures by name only, ignoring target differences', async () => {
const existing = new ExistingFailedTestIssues(log);

Axios.request.mockImplementation(({ data }: any) => ({
data: {
existingIssues: data.failures
.filter((t: any) => t.name === 'scout test name')
fetchMock.mockImplementation(async (_input, init) => {
const body = JSON.parse(init?.body as string) as {
failures: Array<{ classname: string; name: string }>;
};
return jsonResponse({
existingIssues: body.failures
.filter((t) => t.name === 'scout test name')
.map(
(t: any): FailedTestIssue => ({
(t): FailedTestIssue => ({
classname: t.classname || 'scout suite',
name: t.name,
github: {
Expand All @@ -209,8 +184,8 @@ describe('Scout failures', () => {
},
})
),
},
}));
});
});

// First Scout failure with target chrome
const scoutFailure1: TestFailure & { id: string; target: string; location: string } = {
Expand Down Expand Up @@ -249,11 +224,7 @@ describe('Scout failures', () => {
it('correctly identifies seen Scout failures by name only', async () => {
const existing = new ExistingFailedTestIssues(log);

Axios.request.mockImplementation(() => ({
data: {
existingIssues: [],
},
}));
fetchMock.mockImplementation(async () => jsonResponse({ existingIssues: [] }));

// First Scout failure with target chrome
const scoutFailure1: TestFailure & { id: string; target: string; location: string } = {
Expand Down Expand Up @@ -283,16 +254,19 @@ describe('Scout failures', () => {
await existing.loadForFailures([scoutFailure1, scoutFailure2]);

// Should only make one API call (for the first failure, second is already seen)
expect(Axios.request).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
});

it('distinguishes between Scout and FTR failures correctly', async () => {
const existing = new ExistingFailedTestIssues(log);

Axios.request.mockImplementation(({ data }: any) => ({
data: {
existingIssues: data.failures.map(
(t: any, i: number): FailedTestIssue => ({
fetchMock.mockImplementation(async (_input, init) => {
const body = JSON.parse(init?.body as string) as {
failures: Array<{ classname: string; name: string }>;
};
return jsonResponse({
existingIssues: body.failures.map(
(t, i): FailedTestIssue => ({
classname: t.classname,
name: t.name,
github: {
Expand All @@ -303,8 +277,8 @@ describe('Scout failures', () => {
},
})
),
},
}));
});
});

const scoutFailure: TestFailure & { id: string; target: string; location: string } = {
...mockTestFailure,
Expand Down Expand Up @@ -337,11 +311,7 @@ describe('Scout failures', () => {
it('returns undefined for Scout failures when no matching issue exists', async () => {
const existing = new ExistingFailedTestIssues(log);

Axios.request.mockImplementation(() => ({
data: {
existingIssues: [],
},
}));
fetchMock.mockImplementation(async () => jsonResponse({ existingIssues: [] }));

const scoutFailure: TestFailure & { id: string; target: string; location: string } = {
...mockTestFailure,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@

import { setTimeout } from 'timers/promises';

import { isAxiosRequestError, isAxiosResponseError } from '@kbn/dev-utils';
import type { ToolingLog } from '@kbn/tooling-log';
import Axios from 'axios';

import type { TestFailure } from './get_failures';
import type { GithubIssueMini } from './github_api';
Expand Down Expand Up @@ -141,35 +139,42 @@ export class ExistingFailedTestIssues {
while (true) {
attempt += 1;

let response: Response;
try {
const resp = await Axios.request<FindFailedTestIssuesResponse>({
response = await fetch(`${BASE_URL}/v1/find_failed_test_issues`, {
method: 'POST',
baseURL: BASE_URL,
allowAbsoluteUrls: false,
url: '/v1/find_failed_test_issues',
data: {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
failures: failures.map((f) => ({
classname: f.classname,
name: f.name,
})),
},
}),
});

return resp.data.existingIssues;
} catch (error: unknown) {
if (
attempt < maxAttempts &&
((isAxiosResponseError(error) && error.response.status >= 500) ||
isAxiosRequestError(error))
) {
this.log.error(error);
// Network-level error (fetch throws TypeError for DNS/connection failures);
// matches axios's isAxiosRequestError retry condition.
if (attempt < maxAttempts) {
this.log.error(error as Error);
this.log.warning(`Failure talking to ci-stats, waiting ${attempt} before retrying`);
await setTimeout(attempt * 1000);
continue;
}

throw error;
}

if (!response.ok) {
// Server error: retry on 5xx, throw immediately on 4xx.
if (attempt < maxAttempts && response.status >= 500) {
this.log.warning(`Failure talking to ci-stats, waiting ${attempt} before retrying`);
await setTimeout(attempt * 1000);
continue;
}
throw new Error(`${response.status}:${await response.text()}`);
}

const body = (await response.json()) as FindFailedTestIssuesResponse;
return body.existingIssues;
}
}

Expand Down
Loading
Loading