diff --git a/Jenkinsfile b/Jenkinsfile index 08ccc879054b..eb67bdb2877b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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 """ } diff --git a/package.json b/package.json index c954be8bb2ae..8020dd1a34a4 100644 --- a/package.json +++ b/package.json @@ -280,7 +280,6 @@ "@kbn/utility-types": "1.0.0", "@microsoft/api-documenter": "7.4.3", "@microsoft/api-extractor": "7.4.2", - "@octokit/rest": "^15.10.0", "@percy/agent": "^0.11.0", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", diff --git a/packages/kbn-dev-utils/src/kbn_client/errors.ts b/packages/kbn-dev-utils/src/axios/errors.ts similarity index 73% rename from packages/kbn-dev-utils/src/kbn_client/errors.ts rename to packages/kbn-dev-utils/src/axios/errors.ts index 068c68555b62..e449c49a483b 100644 --- a/packages/kbn-dev-utils/src/kbn_client/errors.ts +++ b/packages/kbn-dev-utils/src/axios/errors.ts @@ -28,15 +28,9 @@ export interface AxiosResponseError extends AxiosError { } export const isAxiosRequestError = (error: any): error is AxiosRequestError => { - return error && error.code === undefined && error.response === undefined; + return error && error.config && error.response === undefined; }; -export const isAxiosResponseError = (error: any): error is AxiosResponseError => { - return error && error.code !== undefined && error.response !== undefined; -}; - -export const isConcliftOnGetError = (error: any) => { - return ( - isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409 - ); +export const isAxiosResponseError = (error: any): error is AxiosResponseError => { + return error && error.response && error.response.status !== undefined; }; diff --git a/packages/kbn-dev-utils/src/axios/index.ts b/packages/kbn-dev-utils/src/axios/index.ts new file mode 100644 index 000000000000..8764f468c533 --- /dev/null +++ b/packages/kbn-dev-utils/src/axios/index.ts @@ -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'; diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 5c69036a4b13..7bd22d73df8d 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -18,9 +18,15 @@ */ 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 { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './constants'; export { KbnClient } from './kbn_client'; +export * from './axios'; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts index 56d4d7f99e0b..25962c91a896 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -21,9 +21,15 @@ import Url from 'url'; import Axios from 'axios'; -import { isAxiosRequestError, isConcliftOnGetError } from './errors'; +import { isAxiosRequestError, isAxiosResponseError } from '../axios'; import { ToolingLog } from '../tooling_log'; +const isConcliftOnGetError = (error: any) => { + return ( + isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409 + ); +}; + export const uriencode = ( strings: TemplateStringsArray, ...values: Array diff --git a/packages/kbn-dev-utils/src/tooling_log/index.ts b/packages/kbn-dev-utils/src/tooling_log/index.ts index e00e1c8b7d1b..1f5afac26d56 100644 --- a/packages/kbn-dev-utils/src/tooling_log/index.ts +++ b/packages/kbn-dev-utils/src/tooling_log/index.ts @@ -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'; diff --git a/src/dev/github_utils/index.js b/packages/kbn-dev-utils/src/tooling_log/tooling_log_collecting_writer.ts similarity index 59% rename from src/dev/github_utils/index.js rename to packages/kbn-dev-utils/src/tooling_log/tooling_log_collecting_writer.ts index 0a3d35cf6a5a..46026bdc369d 100644 --- a/src/dev/github_utils/index.js +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_collecting_writer.ts @@ -17,27 +17,20 @@ * under the License. */ -import Octokit from '@octokit/rest'; -import { markdownMetadata } from './metadata'; +import { ToolingLogTextWriter } from './tooling_log_text_writer'; -export { markdownMetadata }; +export class ToolingLogCollectingWriter extends ToolingLogTextWriter { + messages: string[] = []; -export function getGithubClient() { - const client = new Octokit(); - client.authenticate({ - type: 'token', - token: process.env.GITHUB_TOKEN - }); - - return client; -} - -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); + constructor() { + super({ + level: 'verbose', + writeTo: { + write: msg => { + // trim trailing new line + this.messages.push(msg.slice(0, -1)); + }, + }, + }); } - return data; } diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 80eaaa297227..40a3bd475f1c 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -6,5 +6,5 @@ }, "include": [ "src/**/*" - ], + ] } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index d02b2cf41d3f..86a81207a9fa 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -12,7 +12,10 @@ "devDependencies": { "@babel/cli": "^7.5.5", "@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.2", @@ -20,9 +23,12 @@ "del": "^4.1.1", "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.3", "tmp": "^0.1.0", + "xml2js": "^0.4.22", "zlib": "^1.0.5" } } diff --git a/packages/kbn-test/src/failed_tests_reporter/README.md b/packages/kbn-test/src/failed_tests_reporter/README.md new file mode 100644 index 000000000000..42533a45c714 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/README.md @@ -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. \ No newline at end of file diff --git a/src/dev/failed_tests/__fixtures__/ftr_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/ftr_report.xml similarity index 100% rename from src/dev/failed_tests/__fixtures__/ftr_report.xml rename to packages/kbn-test/src/failed_tests_reporter/__fixtures__/ftr_report.xml diff --git a/src/dev/failed_tests/__fixtures__/jest_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/jest_report.xml similarity index 100% rename from src/dev/failed_tests/__fixtures__/jest_report.xml rename to packages/kbn-test/src/failed_tests_reporter/__fixtures__/jest_report.xml diff --git a/src/dev/failed_tests/__fixtures__/karma_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/karma_report.xml similarity index 100% rename from src/dev/failed_tests/__fixtures__/karma_report.xml rename to packages/kbn-test/src/failed_tests_reporter/__fixtures__/karma_report.xml diff --git a/src/dev/failed_tests/__fixtures__/mocha_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/mocha_report.xml similarity index 100% rename from src/dev/failed_tests/__fixtures__/mocha_report.xml rename to packages/kbn-test/src/failed_tests_reporter/__fixtures__/mocha_report.xml diff --git a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts new file mode 100644 index 000000000000..1e0514a9b1cb --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts @@ -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..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. [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 []`); +}); diff --git a/packages/kbn-test/src/failed_tests_reporter/get_failures.ts b/packages/kbn-test/src/failed_tests_reporter/get_failures.ts new file mode 100644 index 000000000000..d38d118a255e --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/get_failures.ts @@ -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; + /* 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) => { + 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; +} diff --git a/packages/kbn-test/src/failed_tests_reporter/github_api.ts b/packages/kbn-test/src/failed_tests_reporter/github_api.ts new file mode 100644 index 000000000000..4f8b339072ef --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/github_api.ts @@ -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(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(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(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(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(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, + }; + } +} diff --git a/packages/kbn-test/src/failed_tests_reporter/index.ts b/packages/kbn-test/src/failed_tests_reporter/index.ts new file mode 100644 index 000000000000..1332f08eb4e1 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/index.ts @@ -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'; diff --git a/packages/kbn-test/src/failed_tests_reporter/issue_metadata.test.ts b/packages/kbn-test/src/failed_tests_reporter/issue_metadata.test.ts new file mode 100644 index 000000000000..f7585cb941a2 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/issue_metadata.test.ts @@ -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 + + +`; + +const HAS_SOME_OTHER_METADATA = dedent` + # my issue + + some text + + +`; + +const INVALID_METADATA = dedent` + # my issue + + some text + + +`; + +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 + + " + `); + }); + + it('adds metadata if not found', () => { + expect( + updateIssueMetadata(MISSING_METADATA, { + box: 'baz', + }) + ).toMatchInlineSnapshot(` + "# my issue + + some text + + " + `); + + expect( + updateIssueMetadata(HAS_SOME_OTHER_METADATA, { + box: 'baz', + }) + ).toMatchInlineSnapshot(` + "# my issue + + some text + + " + `); + }); + + it('overwrites metdata if JSON is malformed', () => { + expect( + updateIssueMetadata(INVALID_METADATA, { + box: 'baz', + }) + ).toMatchInlineSnapshot(` + "# my issue + + some text + + " + `); + }); +}); diff --git a/src/dev/github_utils/metadata.js b/packages/kbn-test/src/failed_tests_reporter/issue_metadata.ts similarity index 69% rename from src/dev/github_utils/metadata.js rename to packages/kbn-test/src/failed_tests_reporter/issue_metadata.ts index ab50ee3cf959..5c87d7c32299 100644 --- a/src/dev/github_utils/metadata.js +++ b/packages/kbn-test/src/failed_tests_reporter/issue_metadata.ts @@ -17,8 +17,6 @@ * under the License. */ -const REGEX = /\n\n/; - /** * 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/; * 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/; - /** - * 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`; +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) { + 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`; +} diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts new file mode 100644 index 000000000000..7a127b07b16b --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.test.ts @@ -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) + + ", + Array [ + "failed-test", + ], + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + `); + expect(writer.messages).toMatchInlineSnapshot(` + Array [ + " info 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 + + " + `, + }, + log, + api + ); + + expect(api.editIssueBodyAndEnsureOpen).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + 1234, + "# existing issue body + + \\"", + ], + ], + "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 [ + " info Updated issue https://github.com/issues/1234, failCount: 11", + ] + `); + }); +}); diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failure.ts b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts new file mode 100644 index 000000000000..55b4dc6e1203 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/report_failure.ts @@ -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}`); +} diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts new file mode 100644 index 000000000000..40f2cec149ad --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -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 + `, + }, + } + ); +} diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 8f2bd00b51e2..f39fde0d82c5 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -40,3 +40,5 @@ export { readConfigFile } from './functional_test_runner/lib/config/read_config_ // @ts-ignore not typed yet export { runFtrCli } from './functional_test_runner/cli'; + +export { runFailedTestsReporterCli } from './failed_tests_reporter'; diff --git a/scripts/report_failed_tests.js b/scripts/report_failed_tests.js new file mode 100644 index 000000000000..6d23f0695b01 --- /dev/null +++ b/scripts/report_failed_tests.js @@ -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(); diff --git a/src/dev/failed_tests/cli.js b/src/dev/failed_tests/cli.js deleted file mode 100644 index a046f70080c2..000000000000 --- a/src/dev/failed_tests/cli.js +++ /dev/null @@ -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(); diff --git a/src/dev/failed_tests/report.js b/src/dev/failed_tests/report.js deleted file mode 100644 index 68ded3ad26bc..000000000000 --- a/src/dev/failed_tests/report.js +++ /dev/null @@ -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.`)); -} diff --git a/src/dev/failed_tests/report.test.js b/src/dev/failed_tests/report.test.js deleted file mode 100644 index f4c0e2e33edf..000000000000 --- a/src/dev/failed_tests/report.test.js +++ /dev/null @@ -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..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 - - 503 Service Temporarily Unavailable - -

503 Service Temporarily Unavailable

-
nginx/1.13.7
- - - - 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. [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", - }, -] -`); - }); - }); -}); diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index e32edd783949..d2d8d8f43e3b 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -109,13 +109,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(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); } diff --git a/src/dev/mocha/junit_report_generation.js b/src/dev/mocha/junit_report_generation.js index 87899c63dfea..eb1808b182f1 100644 --- a/src/dev/mocha/junit_report_generation.js +++ b/src/dev/mocha/junit_report_generation.js @@ -142,13 +142,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(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); }); diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 4c5f9ed271cf..ac0e3e812de7 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -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 diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 2cae804b9441..dc91eb3a2c3d 100755 --- a/test/scripts/jenkins_firefox_smoke.sh +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -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 diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index fc379ec1c5ec..b1ed3ed4551e 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -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 diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_visual_regression.sh index abfb0cca0e99..da077541ea1d 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_visual_regression.sh @@ -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 diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 34928f256415..4faa1ff79314 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -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 diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index d41f2ed9f1ae..a7d443e0586a 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -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 diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh index 216c1c39370e..a30a98472ad2 100755 --- a/test/scripts/jenkins_xpack_firefox_smoke.sh +++ b/test/scripts/jenkins_xpack_firefox_smoke.sh @@ -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 diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index cc9b6adcecd4..7312a9938c10 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -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 diff --git a/yarn.lock b/yarn.lock index 28d8b38a6e41..f67c4579352f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1485,14 +1485,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz#dfa0c92efe44a1d1a7974fb49ffeb40ef2da5a27" integrity sha512-zVgvPwGK7c1aVdUVc9Qv7SqepOGRDrqCw7KZPSZziWGxSlbII3gmvGLPzLX4d0n0BMbamBacUrN22zOMyFFEkQ== -"@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" - "@gulp-sourcemaps/identity-map@1.X": version "1.0.2" resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz#1e6fe5d8027b1f285dc0d31762f566bccd73d5a9" @@ -2323,21 +2315,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" @@ -3798,6 +3775,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.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4" @@ -4072,6 +4054,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" @@ -4219,6 +4208,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-parser@*": version "13.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0" @@ -6525,11 +6521,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -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" @@ -14931,7 +14922,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== @@ -19865,11 +19856,6 @@ 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.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.1.tgz#1fe551e0ded6c45b3b3b937d0fb46f76df718d1e" - integrity sha512-ObXBpNCD3A/vYQiQtEWl7DuqjAXjfptYFuGHLdPl5U19/6kJuZV+8uMHLrkj3wJrJoyfg4nhgyFixZdaZoAiEQ== - node-fetch@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" @@ -21199,6 +21185,13 @@ parse-json@^5.0.0: json-parse-better-errors "^1.0.1" lines-and-columns "^1.1.6" +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" @@ -21887,7 +21880,7 @@ prettier@1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== -prettier@1.18.2, prettier@^1.13.7, prettier@^1.18.2: +prettier@1.18.2, prettier@^1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== @@ -26234,7 +26227,7 @@ 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@5.2.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@*, strip-ansi@5.2.0, strip-ansi@^5.1.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==