[7.x] refactor failed_tests_reporter to use TS, no octokit (#4… (#47631)

* 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:
#	packages/kbn-test/src/index.ts
#	renovate.json5
This commit is contained in:
Spencer 2019-10-08 15:18:34 -07:00 committed by GitHub
parent 95f482d80c
commit 3fadf26699
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1074 additions and 561 deletions

2
Jenkinsfile vendored
View file

@ -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
"""
}

View file

@ -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",

View file

@ -28,15 +28,9 @@ export interface AxiosResponseError<T> 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<any> => {
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 = <T = any>(error: any): error is AxiosResponseError<T> => {
return error && error.response && error.response.status !== undefined;
};

View 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';

View file

@ -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';

View file

@ -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<string | number | boolean>

View file

@ -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';

View file

@ -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;
}

View file

@ -6,5 +6,5 @@
},
"include": [
"src/**/*"
],
]
}

View file

@ -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"
}
}

View 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.

View file

@ -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 []`);
});

View 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;
}

View 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(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<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,
};
}
}

View 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';

View file

@ -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\\"}} -->"
`);
});
});

View file

@ -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 })} -->`;
}

View file

@ -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 [
" 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
<!-- 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 [
" info Updated issue https://github.com/issues/1234, failCount: 11",
]
`);
});
});

View file

@ -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}`);
}

View file

@ -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
`,
},
}
);
}

View file

@ -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';

View 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();

View file

@ -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();

View file

@ -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.`));
}

View file

@ -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",
},
]
`);
});
});
});

View file

@ -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');
}

View file

@ -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');
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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==