mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[7.3] refactor failed_tests_reporter to use TS, no octokit (#4… (#47634)
* refactor failed_tests_reporter to use TS, no octokit (#46993)
* refactor failed_tests_reporter to use TS, no octokit
* update renovate config
* ensure that all kbn-test files are in ts project
* fix some type errors
* add some more tests
* [kbn-test/githubapi] cleanup and document
* collect log messages as strings instead of message objects
* ensure issue is open when updating body
* improve readability of getKibanaIssues
* expose axios helpers from dev-utils
* fix request params for fetching github issues and validate locally
* include a README for failed_tests_reporter
* improve axios error helpers
# Conflicts:
# package.json
# packages/kbn-dev-utils/src/axios/errors.ts
# packages/kbn-dev-utils/src/index.ts
# packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts
# packages/kbn-test/src/index.ts
# packages/kbn-test/src/mocha/junit_report_generation.js
# renovate.json5
# src/dev/jest/junit_reporter.js
# yarn.lock
* ensure all of kbn-test is in ts project
(cherry picked from commit 41134fe134
)
* backport dev-utils constants
This commit is contained in:
parent
b9e0dd7f54
commit
066722f5ea
41 changed files with 1146 additions and 554 deletions
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
|
@ -294,6 +294,6 @@ def buildXpack() {
|
|||
def runErrorReporter() {
|
||||
bash """
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
node src/dev/failed_tests/cli
|
||||
node scripts/report_failed_tests
|
||||
"""
|
||||
}
|
||||
|
|
|
@ -274,7 +274,6 @@
|
|||
"@kbn/test": "1.0.0",
|
||||
"@microsoft/api-documenter": "7.2.1",
|
||||
"@microsoft/api-extractor": "7.1.8",
|
||||
"@octokit/rest": "^15.10.0",
|
||||
"@percy/agent": "^0.7.2",
|
||||
"@types/angular": "1.6.50",
|
||||
"@types/angular-mocks": "^1.7.0",
|
||||
|
|
|
@ -17,27 +17,20 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Octokit from '@octokit/rest';
|
||||
import { markdownMetadata } from './metadata';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
|
||||
export { markdownMetadata };
|
||||
|
||||
export function getGithubClient() {
|
||||
const client = new Octokit();
|
||||
client.authenticate({
|
||||
type: 'token',
|
||||
token: process.env.GITHUB_TOKEN
|
||||
});
|
||||
|
||||
return client;
|
||||
export interface AxiosRequestError extends AxiosError {
|
||||
response: undefined;
|
||||
}
|
||||
|
||||
export async function paginate(client, promise) {
|
||||
let response = await promise;
|
||||
let { data } = response;
|
||||
while (client.hasNextPage(response)) {
|
||||
response = await client.getNextPage(response);
|
||||
data = data.concat(response.data);
|
||||
}
|
||||
return data;
|
||||
export interface AxiosResponseError<T> extends AxiosError {
|
||||
response: AxiosResponse<T>;
|
||||
}
|
||||
|
||||
export const isAxiosRequestError = (error: any): error is AxiosRequestError => {
|
||||
return error && error.config && error.response === undefined;
|
||||
};
|
||||
|
||||
export const isAxiosResponseError = <T = any>(error: any): error is AxiosResponseError<T> => {
|
||||
return error && error.response && error.response.status !== undefined;
|
||||
};
|
20
packages/kbn-dev-utils/src/axios/index.ts
Normal file
20
packages/kbn-dev-utils/src/axios/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './errors';
|
22
packages/kbn-dev-utils/src/constants.ts
Normal file
22
packages/kbn-dev-utils/src/constants.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { dirname } from 'path';
|
||||
|
||||
export const REPO_ROOT = dirname(require.resolve('../../../package.json'));
|
|
@ -18,6 +18,13 @@
|
|||
*/
|
||||
|
||||
export { withProcRunner } from './proc_runner';
|
||||
export { ToolingLog, ToolingLogTextWriter, pickLevelFromFlags } from './tooling_log';
|
||||
export {
|
||||
ToolingLog,
|
||||
ToolingLogTextWriter,
|
||||
pickLevelFromFlags,
|
||||
ToolingLogCollectingWriter,
|
||||
} from './tooling_log';
|
||||
export { createAbsolutePathSerializer } from './serializers';
|
||||
export { run, createFailError, createFlagError, combineErrors, isFailError } from './run';
|
||||
export { REPO_ROOT } from './constants';
|
||||
export * from './axios';
|
||||
|
|
|
@ -20,3 +20,4 @@
|
|||
export { ToolingLog } from './tooling_log';
|
||||
export { ToolingLogTextWriter, ToolingLogTextWriterConfig } from './tooling_log_text_writer';
|
||||
export { pickLevelFromFlags, LogLevel } from './log_levels';
|
||||
export { ToolingLogCollectingWriter } from './tooling_log_collecting_writer';
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ToolingLogTextWriter } from './tooling_log_text_writer';
|
||||
|
||||
export class ToolingLogCollectingWriter extends ToolingLogTextWriter {
|
||||
messages: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
level: 'verbose',
|
||||
writeTo: {
|
||||
write: msg => {
|
||||
// trim trailing new line
|
||||
this.messages.push(msg.slice(0, -1));
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,5 +6,5 @@
|
|||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,7 +12,10 @@
|
|||
"devDependencies": {
|
||||
"@babel/cli": "7.4.4",
|
||||
"@kbn/babel-preset": "1.0.0",
|
||||
"@kbn/dev-utils": "1.0.0"
|
||||
"@kbn/dev-utils": "1.0.0",
|
||||
"@types/parse-link-header": "^1.0.0",
|
||||
"@types/strip-ansi": "^5.2.1",
|
||||
"@types/xml2js": "^0.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
|
@ -20,9 +23,12 @@
|
|||
"del": "^4.0.0",
|
||||
"getopts": "^2.2.4",
|
||||
"glob": "^7.1.2",
|
||||
"parse-link-header": "^1.0.1",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"rxjs": "^6.2.1",
|
||||
"tar-fs": "^1.16.2",
|
||||
"tmp": "^0.1.0",
|
||||
"xml2js": "^0.4.22",
|
||||
"zlib": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
|
21
packages/kbn-test/src/failed_tests_reporter/README.md
Normal file
21
packages/kbn-test/src/failed_tests_reporter/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# failed tests reporter
|
||||
|
||||
A little CLI that runs in CI to find the failed tests in the JUnit reports, then create/update github issues for each failure.
|
||||
|
||||
## Test this script locally
|
||||
|
||||
To fetch some JUnit reports from a recent build on CI, visit its `Google Cloud Storage Upload Report` and execute the following in the JS Console:
|
||||
|
||||
```js
|
||||
copy(`wget "${Array.from($$('a[href$=".xml"]')).filter(a => a.innerText === 'Download').map(a => a.href.replace('https://storage.cloud.google.com/', 'https://storage.googleapis.com/')).join('" "')}"`)
|
||||
```
|
||||
|
||||
This copies a script to download the reporets, which can be executed in the `test/junit` directory.
|
||||
|
||||
Next, run the CLI in `--dry-run` mode so that it doesn't actually communicate with Github.
|
||||
|
||||
```sh
|
||||
node scripts/report_failed_tests.js --verbose --dry-run --build-url foo
|
||||
```
|
||||
|
||||
If you specify the `GITHUB_TOKEN` environment variable then `--dry-run` will execute read operations but still won't execute write operations.
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
|
||||
import { getFailures } from './get_failures';
|
||||
|
||||
const log = new ToolingLog();
|
||||
|
||||
it('discovers failures in ftr report', async () => {
|
||||
const failures = await getFailures(log, require.resolve('./__fixtures__/ftr_report.xml'));
|
||||
expect(failures).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"classname": "Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js",
|
||||
"failure": "
|
||||
Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj~=\\"layerTocActionsPanelToggleButtonRoad_Map_-_Bright\\"])
|
||||
Wait timed out after 10055ms
|
||||
at /var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:834:17
|
||||
at process._tickCallback (internal/process/next_tick.js:68:7)
|
||||
at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9)
|
||||
at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13)
|
||||
",
|
||||
"name": "maps app maps loaded from sample data ecommerce \\"before all\\" hook",
|
||||
"time": "154.378",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('discovers failures in jest report', async () => {
|
||||
const failures = await getFailures(log, require.resolve('./__fixtures__/jest_report.xml'));
|
||||
expect(failures).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"classname": "X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp",
|
||||
"failure": "
|
||||
TypeError: Cannot read property '0' of undefined
|
||||
at Object.<anonymous>.test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10)
|
||||
",
|
||||
"name": "launcher can reconnect if process died",
|
||||
"time": "7.060",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('discovers failures in karma report', async () => {
|
||||
const failures = await getFailures(log, require.resolve('./__fixtures__/karma_report.xml'));
|
||||
expect(failures).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"classname": "Browser Unit Tests.CoordinateMapsVisualizationTest",
|
||||
"failure": "Error: expected 7069 to be below 64
|
||||
at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11)
|
||||
at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8)
|
||||
at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15)
|
||||
at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60)
|
||||
at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40)
|
||||
at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22)
|
||||
at Generator.prototype.<computed> [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21)
|
||||
at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103)
|
||||
at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194)
|
||||
",
|
||||
"name": "CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK",
|
||||
"time": "0.265",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('discovers failures in mocha report', async () => {
|
||||
const failures = await getFailures(log, require.resolve('./__fixtures__/mocha_report.xml'));
|
||||
expect(failures).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
163
packages/kbn-test/src/failed_tests_reporter/get_failures.ts
Normal file
163
packages/kbn-test/src/failed_tests_reporter/get_failures.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { promisify } from 'util';
|
||||
import Fs from 'fs';
|
||||
|
||||
import xml2js from 'xml2js';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
|
||||
type TestReport =
|
||||
| {
|
||||
testsuites: {
|
||||
testsuite: TestSuite[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
testsuite: TestSuite;
|
||||
};
|
||||
|
||||
interface TestSuite {
|
||||
$: {
|
||||
/* ISO8601 timetamp when test suite ran */
|
||||
timestamp: string;
|
||||
/* number of second this tests suite took */
|
||||
time: string;
|
||||
/* number of tests as a string */
|
||||
tests: string;
|
||||
/* number of failed tests as a string */
|
||||
failures: string;
|
||||
/* number of skipped tests as a string */
|
||||
skipped: string;
|
||||
};
|
||||
testcase: TestCase[];
|
||||
}
|
||||
|
||||
interface TestCase {
|
||||
$: {
|
||||
/* unique test name */
|
||||
name: string;
|
||||
/* somewhat human readable combination of test name and file */
|
||||
classname: string;
|
||||
/* number of seconds this test took */
|
||||
time: string;
|
||||
};
|
||||
/* contents of system-out elements */
|
||||
'system-out'?: string[];
|
||||
/* contents of failure elements */
|
||||
failure?: Array<string | { _: string }>;
|
||||
/* contents of skipped elements */
|
||||
skipped?: string[];
|
||||
}
|
||||
|
||||
export type TestFailure = TestCase['$'] & {
|
||||
failure: string;
|
||||
};
|
||||
|
||||
const readAsync = promisify(Fs.readFile);
|
||||
|
||||
const indent = (text: string) =>
|
||||
` ${text
|
||||
.split('\n')
|
||||
.map(l => ` ${l}`)
|
||||
.join('\n')}`;
|
||||
|
||||
const getFailureText = (failure: NonNullable<TestCase['failure']>) => {
|
||||
const [failureNode] = failure;
|
||||
|
||||
if (failureNode && typeof failureNode === 'object' && typeof failureNode._ === 'string') {
|
||||
return stripAnsi(failureNode._);
|
||||
}
|
||||
|
||||
return stripAnsi(String(failureNode));
|
||||
};
|
||||
|
||||
const isLikelyIrrelevant = ({ name, failure }: TestFailure) => {
|
||||
if (
|
||||
failure.includes('NoSuchSessionError: This driver instance does not have a valid session ID')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (failure.includes('Error: No Living connections')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
name.includes('"after all" hook') &&
|
||||
failure.includes(`Cannot read property 'shutdown' of undefined`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
failure.includes('Unable to read artifact info') &&
|
||||
failure.includes('Service Temporarily Unavailable')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (failure.includes('Unable to fetch Kibana status API response from Kibana')) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export async function getFailures(log: ToolingLog, testReportPath: string) {
|
||||
const xml = await readAsync(testReportPath, 'utf8');
|
||||
|
||||
// Parses junit XML files
|
||||
const report: TestReport = await xml2js.parseStringPromise(xml);
|
||||
|
||||
// Grab the failures. Reporters may report multiple testsuites in a single file.
|
||||
const testSuites = 'testsuites' in report ? report.testsuites.testsuite : [report.testsuite];
|
||||
|
||||
const failures: TestFailure[] = [];
|
||||
for (const testSuite of testSuites) {
|
||||
for (const testCase of testSuite.testcase) {
|
||||
const { failure } = testCase;
|
||||
|
||||
if (!failure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// unwrap xml weirdness
|
||||
const failureCase: TestFailure = {
|
||||
...testCase.$,
|
||||
// Strip ANSI color characters
|
||||
failure: getFailureText(failure),
|
||||
};
|
||||
|
||||
if (isLikelyIrrelevant(failureCase)) {
|
||||
log.warning(
|
||||
`Ignoring likely irrelevant failure: ${failureCase.classname} - ${
|
||||
failureCase.name
|
||||
}\n${indent(failureCase.failure)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
failures.push(failureCase);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`Found ${failures.length} test failures`);
|
||||
|
||||
return failures;
|
||||
}
|
169
packages/kbn-test/src/failed_tests_reporter/github_api.ts
Normal file
169
packages/kbn-test/src/failed_tests_reporter/github_api.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Url from 'url';
|
||||
|
||||
import Axios, { AxiosRequestConfig } from 'axios';
|
||||
import parseLinkHeader from 'parse-link-header';
|
||||
import { ToolingLog, isAxiosResponseError } from '@kbn/dev-utils';
|
||||
|
||||
const BASE_URL = 'https://api.github.com/repos/elastic/kibana/';
|
||||
|
||||
export interface GithubIssue {
|
||||
html_url: string;
|
||||
number: number;
|
||||
title: string;
|
||||
labels: unknown[];
|
||||
body: string;
|
||||
}
|
||||
|
||||
type RequestOptions = AxiosRequestConfig & { safeForDryRun?: boolean };
|
||||
|
||||
export class GithubApi {
|
||||
private readonly x = Axios.create({
|
||||
headers: {
|
||||
Authorization: `token ${this.token}`,
|
||||
'User-Agent': 'elastic/kibana#failed_test_reporter',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a GithubApi helper object, if token is undefined requests won't be
|
||||
* sent, but will instead be logged.
|
||||
*/
|
||||
constructor(
|
||||
private readonly log: ToolingLog,
|
||||
private readonly token: string | undefined,
|
||||
private readonly dryRun: boolean
|
||||
) {
|
||||
if (!token && !dryRun) {
|
||||
throw new TypeError('token parameter is required');
|
||||
}
|
||||
}
|
||||
|
||||
async getAllFailedTestIssues() {
|
||||
this.log.info('Fetching failed-test issues');
|
||||
const issues: GithubIssue[] = [];
|
||||
let nextRequest: RequestOptions = {
|
||||
safeForDryRun: true,
|
||||
method: 'GET',
|
||||
url: Url.resolve(BASE_URL, 'issues'),
|
||||
params: {
|
||||
state: 'all',
|
||||
per_page: '100',
|
||||
labels: 'failed-test',
|
||||
},
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const resp = await this.request<GithubIssue[]>(nextRequest, []);
|
||||
|
||||
for (const issue of resp.data) {
|
||||
issues.push(issue);
|
||||
}
|
||||
|
||||
const parsed = parseLinkHeader(resp.headers.link);
|
||||
if (parsed && parsed.next && parsed.next.url) {
|
||||
nextRequest = {
|
||||
safeForDryRun: true,
|
||||
method: 'GET',
|
||||
url: parsed.next.url,
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
async editIssueBodyAndEnsureOpen(issueNumber: number, newBody: string) {
|
||||
await this.request(
|
||||
{
|
||||
method: 'PATCH',
|
||||
url: Url.resolve(BASE_URL, `issues/${encodeURIComponent(String(issueNumber))}`),
|
||||
data: {
|
||||
state: 'open', // Reopen issue if it was closed.
|
||||
body: newBody,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
async addIssueComment(issueNumber: number, commentBody: string) {
|
||||
await this.request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: Url.resolve(BASE_URL, `issues/${encodeURIComponent(String(issueNumber))}/comments`),
|
||||
data: {
|
||||
body: commentBody,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
async createIssue(title: string, body: string, labels?: string[]) {
|
||||
const resp = await this.request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: Url.resolve(BASE_URL, 'issues'),
|
||||
data: {
|
||||
title,
|
||||
body,
|
||||
labels,
|
||||
},
|
||||
},
|
||||
{
|
||||
html_url: 'https://dryrun',
|
||||
}
|
||||
);
|
||||
|
||||
return resp.data.html_url;
|
||||
}
|
||||
|
||||
private async request<T>(options: RequestOptions, dryRunResponse: T) {
|
||||
const executeRequest = !this.dryRun || options.safeForDryRun;
|
||||
this.log.verbose('Github API', executeRequest ? 'Request' : 'Dry Run', options);
|
||||
|
||||
if (executeRequest) {
|
||||
try {
|
||||
return await this.x.request<T>(options);
|
||||
} catch (error) {
|
||||
if (isAxiosResponseError(error)) {
|
||||
throw new Error(
|
||||
`[${error.config.method} ${error.config.url}] ${error.response.status} ${
|
||||
error.response.statusText
|
||||
} Error: ${JSON.stringify(error.response.data)}`
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
data: dryRunResponse,
|
||||
};
|
||||
}
|
||||
}
|
20
packages/kbn-test/src/failed_tests_reporter/index.ts
Normal file
20
packages/kbn-test/src/failed_tests_reporter/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { runFailedTestsReporterCli } from './run_failed_tests_reporter_cli';
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import dedent from 'dedent';
|
||||
|
||||
import { getIssueMetadata, updateIssueMetadata } from './issue_metadata';
|
||||
|
||||
const HAS_METADATA = dedent`
|
||||
# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {"failed-test": {"foo": "bar"}} -->
|
||||
`;
|
||||
|
||||
const HAS_SOME_OTHER_METADATA = dedent`
|
||||
# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {"some-other": {"foo": "bar"}} -->
|
||||
`;
|
||||
|
||||
const INVALID_METADATA = dedent`
|
||||
# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {"failed-test" -->
|
||||
`;
|
||||
|
||||
const MISSING_METADATA = dedent`
|
||||
# my issue
|
||||
|
||||
some text
|
||||
`;
|
||||
|
||||
describe('getIssueMetadata', () => {
|
||||
it('reads properly formatted metadata', () => {
|
||||
expect(getIssueMetadata(HAS_METADATA, 'foo')).toBe('bar');
|
||||
});
|
||||
|
||||
it('returns undefined if JSON is malformed', () => {
|
||||
expect(getIssueMetadata(INVALID_METADATA, 'foo')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined if metadata is missing', () => {
|
||||
expect(getIssueMetadata(MISSING_METADATA, 'foo')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined if JSON is missing `failed-test` property', () => {
|
||||
expect(getIssueMetadata(HAS_SOME_OTHER_METADATA, 'foo')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns defaultValue if specified', () => {
|
||||
expect(getIssueMetadata(HAS_METADATA, 'foo2', 'bar2')).toBe('bar2');
|
||||
});
|
||||
|
||||
it('returns undefined if defaultValue is not specified', () => {
|
||||
expect(getIssueMetadata(HAS_METADATA, 'foo2')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIssueMetadata', () => {
|
||||
it('merges new values with previous values', () => {
|
||||
expect(
|
||||
updateIssueMetadata(HAS_METADATA, {
|
||||
box: 'baz',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {\\"failed-test\\":{\\"foo\\":\\"bar\\",\\"box\\":\\"baz\\"}} -->"
|
||||
`);
|
||||
});
|
||||
|
||||
it('adds metadata if not found', () => {
|
||||
expect(
|
||||
updateIssueMetadata(MISSING_METADATA, {
|
||||
box: 'baz',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {\\"failed-test\\":{\\"box\\":\\"baz\\"}} -->"
|
||||
`);
|
||||
|
||||
expect(
|
||||
updateIssueMetadata(HAS_SOME_OTHER_METADATA, {
|
||||
box: 'baz',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {\\"some-other\\":{\\"foo\\":\\"bar\\"},\\"failed-test\\":{\\"box\\":\\"baz\\"}} -->"
|
||||
`);
|
||||
});
|
||||
|
||||
it('overwrites metdata if JSON is malformed', () => {
|
||||
expect(
|
||||
updateIssueMetadata(INVALID_METADATA, {
|
||||
box: 'baz',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"# my issue
|
||||
|
||||
some text
|
||||
|
||||
<!-- kibanaCiData = {\\"failed-test\\":{\\"box\\":\\"baz\\"}} -->"
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -17,8 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const REGEX = /\n\n<!-- kibanaCiData = (.*) -->/;
|
||||
|
||||
/**
|
||||
* Allows retrieving and setting key/value pairs on a Github Issue. Keys and values must be JSON-serializable.
|
||||
* Derived from https://github.com/probot/metadata/blob/6ae1523d5035ba727d09c0e7f77a6a154d9a4777/index.js
|
||||
|
@ -47,42 +45,43 @@ const REGEX = /\n\n<!-- kibanaCiData = (.*) -->/;
|
|||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
export const markdownMetadata = {
|
||||
get(body, key = null, prefix = 'failed-test') {
|
||||
const match = body.match(REGEX);
|
||||
|
||||
if (match) {
|
||||
const data = JSON.parse(match[1])[prefix];
|
||||
return key ? data && data[key] : data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
const PREFIX = 'failed-test';
|
||||
const REGEX = /\n\n<!-- kibanaCiData = (.*) -->/;
|
||||
|
||||
/**
|
||||
* Set data on the body. Can either be set individually with `key` and `value` OR
|
||||
*/
|
||||
set(body, key, value, prefix = 'failed-test') {
|
||||
let newData = {};
|
||||
// If second arg is an object, use all supplied values.
|
||||
if (typeof key === 'object') {
|
||||
newData = key;
|
||||
prefix = value || prefix; // need to move third arg to prefix.
|
||||
} else {
|
||||
newData[key] = value;
|
||||
}
|
||||
|
||||
let data = {};
|
||||
|
||||
body = body.replace(REGEX, (_, json) => {
|
||||
data = JSON.parse(json);
|
||||
return '';
|
||||
});
|
||||
|
||||
if (!data[prefix]) data[prefix] = {};
|
||||
|
||||
Object.assign(data[prefix], newData);
|
||||
|
||||
return `${body}\n\n<!-- kibanaCiData = ${JSON.stringify(data)} -->`;
|
||||
function safeJsonParse(json: string, onError: any) {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return onError;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse metadata from issue body
|
||||
*/
|
||||
export function getIssueMetadata(body: string, key: string, defaultValue: any = undefined) {
|
||||
const match = body.match(REGEX);
|
||||
|
||||
if (match) {
|
||||
const data = safeJsonParse(match[1], {})[PREFIX];
|
||||
return data && data[key] !== undefined ? data[key] : defaultValue;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on the body.
|
||||
*/
|
||||
export function updateIssueMetadata(body: string, values: Record<string, any>) {
|
||||
if (REGEX.test(body)) {
|
||||
return body.replace(REGEX, (match, json) => {
|
||||
const data = safeJsonParse(json, {});
|
||||
data[PREFIX] = Object.assign(data[PREFIX] || {}, values);
|
||||
return match.replace(json, JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
return `${body}\n\n<!-- kibanaCiData = ${JSON.stringify({ [PREFIX]: values })} -->`;
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import dedent from 'dedent';
|
||||
import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/dev-utils';
|
||||
|
||||
import { createFailureIssue, updatedFailureIssue } from './report_failure';
|
||||
|
||||
jest.mock('./github_api');
|
||||
const { GithubApi } = jest.requireMock('./github_api');
|
||||
|
||||
describe('createFailureIssue()', () => {
|
||||
it('creates new github issue with failure text, link to issue, and valid metadata', async () => {
|
||||
const log = new ToolingLog();
|
||||
const writer = new ToolingLogCollectingWriter();
|
||||
log.setWriters([writer]);
|
||||
|
||||
const api = new GithubApi();
|
||||
|
||||
await createFailureIssue(
|
||||
'https://build-url',
|
||||
{
|
||||
classname: 'some.classname',
|
||||
failure: 'this is the failure text',
|
||||
name: 'test name',
|
||||
time: '2018-01-01T01:00:00Z',
|
||||
},
|
||||
log,
|
||||
api
|
||||
);
|
||||
|
||||
expect(api.createIssue).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"Failing test: some.classname - test name",
|
||||
"A test failed on a tracked branch
|
||||
\`\`\`
|
||||
this is the failure text
|
||||
\`\`\`
|
||||
First failure: [Jenkins Build](https://build-url)
|
||||
|
||||
<!-- kibanaCiData = {\\"failed-test\\":{\\"test.class\\":\\"some.classname\\",\\"test.name\\":\\"test name\\",\\"test.failCount\\":1}} -->",
|
||||
Array [
|
||||
"failed-test",
|
||||
],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(writer.messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
" [34minfo[39m Created issue undefined",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatedFailureIssue()', () => {
|
||||
it('increments failure count and adds new comment to issue', async () => {
|
||||
const log = new ToolingLog();
|
||||
const writer = new ToolingLogCollectingWriter();
|
||||
log.setWriters([writer]);
|
||||
|
||||
const api = new GithubApi();
|
||||
|
||||
await updatedFailureIssue(
|
||||
'https://build-url',
|
||||
{
|
||||
html_url: 'https://github.com/issues/1234',
|
||||
labels: ['some-label'],
|
||||
number: 1234,
|
||||
title: 'issue title',
|
||||
body: dedent`
|
||||
# existing issue body
|
||||
|
||||
<!-- kibanaCiData = {"failed-test":{"test.failCount":10}} -->"
|
||||
`,
|
||||
},
|
||||
log,
|
||||
api
|
||||
);
|
||||
|
||||
expect(api.editIssueBodyAndEnsureOpen).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
1234,
|
||||
"# existing issue body
|
||||
|
||||
<!-- kibanaCiData = {\\"failed-test\\":{\\"test.failCount\\":11}} -->\\"",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(api.addIssueComment).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
1234,
|
||||
"New failure: [Jenkins Build](https://build-url)",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(writer.messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
" [34minfo[39m Updated issue https://github.com/issues/1234, failCount: 11",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
import dedent from 'dedent';
|
||||
|
||||
import { TestFailure } from './get_failures';
|
||||
import { GithubIssue, GithubApi } from './github_api';
|
||||
import { getIssueMetadata, updateIssueMetadata } from './issue_metadata';
|
||||
|
||||
export async function createFailureIssue(
|
||||
buildUrl: string,
|
||||
failure: TestFailure,
|
||||
log: ToolingLog,
|
||||
api: GithubApi
|
||||
) {
|
||||
const title = `Failing test: ${failure.classname} - ${failure.name}`;
|
||||
|
||||
const body = updateIssueMetadata(
|
||||
dedent`
|
||||
A test failed on a tracked branch
|
||||
\`\`\`
|
||||
${failure.failure}
|
||||
\`\`\`
|
||||
First failure: [Jenkins Build](${buildUrl})
|
||||
`,
|
||||
{
|
||||
'test.class': failure.classname,
|
||||
'test.name': failure.name,
|
||||
'test.failCount': 1,
|
||||
}
|
||||
);
|
||||
|
||||
const newIssueUrl = await api.createIssue(title, body, ['failed-test']);
|
||||
log.info(`Created issue ${newIssueUrl}`);
|
||||
}
|
||||
|
||||
export async function updatedFailureIssue(
|
||||
buildUrl: string,
|
||||
issue: GithubIssue,
|
||||
log: ToolingLog,
|
||||
api: GithubApi
|
||||
) {
|
||||
// Increment failCount
|
||||
const newCount = getIssueMetadata(issue.body, 'test.failCount', 0) + 1;
|
||||
const newBody = updateIssueMetadata(issue.body, {
|
||||
'test.failCount': newCount,
|
||||
});
|
||||
|
||||
await api.editIssueBodyAndEnsureOpen(issue.number, newBody);
|
||||
await api.addIssueComment(issue.number, `New failure: [Jenkins Build](${buildUrl})`);
|
||||
|
||||
log.info(`Updated issue ${issue.html_url}, failCount: ${newCount}`);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { REPO_ROOT, run, createFailError, createFlagError } from '@kbn/dev-utils';
|
||||
import globby from 'globby';
|
||||
|
||||
import { getFailures } from './get_failures';
|
||||
import { GithubApi } from './github_api';
|
||||
import { updatedFailureIssue, createFailureIssue } from './report_failure';
|
||||
import { getIssueMetadata } from './issue_metadata';
|
||||
|
||||
export function runFailedTestsReporterCli() {
|
||||
run(
|
||||
async ({ log, flags }) => {
|
||||
const buildUrl = flags['build-url'];
|
||||
if (typeof buildUrl !== 'string' || !buildUrl) {
|
||||
throw createFlagError('Missing --build-url or process.env.BUILD_URL');
|
||||
}
|
||||
|
||||
const dryRun = !!flags['dry-run'];
|
||||
if (!dryRun) {
|
||||
// JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others
|
||||
const jobNameSplit = (process.env.JOB_NAME || '').split(/\+|\//);
|
||||
const branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH;
|
||||
if (!branch) {
|
||||
throw createFailError(
|
||||
'Unable to determine originating branch from job name or other environment variables'
|
||||
);
|
||||
}
|
||||
|
||||
const isPr = !!process.env.ghprbPullId;
|
||||
const isMasterOrVersion =
|
||||
branch.match(/^(origin\/){0,1}master$/) || branch.match(/^(origin\/){0,1}\d+\.(x|\d+)$/);
|
||||
if (!isMasterOrVersion || isPr) {
|
||||
throw createFailError('Failure issues only created on master/version branch jobs', {
|
||||
exitCode: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
throw createFailError(
|
||||
'GITHUB_TOKEN environment variable must be set, otherwise use --dry-run flag'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const githubApi = new GithubApi(log, process.env.GITHUB_TOKEN, dryRun);
|
||||
const issues = await githubApi.getAllFailedTestIssues();
|
||||
const reportPaths = await globby(['target/junit/**/*.xml'], {
|
||||
cwd: REPO_ROOT,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
for (const reportPath of reportPaths) {
|
||||
for (const failure of await getFailures(log, reportPath)) {
|
||||
const existingIssue = issues.find(
|
||||
i =>
|
||||
getIssueMetadata(i.body, 'test.class') === failure.classname &&
|
||||
getIssueMetadata(i.body, 'test.name') === failure.name
|
||||
);
|
||||
|
||||
if (existingIssue) {
|
||||
await updatedFailureIssue(buildUrl, existingIssue, log, githubApi);
|
||||
} else {
|
||||
await createFailureIssue(buildUrl, failure, log, githubApi);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
description: `a cli that opens issues or updates existing issues based on junit reports`,
|
||||
flags: {
|
||||
boolean: ['dry-run'],
|
||||
string: ['build-url'],
|
||||
default: {
|
||||
'build-url': process.env.BUILD_URL,
|
||||
},
|
||||
help: `
|
||||
--dry-run Execute the CLI without contacting Github
|
||||
--build-url URL of the failed build, defaults to process.env.BUILD_URL
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -28,3 +28,5 @@ export { esTestConfig, createEsTestCluster } from './es';
|
|||
export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn';
|
||||
|
||||
export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth';
|
||||
|
||||
export { runFailedTestsReporterCli } from './failed_tests_reporter';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"types/**/*"
|
||||
"types/**/*",
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
|
|
21
scripts/report_failed_tests.js
Normal file
21
scripts/report_failed_tests.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
require('../src/setup_node_env');
|
||||
require('@kbn/test').runFailedTestsReporterCli();
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
const { resolve } = require('path');
|
||||
|
||||
// force cwd
|
||||
process.chdir(resolve(__dirname, '../../..'));
|
||||
|
||||
// JOB_NAME is formatted as `elastic+kibana+7.x` in some places and `elastic+kibana+7.x/JOB=kibana-intake,node=immutable` in others
|
||||
const jobNameSplit = (process.env.JOB_NAME || '').split(/\+|\//);
|
||||
const branch = jobNameSplit.length >= 3 ? jobNameSplit[2] : process.env.GIT_BRANCH;
|
||||
if (!branch) {
|
||||
console.log('Unable to determine originating branch from job name or other environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const isPr = !!process.env.ghprbPullId;
|
||||
const isMasterOrVersion = branch.match(/^(origin\/){0,1}master$/) || branch.match(/^(origin\/){0,1}\d+\.(x|\d+)$/);
|
||||
if (!isMasterOrVersion || isPr) {
|
||||
console.log('Failure issues only created on master/version branch jobs');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
require('../../setup_node_env');
|
||||
require('./report').reportFailedTests();
|
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import xml2js from 'xml2js';
|
||||
import vfs from 'vinyl-fs';
|
||||
import { createMapStream } from '../../legacy/utils/streams';
|
||||
import { getGithubClient, markdownMetadata, paginate } from '../github_utils';
|
||||
import { find } from 'lodash';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
const GITHUB_FLAKY_TEST_LABEL = 'failed-test';
|
||||
const GITHUB_OWNER = 'elastic';
|
||||
const GITHUB_REPO = 'kibana';
|
||||
const BUILD_URL = process.env.BUILD_URL;
|
||||
|
||||
const indent = text => (
|
||||
` ${text.split('\n').map(l => ` ${l}`).join('\n')}`
|
||||
);
|
||||
|
||||
const getFailureText = (testCase) => {
|
||||
const [failureNode] = testCase.failure;
|
||||
|
||||
if (failureNode && typeof failureNode === 'object' && typeof failureNode._ === 'string') {
|
||||
return stripAnsi(failureNode._);
|
||||
}
|
||||
|
||||
return stripAnsi(String(failureNode));
|
||||
};
|
||||
|
||||
const isLikelyIrrelevant = ({ name, failure }) => {
|
||||
if (failure.includes('NoSuchSessionError: This driver instance does not have a valid session ID')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (failure.includes('Error: No Living connections')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name.includes('"after all" hook') && failure.includes(`Cannot read property 'shutdown' of undefined`)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (failure.includes('Unable to read artifact info') && failure.includes('Service Temporarily Unavailable')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (failure.includes('Unable to fetch Kibana status API response from Kibana')) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses junit XML files into JSON
|
||||
*/
|
||||
export const mapXml = () => createMapStream((file) => new Promise((resolve, reject) => {
|
||||
xml2js.parseString(file.contents.toString(), (err, result) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
}));
|
||||
|
||||
/**
|
||||
* Filters all testsuites to find failed testcases
|
||||
*/
|
||||
export const filterFailures = () => createMapStream((testSuite) => {
|
||||
// Grab the failures. Reporters may report multiple testsuites in a single file.
|
||||
const testFiles = testSuite.testsuites
|
||||
? testSuite.testsuites.testsuite
|
||||
: [testSuite.testsuite];
|
||||
|
||||
const failures = [];
|
||||
for (const testFile of testFiles) {
|
||||
for (const testCase of testFile.testcase) {
|
||||
if (!testCase.failure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// unwrap xml weirdness
|
||||
const failureCase = {
|
||||
...testCase.$,
|
||||
// Strip ANSI color characters
|
||||
failure: getFailureText(testCase)
|
||||
};
|
||||
|
||||
if (isLikelyIrrelevant(failureCase)) {
|
||||
console.log(`Ignoring likely irrelevant failure: ${failureCase.classname} - ${failureCase.name}\n${indent(failureCase.failure)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
failures.push(failureCase);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${failures.length} test failures`);
|
||||
|
||||
return failures;
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates and updates github issues for the given testcase failures.
|
||||
*/
|
||||
const updateGithubIssues = (githubClient, issues) => {
|
||||
return createMapStream(async (failureCases) => {
|
||||
|
||||
await Promise.all(failureCases.map(async (failureCase) => {
|
||||
const existingIssue = find(issues, (issue) => {
|
||||
return markdownMetadata.get(issue.body, 'test.class') === failureCase.classname &&
|
||||
markdownMetadata.get(issue.body, 'test.name') === failureCase.name;
|
||||
});
|
||||
|
||||
if (existingIssue) {
|
||||
// Increment failCount
|
||||
const newCount = (markdownMetadata.get(existingIssue.body, 'test.failCount') || 0) + 1;
|
||||
const newBody = markdownMetadata.set(existingIssue.body, 'test.failCount', newCount);
|
||||
|
||||
await githubClient.issues.edit({
|
||||
owner: GITHUB_OWNER,
|
||||
repo: GITHUB_REPO,
|
||||
number: existingIssue.number,
|
||||
state: 'open', // Reopen issue if it was closed.
|
||||
body: newBody
|
||||
});
|
||||
|
||||
// Append a new comment
|
||||
await githubClient.issues.createComment({
|
||||
owner: GITHUB_OWNER,
|
||||
repo: GITHUB_REPO,
|
||||
number: existingIssue.number,
|
||||
body: `New failure: [Jenkins Build](${BUILD_URL})`
|
||||
});
|
||||
|
||||
console.log(`Updated issue ${existingIssue.html_url}, failCount: ${newCount}`);
|
||||
} else {
|
||||
let body = 'A test failed on a tracked branch\n' +
|
||||
'```\n' + failureCase.failure + '\n```\n' +
|
||||
`First failure: [Jenkins Build](${BUILD_URL})`;
|
||||
body = markdownMetadata.set(body, {
|
||||
'test.class': failureCase.classname,
|
||||
'test.name': failureCase.name,
|
||||
'test.failCount': 1
|
||||
});
|
||||
|
||||
const newIssue = await githubClient.issues.create({
|
||||
owner: GITHUB_OWNER,
|
||||
repo: GITHUB_REPO,
|
||||
title: `Failing test: ${failureCase.classname} - ${failureCase.name}`,
|
||||
body: body,
|
||||
labels: [GITHUB_FLAKY_TEST_LABEL]
|
||||
});
|
||||
|
||||
console.log(`Created issue ${newIssue.data.html_url}`);
|
||||
}
|
||||
}));
|
||||
|
||||
return failureCases;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Scans all junit XML files in ./target/junit/ and reports any found test failures to Github Issues.
|
||||
*/
|
||||
export async function reportFailedTests() {
|
||||
const githubClient = getGithubClient();
|
||||
const issues = await paginate(githubClient, githubClient.issues.getForRepo({
|
||||
owner: GITHUB_OWNER,
|
||||
repo: GITHUB_REPO,
|
||||
labels: GITHUB_FLAKY_TEST_LABEL,
|
||||
state: 'all',
|
||||
per_page: 100
|
||||
}));
|
||||
|
||||
vfs
|
||||
.src(['./target/junit/**/*.xml'])
|
||||
.pipe(mapXml())
|
||||
.pipe(filterFailures())
|
||||
.pipe(updateGithubIssues(githubClient, issues))
|
||||
.on('done', () => console.log(`Finished reporting test failures.`));
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
import { resolve } from 'path';
|
||||
|
||||
import vfs from 'vinyl-fs';
|
||||
|
||||
import { mapXml, filterFailures } from './report';
|
||||
import { createPromiseFromStreams } from '../../legacy/utils/streams/promise_from_streams';
|
||||
import { createConcatStream } from '../../legacy/utils/streams/concat_stream';
|
||||
|
||||
console.log = jest.fn();
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('irrelevant failure filtering', () => {
|
||||
describe('jest report', () => {
|
||||
it('allows relevant tests', async () => {
|
||||
const failures = await createPromiseFromStreams([
|
||||
vfs.src([resolve(__dirname, '__fixtures__/jest_report.xml')]),
|
||||
mapXml(),
|
||||
filterFailures(),
|
||||
createConcatStream(),
|
||||
]);
|
||||
|
||||
expect(console.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Found 1 test failures",
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(failures).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"classname": "X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp",
|
||||
"failure": "
|
||||
TypeError: Cannot read property '0' of undefined
|
||||
at Object.<anonymous>.test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10)
|
||||
",
|
||||
"name": "launcher can reconnect if process died",
|
||||
"time": "7.060",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ftr report', () => {
|
||||
it('allows relevant tests', async () => {
|
||||
const failures = await createPromiseFromStreams([
|
||||
vfs.src([resolve(__dirname, '__fixtures__/ftr_report.xml')]),
|
||||
mapXml(),
|
||||
filterFailures(),
|
||||
createConcatStream(),
|
||||
]);
|
||||
|
||||
expect(console.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Ignoring likely irrelevant failure: Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps - maps app \\"after all\\" hook
|
||||
|
||||
{ NoSuchSessionError: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used.
|
||||
at promise.finally (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:726:38)
|
||||
at Object.thenFinally [as finally] (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/promise.js:124:12)
|
||||
at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' }
|
||||
",
|
||||
],
|
||||
Array [
|
||||
"Found 1 test failures",
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(failures).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"classname": "Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js",
|
||||
"failure": "
|
||||
Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj~=\\"layerTocActionsPanelToggleButtonRoad_Map_-_Bright\\"])
|
||||
Wait timed out after 10055ms
|
||||
at /var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/node_modules/selenium-webdriver/lib/webdriver.js:834:17
|
||||
at process._tickCallback (internal/process/next_tick.js:68:7)
|
||||
at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9)
|
||||
at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13)
|
||||
",
|
||||
"name": "maps app maps loaded from sample data ecommerce \\"before all\\" hook",
|
||||
"time": "154.378",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mocha report', () => {
|
||||
it('allows relevant tests', async () => {
|
||||
const failures = await createPromiseFromStreams([
|
||||
vfs.src([resolve(__dirname, '__fixtures__/mocha_report.xml')]),
|
||||
mapXml(),
|
||||
filterFailures(),
|
||||
createConcatStream(),
|
||||
]);
|
||||
|
||||
expect(console.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Ignoring likely irrelevant failure: X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts - code in multiple nodes \\"before all\\" hook
|
||||
|
||||
Error: Unable to read artifact info from https://artifacts-api.elastic.co/v1/versions/8.0.0-SNAPSHOT/builds/latest/projects/elasticsearch: Service Temporarily Unavailable
|
||||
<html>
|
||||
<head><title>503 Service Temporarily Unavailable</title></head>
|
||||
<body bgcolor=\\"white\\">
|
||||
<center><h1>503 Service Temporarily Unavailable</h1></center>
|
||||
<hr><center>nginx/1.13.7</center>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
at Function.getSnapshot (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/packages/kbn-es/src/artifact.js:95:13)
|
||||
at process._tickCallback (internal/process/next_tick.js:68:7)
|
||||
",
|
||||
],
|
||||
Array [
|
||||
"Ignoring likely irrelevant failure: X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts - code in multiple nodes \\"after all\\" hook
|
||||
|
||||
TypeError: Cannot read property 'shutdown' of undefined
|
||||
at Context.shutdown (plugins/code/server/__tests__/multi_node.ts:125:23)
|
||||
at process.topLevelDomainCallback (domain.js:120:23)
|
||||
",
|
||||
],
|
||||
Array [
|
||||
"Found 0 test failures",
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(failures).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('karma report', () => {
|
||||
it('allows relevant tests', async () => {
|
||||
const failures = await createPromiseFromStreams([
|
||||
vfs.src([resolve(__dirname, '__fixtures__/karma_report.xml')]),
|
||||
mapXml(),
|
||||
filterFailures(),
|
||||
createConcatStream(),
|
||||
]);
|
||||
|
||||
expect(console.log.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Found 1 test failures",
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(failures).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"classname": "Browser Unit Tests.CoordinateMapsVisualizationTest",
|
||||
"failure": "Error: expected 7069 to be below 64
|
||||
at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.assert (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13671:11)
|
||||
at Assertion.__kbnBundles__.tests../packages/kbn-expect/expect.js.Assertion.lessThan.Assertion.below (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:13891:8)
|
||||
at Function.lessThan (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:14078:15)
|
||||
at _callee3$ (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158985:60)
|
||||
at tryCatch (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:62:40)
|
||||
at Generator.invoke [as _invoke] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:288:22)
|
||||
at Generator.prototype.<computed> [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21)
|
||||
at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103)
|
||||
at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194)
|
||||
",
|
||||
"name": "CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should initialize OK",
|
||||
"time": "0.265",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -110,13 +110,7 @@ export default class JestJUnitReporter {
|
|||
`TEST-${process.env.JOB ? process.env.JOB + '-' : ''}${reportName}.xml`
|
||||
);
|
||||
|
||||
const reportXML = root.end({
|
||||
pretty: true,
|
||||
indent: ' ',
|
||||
newline: '\n',
|
||||
spacebeforeslash: '',
|
||||
});
|
||||
|
||||
const reportXML = root.end();
|
||||
mkdirp.sync(dirname(reportPath));
|
||||
writeFileSync(reportPath, reportXML, 'utf8');
|
||||
}
|
||||
|
|
|
@ -149,13 +149,7 @@ export function setupJUnitReportGeneration(runner, options = {}) {
|
|||
`TEST-${process.env.JOB ? process.env.JOB + '-' : ''}${reportName}.xml`
|
||||
);
|
||||
|
||||
const reportXML = builder.end({
|
||||
pretty: true,
|
||||
indent: ' ',
|
||||
newline: '\n',
|
||||
spacebeforeslash: ''
|
||||
});
|
||||
|
||||
const reportXML = builder.end();
|
||||
mkdirp.sync(dirname(reportPath));
|
||||
writeFileSync(reportPath, reportXML, 'utf8');
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
else
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
fi
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
else
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
fi
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
fi
|
||||
|
||||
export TEST_BROWSER_HEADLESS=1
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
else
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
fi
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
fi
|
||||
|
||||
export TEST_BROWSER_HEADLESS=1
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
else
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
fi
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
else
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
fi
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
set -e
|
||||
|
||||
if [[ -z "$IS_PIPELINE_JOB" ]] ; then
|
||||
trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT
|
||||
trap 'node "$KIBANA_DIR/scripts/report_failed_tests"' EXIT
|
||||
else
|
||||
source src/dev/ci_setup/setup_env.sh
|
||||
fi
|
||||
|
|
88
yarn.lock
88
yarn.lock
|
@ -1930,14 +1930,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz#63985d3d8b02530e0869962f4da09142ee8e200e"
|
||||
integrity sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA==
|
||||
|
||||
"@gimenete/type-writer@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@gimenete/type-writer/-/type-writer-0.1.3.tgz#2d4f26118b18d71f5b34ca24fdd6d1fd455c05b6"
|
||||
integrity sha512-vhpvVfM/fYqb1aAnkgOvtDKoOgU3ZYIvDnKSDAFSoBvallmGURMlHOE0/VG/gqunUZVXGCFBGHxI8swjBh+sIA==
|
||||
dependencies:
|
||||
camelcase "^5.0.0"
|
||||
prettier "^1.13.7"
|
||||
|
||||
"@graphql-modules/epoxy@0.1.9":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-modules/epoxy/-/epoxy-0.1.9.tgz#291879d517ba4473ea334edbb137a2cc6aed384c"
|
||||
|
@ -2726,21 +2718,6 @@
|
|||
once "^1.4.0"
|
||||
universal-user-agent "^2.0.1"
|
||||
|
||||
"@octokit/rest@^15.10.0":
|
||||
version "15.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-15.10.0.tgz#9baf7430e55edf1a1024c35ae72ed2f5fc6e90e9"
|
||||
integrity sha512-xZ4ejCZoqvKrIN3tQOKZlJ6nDQxaOdLcjRsamDnbckU7V5YTn2xheIqFXnQ2vLvxqVwyI8+2dfsODYbHxtwtSw==
|
||||
dependencies:
|
||||
"@gimenete/type-writer" "^0.1.3"
|
||||
before-after-hook "^1.1.0"
|
||||
btoa-lite "^1.0.0"
|
||||
debug "^3.1.0"
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^2.2.0"
|
||||
lodash "^4.17.4"
|
||||
node-fetch "^2.1.1"
|
||||
url-template "^2.0.8"
|
||||
|
||||
"@octokit/rest@^16.23.2":
|
||||
version "16.23.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.23.2.tgz#975e84610427c4ab6c41bec77c24aed9b7563db4"
|
||||
|
@ -4171,6 +4148,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/parse-link-header@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a"
|
||||
integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA==
|
||||
|
||||
"@types/pngjs@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.1.tgz#47d97bd29dd6372856050e9e5e366517dd1ba2d8"
|
||||
|
@ -4438,6 +4420,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-3.0.0.tgz#9b63d453a6b54aa849182207711a08be8eea48ae"
|
||||
integrity sha1-m2PUU6a1SqhJGCIHcRoIvo7qSK4=
|
||||
|
||||
"@types/strip-ansi@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-5.2.1.tgz#acd97f1f091e332bb7ce697c4609eb2370fa2a92"
|
||||
integrity sha512-1l5iM0LBkVU8JXxnIoBqNvg+yyOXxPeN6DNoD+7A9AN1B8FhYPSeIXgyNqwIqg1uzipTgVC2hmuDzB0u9qw/PA==
|
||||
dependencies:
|
||||
strip-ansi "*"
|
||||
|
||||
"@types/strong-log-transformer@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/strong-log-transformer/-/strong-log-transformer-1.0.0.tgz#47b0c9fe1f0c997ed4239746e633e8e36fc836ac"
|
||||
|
@ -4557,6 +4546,13 @@
|
|||
"@types/events" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/xml2js@^0.4.5":
|
||||
version "0.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.5.tgz#d21759b056f282d9c7066f15bbf5c19b908f22fa"
|
||||
integrity sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/yargs@^12.0.2":
|
||||
version "12.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
|
||||
|
@ -6757,11 +6753,6 @@ beeper@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
|
||||
integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=
|
||||
|
||||
before-after-hook@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.1.0.tgz#83165e15a59460d13702cb8febd6a1807896db5a"
|
||||
integrity sha512-VOMDtYPwLbIncTxNoSzRyvaMxtXmLWLUqr8k5AfC1BzLk34HvBXaQX8snOwQZ4c0aX8aSERqtJSiI9/m2u5kuA==
|
||||
|
||||
before-after-hook@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d"
|
||||
|
@ -15024,7 +15015,7 @@ https-browserify@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
||||
|
||||
https-proxy-agent@2.2.1, https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1:
|
||||
https-proxy-agent@2.2.1, https-proxy-agent@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
|
||||
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
|
||||
|
@ -19890,7 +19881,7 @@ node-fetch@2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
|
||||
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
|
||||
|
||||
node-fetch@^2.0.0, node-fetch@^2.1.1, node-fetch@^2.1.2:
|
||||
node-fetch@^2.0.0, node-fetch@^2.1.2:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.1.tgz#1fe551e0ded6c45b3b3b937d0fb46f76df718d1e"
|
||||
integrity sha512-ObXBpNCD3A/vYQiQtEWl7DuqjAXjfptYFuGHLdPl5U19/6kJuZV+8uMHLrkj3wJrJoyfg4nhgyFixZdaZoAiEQ==
|
||||
|
@ -21106,6 +21097,13 @@ parse-json@^4.0.0:
|
|||
error-ex "^1.3.1"
|
||||
json-parse-better-errors "^1.0.1"
|
||||
|
||||
parse-link-header@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7"
|
||||
integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc=
|
||||
dependencies:
|
||||
xtend "~4.0.1"
|
||||
|
||||
parse-ms@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
||||
|
@ -21783,7 +21781,7 @@ prettier@1.14.3:
|
|||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895"
|
||||
integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg==
|
||||
|
||||
prettier@1.18.2, prettier@^1.13.7, prettier@^1.14.3, prettier@^1.17.0:
|
||||
prettier@1.18.2, prettier@^1.14.3, prettier@^1.17.0:
|
||||
version "1.18.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
|
||||
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
|
||||
|
@ -26037,6 +26035,13 @@ stringstream@~0.0.4, stringstream@~0.0.5:
|
|||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
|
||||
integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==
|
||||
|
||||
strip-ansi@*, strip-ansi@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
|
||||
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@5.0.0, strip-ansi@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f"
|
||||
|
@ -26065,13 +26070,6 @@ strip-ansi@^4.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-ansi@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
|
||||
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
|
||||
|
@ -29617,6 +29615,15 @@ xml2js@^0.4.19, xml2js@^0.4.5:
|
|||
sax ">=0.6.0"
|
||||
xmlbuilder "~9.0.1"
|
||||
|
||||
xml2js@^0.4.22:
|
||||
version "0.4.22"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02"
|
||||
integrity sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==
|
||||
dependencies:
|
||||
sax ">=0.6.0"
|
||||
util.promisify "~1.0.0"
|
||||
xmlbuilder "~11.0.0"
|
||||
|
||||
xmlbuilder@8.2.2:
|
||||
version "8.2.2"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
|
||||
|
@ -29627,6 +29634,11 @@ xmlbuilder@9.0.7:
|
|||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
||||
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
|
||||
|
||||
xmlbuilder@~11.0.0:
|
||||
version "11.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
||||
|
||||
xmlbuilder@~9.0.1:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue