mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[kbn-scout] Custom event-oriented test reporter & persistence (#202906)](https://github.com/elastic/kibana/pull/202906) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"David Olaru","email":"dolaru@elastic.co"},"sourceCommit":{"committedDate":"2024-12-09T14:34:25Z","message":"[kbn-scout] Custom event-oriented test reporter & persistence (#202906)","sha":"ad4e8efd0f07f8f682709efce271493a4872e331","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","test:scout"],"number":202906,"url":"https://github.com/elastic/kibana/pull/202906","mergeCommit":{"message":"[kbn-scout] Custom event-oriented test reporter & persistence (#202906)","sha":"ad4e8efd0f07f8f682709efce271493a4872e331"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202906","number":202906,"mergeCommit":{"message":"[kbn-scout] Custom event-oriented test reporter & persistence (#202906)","sha":"ad4e8efd0f07f8f682709efce271493a4872e331"}}]}] BACKPORT--> --------- Co-authored-by: David Olaru <dolaru@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8e8fdee03f
commit
8d49feb271
48 changed files with 1761 additions and 71 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -779,6 +779,8 @@ x-pack/plugins/saved_objects_tagging @elastic/appex-sharedux
|
|||
packages/kbn-saved-search-component @elastic/obs-ux-logs-team
|
||||
src/plugins/saved_search @elastic/kibana-data-discovery
|
||||
packages/kbn-scout @elastic/appex-qa
|
||||
packages/kbn-scout-info @elastic/appex-qa
|
||||
packages/kbn-scout-reporting @elastic/appex-qa
|
||||
examples/screenshot_mode_example @elastic/appex-sharedux
|
||||
src/plugins/screenshot_mode @elastic/appex-sharedux
|
||||
x-pack/examples/screenshotting_example @elastic/appex-sharedux
|
||||
|
|
|
@ -1496,6 +1496,8 @@
|
|||
"@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier",
|
||||
"@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli",
|
||||
"@kbn/scout": "link:packages/kbn-scout",
|
||||
"@kbn/scout-info": "link:packages/kbn-scout-info",
|
||||
"@kbn/scout-reporting": "link:packages/kbn-scout-reporting",
|
||||
"@kbn/security-api-integration-helpers": "link:x-pack/test/security_api_integration/packages/helpers",
|
||||
"@kbn/serverless-storybook-config": "link:packages/serverless/storybook/config",
|
||||
"@kbn/some-dev-log": "link:packages/kbn-some-dev-log",
|
||||
|
|
6
packages/kbn-scout-info/README.md
Normal file
6
packages/kbn-scout-info/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# @kbn/scout-info
|
||||
|
||||
This package stores information that's commonly used by packages in the `@kbn/scout*` namespace, and any other
|
||||
package that wishes to extend Scout functionality.
|
||||
|
||||
Check out the `@kbn/scout` package if you want to learn more about Scout.
|
|
@ -7,5 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
require('../src/setup_node_env');
|
||||
require('@kbn/scout').runTestsCli();
|
||||
export * from './src/paths';
|
||||
export * from './src/reporting';
|
14
packages/kbn-scout-info/jest.config.js
Normal file
14
packages/kbn-scout-info/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-scout-info'],
|
||||
};
|
6
packages/kbn-scout-info/kibana.jsonc
Normal file
6
packages/kbn-scout-info/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/scout-info",
|
||||
"owner": "@elastic/appex-qa",
|
||||
"devOnly": true
|
||||
}
|
6
packages/kbn-scout-info/package.json
Normal file
6
packages/kbn-scout-info/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/scout-info",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
19
packages/kbn-scout-info/src/paths.ts
Normal file
19
packages/kbn-scout-info/src/paths.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
|
||||
export const SCOUT_OUTPUT_ROOT = path.resolve(REPO_ROOT, '.scout');
|
||||
|
||||
// Servers
|
||||
export const SCOUT_SERVERS_ROOT = path.resolve(SCOUT_OUTPUT_ROOT, 'servers');
|
||||
|
||||
// Reporting
|
||||
export const SCOUT_REPORT_OUTPUT_ROOT = path.resolve(SCOUT_OUTPUT_ROOT, 'reports');
|
12
packages/kbn-scout-info/src/reporting.ts
Normal file
12
packages/kbn-scout-info/src/reporting.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export const SCOUT_TEST_EVENTS_TEMPLATE_NAME = 'scout-test-events';
|
||||
export const SCOUT_TEST_EVENTS_INDEX_PATTERN = `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-*`;
|
||||
export const SCOUT_TEST_EVENTS_DATA_STREAM_NAME = `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-kibana`;
|
19
packages/kbn-scout-info/tsconfig.json
Normal file
19
packages/kbn-scout-info/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/repo-info"
|
||||
]
|
||||
}
|
5
packages/kbn-scout-reporting/README.md
Normal file
5
packages/kbn-scout-reporting/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# @kbn/scout-reporting
|
||||
|
||||
This package contains reporting functionality for Scout.
|
||||
|
||||
Check out the `@kbn/scout` package if you want to learn more about Scout.
|
12
packages/kbn-scout-reporting/index.ts
Normal file
12
packages/kbn-scout-reporting/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * as cli from './src/cli';
|
||||
export * as datasources from './src/datasources';
|
||||
export * from './src/reporting';
|
14
packages/kbn-scout-reporting/jest.config.js
Normal file
14
packages/kbn-scout-reporting/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-scout-reporting'],
|
||||
};
|
6
packages/kbn-scout-reporting/kibana.jsonc
Normal file
6
packages/kbn-scout-reporting/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/scout-reporting",
|
||||
"owner": "@elastic/appex-qa",
|
||||
"devOnly": true
|
||||
}
|
6
packages/kbn-scout-reporting/package.json
Normal file
6
packages/kbn-scout-reporting/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/scout-reporting",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
37
packages/kbn-scout-reporting/src/cli/common.ts
Normal file
37
packages/kbn-scout-reporting/src/cli/common.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Client as ESClient, ClientOptions as ESClientOptions } from '@elastic/elasticsearch';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { createFailError } from '@kbn/dev-cli-errors';
|
||||
|
||||
/**
|
||||
* Get an Elasticsearch client for which connectivity has been validated
|
||||
*
|
||||
* @param options Elasticsearch client options
|
||||
* @param log Logger instance
|
||||
* @throws FailError if cluster information cannot be read from the target Elasticsearch instance
|
||||
*/
|
||||
export async function getValidatedESClient(
|
||||
options: ESClientOptions,
|
||||
log: ToolingLog
|
||||
): Promise<ESClient> {
|
||||
const es = new ESClient(options);
|
||||
|
||||
await es.info().then(
|
||||
(esInfo) => {
|
||||
log.info(`Connected to Elasticsearch node '${esInfo.name}'`);
|
||||
},
|
||||
(err) => {
|
||||
throw createFailError(`Failed to connect to Elasticsearch\n${err}`);
|
||||
}
|
||||
);
|
||||
|
||||
return es;
|
||||
}
|
11
packages/kbn-scout-reporting/src/cli/index.ts
Normal file
11
packages/kbn-scout-reporting/src/cli/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { initializeReportDatastream } from './initialize_report_datastream';
|
||||
export { uploadEvents } from './upload_events';
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Command } from '@kbn/dev-cli-runner';
|
||||
import { ScoutReportDataStream } from '../reporting/report';
|
||||
import { getValidatedESClient } from './common';
|
||||
|
||||
export const initializeReportDatastream: Command<void> = {
|
||||
name: 'initialize-report-datastream',
|
||||
description: 'Initialize a Scout report datastream in Elasticsearch',
|
||||
flags: {
|
||||
string: ['esURL', 'esAPIKey'],
|
||||
boolean: ['verifyTLSCerts'],
|
||||
default: {
|
||||
esURL: process.env.ES_URL,
|
||||
esAPIKey: process.env.ES_API_KEY,
|
||||
},
|
||||
help: `
|
||||
--esURL (required) Elasticsearch URL [env: ES_URL]
|
||||
--esAPIKey (required) Elasticsearch API Key [env: ES_API_KEY]
|
||||
--verifyTLSCerts (optional) Verify TLS certificates
|
||||
`,
|
||||
},
|
||||
run: async ({ flagsReader, log }) => {
|
||||
const esURL = flagsReader.requiredString('esURL');
|
||||
const esAPIKey = flagsReader.requiredString('esAPIKey');
|
||||
|
||||
// ES connection
|
||||
log.info(`Connecting to Elasticsearch at ${esURL}`);
|
||||
const es = await getValidatedESClient(
|
||||
{
|
||||
node: esURL,
|
||||
auth: { apiKey: esAPIKey },
|
||||
tls: {
|
||||
rejectUnauthorized: flagsReader.boolean('verifyTLSCerts'),
|
||||
},
|
||||
},
|
||||
log
|
||||
);
|
||||
|
||||
// Initialize the report datastream
|
||||
const reportDataStream = new ScoutReportDataStream(es, log);
|
||||
await reportDataStream.initialize();
|
||||
|
||||
log.success('Scout report data stream initialized');
|
||||
},
|
||||
};
|
61
packages/kbn-scout-reporting/src/cli/upload_events.ts
Normal file
61
packages/kbn-scout-reporting/src/cli/upload_events.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { Command } from '@kbn/dev-cli-runner';
|
||||
import { createFlagError } from '@kbn/dev-cli-errors';
|
||||
import { ScoutReportDataStream } from '../reporting/report';
|
||||
import { getValidatedESClient } from './common';
|
||||
|
||||
export const uploadEvents: Command<void> = {
|
||||
name: 'upload-events',
|
||||
description: 'Upload events recorded by the Scout reporter to Elasticsearch',
|
||||
flags: {
|
||||
string: ['eventLogPath', 'esURL', 'esAPIKey'],
|
||||
boolean: ['verifyTLSCerts'],
|
||||
default: {
|
||||
esURL: process.env.ES_URL,
|
||||
esAPIKey: process.env.ES_API_KEY,
|
||||
},
|
||||
help: `
|
||||
--eventLogPath (required) Path to the event log to upload
|
||||
--esURL (required) Elasticsearch URL [env: ES_URL]
|
||||
--esAPIKey (required) Elasticsearch API Key [env: ES_API_KEY]
|
||||
--verifyTLSCerts (optional) Verify TLS certificates
|
||||
`,
|
||||
},
|
||||
run: async ({ flagsReader, log }) => {
|
||||
// Read & validate CLI options
|
||||
const eventLogPath = flagsReader.requiredString('eventLogPath');
|
||||
|
||||
if (!fs.existsSync(eventLogPath)) {
|
||||
throw createFlagError(`Event log path '${eventLogPath}' does not exist.`);
|
||||
}
|
||||
|
||||
const esURL = flagsReader.requiredString('esURL');
|
||||
const esAPIKey = flagsReader.requiredString('esAPIKey');
|
||||
|
||||
// ES connection
|
||||
log.info(`Connecting to Elasticsearch at ${esURL}`);
|
||||
const es = await getValidatedESClient(
|
||||
{
|
||||
node: esURL,
|
||||
auth: { apiKey: esAPIKey },
|
||||
tls: {
|
||||
rejectUnauthorized: flagsReader.boolean('verifyTLSCerts'),
|
||||
},
|
||||
},
|
||||
log
|
||||
);
|
||||
|
||||
// Event log upload
|
||||
const reportDataStream = new ScoutReportDataStream(es, log);
|
||||
await reportDataStream.addEventsFromFile(eventLogPath);
|
||||
},
|
||||
};
|
87
packages/kbn-scout-reporting/src/datasources/buildkite.ts
Normal file
87
packages/kbn-scout-reporting/src/datasources/buildkite.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Buildkite info
|
||||
*/
|
||||
export interface BuildkiteMetadata {
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
job_id?: string;
|
||||
message?: string;
|
||||
build: {
|
||||
id?: string;
|
||||
number?: string;
|
||||
url?: string;
|
||||
};
|
||||
pipeline: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
slug?: string;
|
||||
};
|
||||
agent: {
|
||||
name?: string;
|
||||
};
|
||||
group: {
|
||||
id?: string;
|
||||
key?: string;
|
||||
label?: string;
|
||||
};
|
||||
step: {
|
||||
id?: string;
|
||||
key?: string;
|
||||
label?: string;
|
||||
};
|
||||
command?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buildkite information extracted from environment variables
|
||||
*
|
||||
* This object is empty if the process is not running in a Buildkite pipeline.
|
||||
*/
|
||||
export const buildkite: BuildkiteMetadata =
|
||||
process.env.BUILDKITE === 'true'
|
||||
? {
|
||||
branch: process.env.BUILDKITE_BRANCH,
|
||||
commit: process.env.BUILDKITE_COMMIT,
|
||||
job_id: process.env.BUILDKITE_JOB_ID,
|
||||
message: process.env.BUILDKITE_MESSAGE,
|
||||
build: {
|
||||
id: process.env.BUILDKITE_BUILD_ID,
|
||||
number: process.env.BUILDKITE_BUILD_NUMBER,
|
||||
url: process.env.BUILDKITE_BUILD_URL,
|
||||
},
|
||||
pipeline: {
|
||||
id: process.env.BUILDKITE_PIPELINE_ID,
|
||||
name: process.env.BUILDKITE_PIPELINE_NAME,
|
||||
slug: process.env.BUILDKITE_PIPELINE_SLUG,
|
||||
},
|
||||
agent: {
|
||||
name: process.env.BUILDKITE_AGENT_NAME,
|
||||
},
|
||||
group: {
|
||||
id: process.env.BUILDKITE_GROUP_ID,
|
||||
key: process.env.BUILDKITE_GROUP_KEY,
|
||||
label: process.env.BUILDKITE_GROUP_LABEL,
|
||||
},
|
||||
step: {
|
||||
id: process.env.BUILDKITE_STEP_ID,
|
||||
key: process.env.BUILDKITE_STEP_KEY,
|
||||
label: process.env.BUILDKITE_LABEL,
|
||||
},
|
||||
command: process.env.BUILDKITE_COMMAND,
|
||||
}
|
||||
: {
|
||||
build: {},
|
||||
pipeline: {},
|
||||
agent: {},
|
||||
group: {},
|
||||
step: {},
|
||||
};
|
41
packages/kbn-scout-reporting/src/datasources/host.ts
Normal file
41
packages/kbn-scout-reporting/src/datasources/host.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import os from 'node:os';
|
||||
|
||||
/**
|
||||
* Host info
|
||||
*/
|
||||
export interface HostMetadata {
|
||||
architecture: string;
|
||||
hostname: string;
|
||||
os: OSMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Operating system info
|
||||
*/
|
||||
export interface OSMetadata {
|
||||
platform: string;
|
||||
version: string;
|
||||
family: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the host this process is running on
|
||||
*/
|
||||
export const host: HostMetadata = {
|
||||
architecture: os.arch(),
|
||||
hostname: os.hostname(),
|
||||
os: {
|
||||
platform: os.platform(),
|
||||
version: os.release(),
|
||||
family: os.type(),
|
||||
},
|
||||
};
|
19
packages/kbn-scout-reporting/src/datasources/index.ts
Normal file
19
packages/kbn-scout-reporting/src/datasources/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { buildkite } from './buildkite';
|
||||
import { host } from './host';
|
||||
|
||||
export * from './buildkite';
|
||||
export * from './host';
|
||||
|
||||
export const environmentMetadata = {
|
||||
buildkite,
|
||||
host,
|
||||
};
|
30
packages/kbn-scout-reporting/src/reporting/index.ts
Normal file
30
packages/kbn-scout-reporting/src/reporting/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import type { ReporterDescription } from 'playwright/test';
|
||||
import type { ScoutPlaywrightReporterOptions } from './playwright';
|
||||
|
||||
export * from './report';
|
||||
|
||||
// ID helpers
|
||||
export function generateTestRunId() {
|
||||
return randomBytes(8).toString('hex');
|
||||
}
|
||||
|
||||
export function getTestIDForTitle(title: string) {
|
||||
return createHash('sha256').update(title).digest('hex').slice(0, 31);
|
||||
}
|
||||
|
||||
// Playwright reporting
|
||||
export const scoutPlaywrightReporter = (
|
||||
options?: ScoutPlaywrightReporterOptions
|
||||
): ReporterDescription => {
|
||||
return ['@kbn/scout-reporting/src/reporting/playwright.ts', options];
|
||||
};
|
300
packages/kbn-scout-reporting/src/reporting/playwright.ts
Normal file
300
packages/kbn-scout-reporting/src/reporting/playwright.ts
Normal file
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type {
|
||||
FullConfig,
|
||||
FullResult,
|
||||
Reporter,
|
||||
Suite,
|
||||
TestCase,
|
||||
TestError,
|
||||
TestResult,
|
||||
TestStep,
|
||||
} from '@playwright/test/reporter';
|
||||
|
||||
import path from 'node:path';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { SCOUT_REPORT_OUTPUT_ROOT } from '@kbn/scout-info';
|
||||
import stripANSI from 'strip-ansi';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { PathWithOwners, getPathsWithOwnersReversed, getCodeOwnersForFile } from '@kbn/code-owners';
|
||||
import { generateTestRunId, getTestIDForTitle, ScoutReport, ScoutReportEventAction } from '.';
|
||||
import { environmentMetadata } from '../datasources';
|
||||
|
||||
/**
|
||||
* Configuration options for the Scout Playwright reporter
|
||||
*/
|
||||
export interface ScoutPlaywrightReporterOptions {
|
||||
name?: string;
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout Playwright reporter
|
||||
*/
|
||||
export class ScoutPlaywrightReporter implements Reporter {
|
||||
readonly log: ToolingLog;
|
||||
readonly name: string;
|
||||
readonly runId: string;
|
||||
private report: ScoutReport;
|
||||
private readonly pathsWithOwners: PathWithOwners[];
|
||||
|
||||
constructor(private reporterOptions: ScoutPlaywrightReporterOptions = {}) {
|
||||
this.log = new ToolingLog({
|
||||
level: 'info',
|
||||
writeTo: process.stdout,
|
||||
});
|
||||
|
||||
this.name = this.reporterOptions.name || 'unknown';
|
||||
this.runId = generateTestRunId();
|
||||
this.log.info(`Scout test run ID: ${this.runId}`);
|
||||
|
||||
this.report = new ScoutReport(this.log);
|
||||
this.pathsWithOwners = getPathsWithOwnersReversed();
|
||||
}
|
||||
|
||||
private getFileOwners(filePath: string): string[] {
|
||||
const concatenatedOwners = getCodeOwnersForFile(filePath, this.pathsWithOwners);
|
||||
|
||||
if (concatenatedOwners === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return concatenatedOwners
|
||||
.replace(/#.+$/, '')
|
||||
.split(',')
|
||||
.filter((value) => value.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root path of this reporter's output
|
||||
*/
|
||||
public get reportRootPath(): string {
|
||||
const outputPath = this.reporterOptions.outputPath || SCOUT_REPORT_OUTPUT_ROOT;
|
||||
return path.join(outputPath, `scout-playwright-${this.runId}`);
|
||||
}
|
||||
|
||||
printsToStdio(): boolean {
|
||||
// Don't take over console output
|
||||
return false;
|
||||
}
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.RUN_BEGIN,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onTestBegin(test: TestCase, result: TestResult) {
|
||||
this.report.logEvent({
|
||||
'@timestamp': result.startTime,
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
},
|
||||
test: {
|
||||
id: getTestIDForTitle(test.titlePath().join(' ')),
|
||||
title: test.title,
|
||||
tags: test.tags,
|
||||
annotations: test.annotations,
|
||||
expected_status: test.expectedStatus,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_BEGIN,
|
||||
},
|
||||
file: {
|
||||
path: path.relative(REPO_ROOT, test.location.file),
|
||||
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
||||
this.report.logEvent({
|
||||
'@timestamp': step.startTime,
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
},
|
||||
test: {
|
||||
id: getTestIDForTitle(test.titlePath().join(' ')),
|
||||
title: test.title,
|
||||
tags: test.tags,
|
||||
annotations: test.annotations,
|
||||
expected_status: test.expectedStatus,
|
||||
step: {
|
||||
title: step.titlePath().join(' '),
|
||||
category: step.category,
|
||||
},
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_STEP_BEGIN,
|
||||
},
|
||||
file: {
|
||||
path: path.relative(REPO_ROOT, test.location.file),
|
||||
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
},
|
||||
test: {
|
||||
id: getTestIDForTitle(test.titlePath().join(' ')),
|
||||
title: test.title,
|
||||
tags: test.tags,
|
||||
annotations: test.annotations,
|
||||
expected_status: test.expectedStatus,
|
||||
step: {
|
||||
title: step.titlePath().join(' '),
|
||||
category: step.category,
|
||||
duration: step.duration,
|
||||
},
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_STEP_END,
|
||||
error: {
|
||||
message: step.error?.message ? stripANSI(step.error.message) : undefined,
|
||||
stack_trace: step.error?.stack ? stripANSI(step.error.stack) : undefined,
|
||||
},
|
||||
},
|
||||
file: {
|
||||
path: path.relative(REPO_ROOT, test.location.file),
|
||||
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onTestEnd(test: TestCase, result: TestResult) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
},
|
||||
test: {
|
||||
id: getTestIDForTitle(test.titlePath().join(' ')),
|
||||
title: test.title,
|
||||
tags: test.tags,
|
||||
annotations: test.annotations,
|
||||
expected_status: test.expectedStatus,
|
||||
status: result.status,
|
||||
duration: result.duration,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_END,
|
||||
error: {
|
||||
message: result.error?.message ? stripANSI(result.error.message) : undefined,
|
||||
stack_trace: result.error?.stack ? stripANSI(result.error.stack) : undefined,
|
||||
},
|
||||
},
|
||||
file: {
|
||||
path: path.relative(REPO_ROOT, test.location.file),
|
||||
owner: this.getFileOwners(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onEnd(result: FullResult) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
status: result.status,
|
||||
duration: result.duration,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.RUN_END,
|
||||
},
|
||||
});
|
||||
|
||||
// Save & conclude the report
|
||||
try {
|
||||
this.report.save(this.reportRootPath);
|
||||
} finally {
|
||||
this.report.conclude();
|
||||
}
|
||||
}
|
||||
|
||||
async onExit() {
|
||||
// noop
|
||||
}
|
||||
|
||||
onError(error: TestError) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.ERROR,
|
||||
error: {
|
||||
message: error.message ? stripANSI(error.message) : undefined,
|
||||
stack_trace: error.stack ? stripANSI(error.stack) : undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ScoutPlaywrightReporter;
|
108
packages/kbn-scout-reporting/src/reporting/report/event.ts
Normal file
108
packages/kbn-scout-reporting/src/reporting/report/event.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { BuildkiteMetadata, HostMetadata } from '../../datasources';
|
||||
|
||||
/**
|
||||
* Scout reporter event type
|
||||
*/
|
||||
export enum ScoutReportEventAction {
|
||||
RUN_BEGIN = 'run-begin',
|
||||
RUN_END = 'run-end',
|
||||
TEST_BEGIN = 'test-begin',
|
||||
TEST_END = 'test-end',
|
||||
TEST_STEP_BEGIN = 'test-step-begin',
|
||||
TEST_STEP_END = 'test-step-end',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout report event info
|
||||
*/
|
||||
export interface ScoutReportEventInfo {
|
||||
action: ScoutReportEventAction;
|
||||
outcome?: 'failure' | 'success' | 'unknown';
|
||||
error?: {
|
||||
message?: string;
|
||||
id?: string;
|
||||
code?: string;
|
||||
stack_trace?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout reporter info
|
||||
*/
|
||||
export interface ScoutReporterInfo {
|
||||
name: string;
|
||||
type: 'jest' | 'ftr' | 'playwright';
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout test run info
|
||||
*/
|
||||
export interface ScoutTestRunInfo {
|
||||
id: string;
|
||||
status?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout suite info
|
||||
*/
|
||||
export interface ScoutSuiteInfo {
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout test info
|
||||
*/
|
||||
export interface ScoutTestInfo {
|
||||
id: string;
|
||||
title: string;
|
||||
tags: string[];
|
||||
annotations?: Array<{
|
||||
type: string;
|
||||
description?: string;
|
||||
}>;
|
||||
expected_status?: string;
|
||||
duration?: number;
|
||||
status?: string;
|
||||
step?: {
|
||||
title: string;
|
||||
category?: string;
|
||||
duration?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout file info
|
||||
*/
|
||||
export interface ScoutFileInfo {
|
||||
path: string;
|
||||
owner: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Document that records an event to be logged by the Scout reporter
|
||||
*/
|
||||
export interface ScoutReportEvent {
|
||||
'@timestamp'?: Date;
|
||||
buildkite?: BuildkiteMetadata;
|
||||
host?: HostMetadata;
|
||||
event: ScoutReportEventInfo;
|
||||
file?: ScoutFileInfo;
|
||||
labels?: { [id: string]: string };
|
||||
reporter: ScoutReporterInfo;
|
||||
test_run: ScoutTestRunInfo;
|
||||
suite?: ScoutSuiteInfo;
|
||||
test?: ScoutTestInfo;
|
||||
}
|
103
packages/kbn-scout-reporting/src/reporting/report/index.ts
Normal file
103
packages/kbn-scout-reporting/src/reporting/report/index.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { ScoutReportEvent } from './event';
|
||||
|
||||
/**
|
||||
* Generic error raised by a Scout report
|
||||
*/
|
||||
export class ScoutReportError extends Error {}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class ScoutReport {
|
||||
log: ToolingLog;
|
||||
workDir: string;
|
||||
concluded = false;
|
||||
|
||||
constructor(log?: ToolingLog) {
|
||||
this.log = log || new ToolingLog();
|
||||
this.workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scout-report-'));
|
||||
}
|
||||
|
||||
public get eventLogPath(): string {
|
||||
return path.join(this.workDir, `event-log.ndjson`);
|
||||
}
|
||||
|
||||
private raiseIfConcluded(additionalInfo?: string) {
|
||||
if (this.concluded) {
|
||||
let message = `Report at ${this.workDir} was concluded`;
|
||||
|
||||
if (additionalInfo) {
|
||||
message += `: ${additionalInfo}`;
|
||||
}
|
||||
|
||||
throw new ScoutReportError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an event to be processed by this reporter
|
||||
*
|
||||
* @param event {ScoutReportEvent} - Event to record
|
||||
*/
|
||||
logEvent(event: ScoutReportEvent) {
|
||||
this.raiseIfConcluded('logging new events is no longer allowed');
|
||||
|
||||
if (event['@timestamp'] === undefined) {
|
||||
event['@timestamp'] = new Date();
|
||||
}
|
||||
|
||||
fs.appendFileSync(this.eventLogPath, JSON.stringify(event) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the report to a non-ephemeral location
|
||||
*
|
||||
* @param destination - Full path to the save location. Must not exist.
|
||||
*/
|
||||
save(destination: string) {
|
||||
this.raiseIfConcluded('nothing to save because workdir has been cleared');
|
||||
|
||||
if (fs.existsSync(destination)) {
|
||||
throw new ScoutReportError(`Save destination path '${destination}' already exists`);
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
this.log.info(`Saving Scout report to ${destination}`);
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
|
||||
// Copy the workdir data to the destination
|
||||
fs.cpSync(this.workDir, destination, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when you're done adding information to this report.
|
||||
*
|
||||
* ⚠️**This will delete all the contents of the report's working directory**
|
||||
*/
|
||||
conclude() {
|
||||
// Remove the working directory
|
||||
this.log.info(`Removing Scout report working directory ${this.workDir}`);
|
||||
fs.rmSync(this.workDir, { recursive: true, force: true });
|
||||
|
||||
// Mark this report as concluded
|
||||
this.concluded = true;
|
||||
this.log.success('Scout report has concluded.');
|
||||
}
|
||||
}
|
||||
|
||||
export * from './event';
|
||||
export * from './persistence';
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ClusterPutComponentTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
buildkiteProperties,
|
||||
reporterProperties,
|
||||
testRunProperties,
|
||||
suiteProperties,
|
||||
testProperties,
|
||||
} from './mappings';
|
||||
|
||||
export const buildkiteMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.buildkite',
|
||||
version: 1,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
buildkite: {
|
||||
type: 'object',
|
||||
properties: buildkiteProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const reporterMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.reporter',
|
||||
version: 1,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
reporter: {
|
||||
type: 'object',
|
||||
properties: reporterProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const testRunMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.test-run',
|
||||
version: 1,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
test_run: {
|
||||
type: 'object',
|
||||
properties: testRunProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const suiteMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.suite',
|
||||
version: 1,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
suite: {
|
||||
type: 'object',
|
||||
properties: suiteProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const testMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.test',
|
||||
version: 1,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
test: {
|
||||
type: 'object',
|
||||
properties: testProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { Client as ESClient } from '@elastic/elasticsearch';
|
||||
import { SCOUT_TEST_EVENTS_DATA_STREAM_NAME } from '@kbn/scout-info';
|
||||
import { ScoutReportEvent } from '../event';
|
||||
import * as componentTemplates from './component_templates';
|
||||
import * as indexTemplates from './index_templates';
|
||||
|
||||
export class ScoutReportDataStream {
|
||||
private log: ToolingLog;
|
||||
|
||||
constructor(private es: ESClient, log?: ToolingLog) {
|
||||
this.log = log || new ToolingLog();
|
||||
}
|
||||
|
||||
async exists() {
|
||||
return await this.es.indices.exists({ index: SCOUT_TEST_EVENTS_DATA_STREAM_NAME });
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.setupComponentTemplates();
|
||||
await this.setupIndexTemplate();
|
||||
|
||||
if (await this.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info(`Creating data stream '${SCOUT_TEST_EVENTS_DATA_STREAM_NAME}'`);
|
||||
await this.es.indices.createDataStream({
|
||||
name: SCOUT_TEST_EVENTS_DATA_STREAM_NAME,
|
||||
});
|
||||
}
|
||||
|
||||
async setupComponentTemplates() {
|
||||
for (const template of [
|
||||
componentTemplates.buildkiteMappings,
|
||||
componentTemplates.reporterMappings,
|
||||
componentTemplates.testRunMappings,
|
||||
componentTemplates.suiteMappings,
|
||||
componentTemplates.testMappings,
|
||||
]) {
|
||||
const templateExists = await this.es.cluster.existsComponentTemplate({ name: template.name });
|
||||
if (!templateExists) {
|
||||
this.log.info(`Creating component template '${template.name}'`);
|
||||
await this.es.cluster.putComponentTemplate(template);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Template exists but might need to be updated
|
||||
const newTemplateVersion = template.version || 0;
|
||||
const existingTemplateVersion =
|
||||
(await this.es.cluster.getComponentTemplate({ name: template.name })).component_templates[0]
|
||||
.component_template.version || 0;
|
||||
|
||||
if (existingTemplateVersion >= newTemplateVersion) {
|
||||
this.log.info(`Component template '${template.name} exists and is up to date.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.log.info(
|
||||
`Updating component template '${template.name}' (version ${existingTemplateVersion} -> ${newTemplateVersion})`
|
||||
);
|
||||
await this.es.cluster.putComponentTemplate(template);
|
||||
}
|
||||
}
|
||||
|
||||
async setupIndexTemplate() {
|
||||
const template = indexTemplates.testEvents;
|
||||
const templateExists: boolean = await this.es.indices.existsIndexTemplate({
|
||||
name: template.name,
|
||||
});
|
||||
|
||||
if (!templateExists) {
|
||||
this.log.info(`Creating index template '${template.name}'`);
|
||||
await this.es.indices.putIndexTemplate(template);
|
||||
return;
|
||||
}
|
||||
|
||||
// Template exists but might need to be updated
|
||||
const newTemplateVersion = template.version || 0;
|
||||
const existingTemplateVersion =
|
||||
(await this.es.indices.getIndexTemplate({ name: template.name })).index_templates[0]
|
||||
.index_template.version || 0;
|
||||
|
||||
if (existingTemplateVersion >= newTemplateVersion) {
|
||||
this.log.info(`Index template '${template.name} exists and is up to date.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info(
|
||||
`Updating index template '${template.name}' (version ${existingTemplateVersion} -> ${newTemplateVersion})`
|
||||
);
|
||||
await this.es.indices.putIndexTemplate(template);
|
||||
}
|
||||
|
||||
async addEvent(event: ScoutReportEvent) {
|
||||
await this.es.index({ index: SCOUT_TEST_EVENTS_DATA_STREAM_NAME, document: event });
|
||||
}
|
||||
|
||||
async addEventsFromFile(eventLogPath: string) {
|
||||
// Make the given event log path absolute
|
||||
eventLogPath = path.resolve(eventLogPath);
|
||||
|
||||
const events = async function* () {
|
||||
const lineReader = readline.createInterface({
|
||||
input: fs.createReadStream(eventLogPath),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
for await (const line of lineReader) {
|
||||
yield line;
|
||||
}
|
||||
};
|
||||
|
||||
this.log.info(
|
||||
`Uploading events from file ${eventLogPath} to data stream '${SCOUT_TEST_EVENTS_DATA_STREAM_NAME}'`
|
||||
);
|
||||
|
||||
const stats = await this.es.helpers.bulk({
|
||||
datasource: events(),
|
||||
onDocument: () => {
|
||||
return { create: { _index: SCOUT_TEST_EVENTS_DATA_STREAM_NAME } };
|
||||
},
|
||||
});
|
||||
|
||||
this.log.info(`Uploaded ${stats.total} events in ${stats.time / 1000}s.`);
|
||||
|
||||
if (stats.failed > 0) {
|
||||
this.log.warning(`Failed to upload ${stats.failed} events`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SCOUT_TEST_EVENTS_TEMPLATE_NAME, SCOUT_TEST_EVENTS_INDEX_PATTERN } from '@kbn/scout-info';
|
||||
import * as componentTemplates from './component_templates';
|
||||
|
||||
export const testEvents: IndicesPutIndexTemplateRequest = {
|
||||
name: SCOUT_TEST_EVENTS_TEMPLATE_NAME,
|
||||
version: 1,
|
||||
data_stream: {},
|
||||
index_patterns: SCOUT_TEST_EVENTS_INDEX_PATTERN,
|
||||
composed_of: [
|
||||
'ecs@mappings',
|
||||
componentTemplates.buildkiteMappings.name,
|
||||
componentTemplates.reporterMappings.name,
|
||||
componentTemplates.testRunMappings.name,
|
||||
componentTemplates.suiteMappings.name,
|
||||
componentTemplates.testMappings.name,
|
||||
],
|
||||
};
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import { PropertyName, MappingProperty } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
export const buildkiteProperties: Record<PropertyName, MappingProperty> = {
|
||||
branch: {
|
||||
type: 'keyword',
|
||||
},
|
||||
commit: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
job_id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
message: {
|
||||
type: 'text',
|
||||
},
|
||||
build: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
number: {
|
||||
type: 'integer',
|
||||
},
|
||||
url: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
},
|
||||
},
|
||||
pipeline: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
slug: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
},
|
||||
},
|
||||
group: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
key: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
label: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
step: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
key: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
label: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
command: {
|
||||
type: 'wildcard',
|
||||
fields: {
|
||||
text: {
|
||||
type: 'match_only_text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const reporterProperties: Record<PropertyName, MappingProperty> = {
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
};
|
||||
|
||||
export const testRunProperties: Record<PropertyName, MappingProperty> = {
|
||||
id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
status: {
|
||||
type: 'keyword',
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
},
|
||||
};
|
||||
|
||||
export const suiteProperties: Record<PropertyName, MappingProperty> = {
|
||||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
};
|
||||
|
||||
export const testProperties: Record<PropertyName, MappingProperty> = {
|
||||
id: {
|
||||
type: 'wildcard',
|
||||
},
|
||||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
tags: {
|
||||
type: 'keyword',
|
||||
},
|
||||
annotations: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
expected_status: {
|
||||
type: 'keyword',
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
},
|
||||
status: {
|
||||
type: 'keyword',
|
||||
},
|
||||
step: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
category: {
|
||||
type: 'keyword',
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
24
packages/kbn-scout-reporting/tsconfig.json
Normal file
24
packages/kbn-scout-reporting/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/tooling-log",
|
||||
"@kbn/dev-cli-runner",
|
||||
"@kbn/dev-cli-errors",
|
||||
"@kbn/scout-info",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/code-owners",
|
||||
]
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { startServersCli, runTestsCli } from './src/cli';
|
||||
export * as cli from './src/cli';
|
||||
export { expect, test, createPlaywrightConfig, createLazyPageObject } from './src/playwright';
|
||||
export type {
|
||||
ScoutPage,
|
||||
|
|
|
@ -6,6 +6,16 @@
|
|||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import { RunWithCommands } from '@kbn/dev-cli-runner';
|
||||
import { cli as reportingCLI } from '@kbn/scout-reporting';
|
||||
import { startServer } from './start_server';
|
||||
import { runTests } from './run_tests';
|
||||
|
||||
export { runTestsCli } from './run_tests_cli';
|
||||
export { startServersCli } from './start_servers_cli';
|
||||
export async function run() {
|
||||
await new RunWithCommands(
|
||||
{
|
||||
description: 'Scout CLI',
|
||||
},
|
||||
[startServer, runTests, reportingCLI.initializeReportDatastream, reportingCLI.uploadEvents]
|
||||
).execute();
|
||||
}
|
||||
|
|
40
packages/kbn-scout/src/cli/run_tests.ts
Normal file
40
packages/kbn-scout/src/cli/run_tests.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Command } from '@kbn/dev-cli-runner';
|
||||
import { initLogsDir } from '@kbn/test';
|
||||
import { TEST_FLAG_OPTIONS } from '../playwright/runner';
|
||||
import { parseTestFlags, runTests as runTestsFn } from '../playwright/runner';
|
||||
|
||||
/**
|
||||
* Start servers and run the tests
|
||||
*/
|
||||
export const runTests: Command<void> = {
|
||||
name: 'run-tests',
|
||||
description: `
|
||||
Run a Scout Playwright config.
|
||||
|
||||
Note:
|
||||
This also handles server starts. Make sure a Scout test server is not already running before invoking this command.
|
||||
|
||||
Common usage:
|
||||
node scripts/scout run-tests --stateful --config <playwright_config_path>
|
||||
node scripts/scout run-tests --serverless=es --headed --config <playwright_config_path>
|
||||
`,
|
||||
flags: TEST_FLAG_OPTIONS,
|
||||
run: async ({ flagsReader, log }) => {
|
||||
const options = await parseTestFlags(flagsReader);
|
||||
|
||||
if (options.logsDir) {
|
||||
await initLogsDir(log, options.logsDir);
|
||||
}
|
||||
|
||||
await runTestsFn(log, options);
|
||||
},
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { run } from '@kbn/dev-cli-runner';
|
||||
import { initLogsDir } from '@kbn/test';
|
||||
import { TEST_FLAG_OPTIONS, parseTestFlags, runTests } from '../playwright/runner';
|
||||
|
||||
/**
|
||||
* Start servers and run the tests
|
||||
*/
|
||||
export function runTestsCli() {
|
||||
run(
|
||||
async ({ flagsReader, log }) => {
|
||||
const options = await parseTestFlags(flagsReader);
|
||||
|
||||
if (options.logsDir) {
|
||||
initLogsDir(log, options.logsDir);
|
||||
}
|
||||
|
||||
await runTests(log, options);
|
||||
},
|
||||
{
|
||||
description: `Run Scout UI Tests`,
|
||||
usage: `
|
||||
Usage:
|
||||
node scripts/scout_test --help
|
||||
node scripts/scout_test --stateful --config <playwright_config_path>
|
||||
node scripts/scout_test --serverless=es --headed --config <playwright_config_path>
|
||||
`,
|
||||
flags: TEST_FLAG_OPTIONS,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -7,8 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { run } from '@kbn/dev-cli-runner';
|
||||
|
||||
import { Command } from '@kbn/dev-cli-runner';
|
||||
import { initLogsDir } from '@kbn/test';
|
||||
|
||||
import { startServers, parseServerFlags, SERVER_FLAG_OPTIONS } from '../servers';
|
||||
|
@ -16,19 +15,16 @@ import { startServers, parseServerFlags, SERVER_FLAG_OPTIONS } from '../servers'
|
|||
/**
|
||||
* Start servers
|
||||
*/
|
||||
export function startServersCli() {
|
||||
run(
|
||||
async ({ flagsReader: flags, log }) => {
|
||||
const options = parseServerFlags(flags);
|
||||
export const startServer: Command<void> = {
|
||||
name: 'start-server',
|
||||
description: 'Start Elasticsearch & Kibana for testing purposes',
|
||||
flags: SERVER_FLAG_OPTIONS,
|
||||
run: async ({ flagsReader, log }) => {
|
||||
const options = parseServerFlags(flagsReader);
|
||||
|
||||
if (options.logsDir) {
|
||||
initLogsDir(log, options.logsDir);
|
||||
}
|
||||
|
||||
await startServers(log, options);
|
||||
},
|
||||
{
|
||||
flags: SERVER_FLAG_OPTIONS,
|
||||
if (options.logsDir) {
|
||||
await initLogsDir(log, options.logsDir);
|
||||
}
|
||||
);
|
||||
}
|
||||
await startServers(log, options);
|
||||
},
|
||||
};
|
|
@ -12,7 +12,7 @@ import getopts from 'getopts';
|
|||
import path from 'path';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { ServerlessProjectType } from '@kbn/es';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info';
|
||||
import { CliSupportedServerModes, ScoutServerConfig } from '../types';
|
||||
import { getConfigFilePath } from './get_config_file';
|
||||
import { loadConfig } from './loader/config_load';
|
||||
|
@ -30,15 +30,14 @@ export const formatCurrentDate = () => {
|
|||
};
|
||||
|
||||
const saveTestServersConfigOnDisk = (testServersConfig: ScoutServerConfig, log: ToolingLog) => {
|
||||
const configDirPath = path.resolve(REPO_ROOT, '.scout', 'servers');
|
||||
const configFilePath = path.join(configDirPath, `local.json`);
|
||||
const configFilePath = path.join(SCOUT_SERVERS_ROOT, `local.json`);
|
||||
|
||||
try {
|
||||
const jsonData = JSON.stringify(testServersConfig, null, 2);
|
||||
|
||||
if (!Fs.existsSync(configDirPath)) {
|
||||
log.debug(`scout: creating configuration directory: ${configDirPath}`);
|
||||
Fs.mkdirSync(configDirPath, { recursive: true });
|
||||
if (!Fs.existsSync(SCOUT_SERVERS_ROOT)) {
|
||||
log.debug(`scout: creating configuration directory: ${SCOUT_SERVERS_ROOT}`);
|
||||
Fs.mkdirSync(SCOUT_SERVERS_ROOT, { recursive: true });
|
||||
}
|
||||
|
||||
Fs.writeFileSync(configFilePath, jsonData, 'utf-8');
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
*/
|
||||
|
||||
import { defineConfig, PlaywrightTestConfig, devices } from '@playwright/test';
|
||||
import * as Path from 'path';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { scoutPlaywrightReporter } from '@kbn/scout-reporting';
|
||||
import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info';
|
||||
import { ScoutPlaywrightOptions, ScoutTestOptions, VALID_CONFIG_MARKER } from '../types';
|
||||
|
||||
export function createPlaywrightConfig(options: ScoutPlaywrightOptions): PlaywrightTestConfig {
|
||||
|
@ -27,10 +27,11 @@ export function createPlaywrightConfig(options: ScoutPlaywrightOptions): Playwri
|
|||
reporter: [
|
||||
['html', { outputFolder: './output/reports', open: 'never' }], // HTML report configuration
|
||||
['json', { outputFile: './output/reports/test-results.json' }], // JSON report
|
||||
scoutPlaywrightReporter({ name: 'scout-playwright' }), // Scout report
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
serversConfigDir: Path.resolve(REPO_ROOT, '.scout', 'servers'),
|
||||
serversConfigDir: SCOUT_SERVERS_ROOT,
|
||||
[VALID_CONFIG_MARKER]: true,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
|
2
packages/kbn-scout/src/types/config.d.ts
vendored
2
packages/kbn-scout/src/types/config.d.ts
vendored
|
@ -29,6 +29,6 @@ export interface ScoutLoaderConfig {
|
|||
buildArgs?: string[];
|
||||
sourceArgs?: string[];
|
||||
serverArgs: string[];
|
||||
useDedicatedTastRunner?: boolean;
|
||||
useDedicatedTestRunner?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,5 +27,7 @@
|
|||
"@kbn/mock-idp-utils",
|
||||
"@kbn/test-suites-xpack",
|
||||
"@kbn/test-subj-selector",
|
||||
"@kbn/scout-info",
|
||||
"@kbn/scout-reporting"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface Suite extends Runnable {
|
|||
suites: Suite[];
|
||||
tests: Test[];
|
||||
title: string;
|
||||
fullTitle(): string;
|
||||
file: string;
|
||||
parent?: Suite;
|
||||
eachTest: (cb: (test: Test) => void) => void;
|
||||
|
@ -39,6 +40,7 @@ export interface Test extends Runnable {
|
|||
parent?: Suite;
|
||||
isPassed: () => boolean;
|
||||
pending?: boolean;
|
||||
err?: Error;
|
||||
}
|
||||
|
||||
export interface Runnable {
|
||||
|
@ -51,10 +53,22 @@ export interface Runnable {
|
|||
parent?: Suite;
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
suites: number;
|
||||
tests: number;
|
||||
passes: number;
|
||||
pending: number;
|
||||
failures: number;
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface Runner extends EventEmitter {
|
||||
abort(): void;
|
||||
failures: any[];
|
||||
uncaught: (error: Error) => void;
|
||||
stats?: Stats;
|
||||
}
|
||||
|
||||
export interface Mocha {
|
||||
|
|
|
@ -178,6 +178,12 @@ export const schema = Joi.object()
|
|||
})
|
||||
.default(),
|
||||
|
||||
scoutReporter: Joi.object()
|
||||
.keys({
|
||||
enabled: Joi.boolean().default(process.env.ENABLE_SCOUT_REPORTER || false),
|
||||
})
|
||||
.default(),
|
||||
|
||||
users: Joi.object().pattern(
|
||||
ID_PATTERN,
|
||||
Joi.object()
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as symbols from './symbols';
|
|||
import { ms } from './ms';
|
||||
import { writeEpilogue } from './write_epilogue';
|
||||
import { setupCiStatsFtrTestGroupReporter } from './ci_stats_ftr_reporter';
|
||||
import { ScoutFTRReporter } from './scout_ftr_reporter';
|
||||
|
||||
export function MochaReporterProvider({ getService }) {
|
||||
const log = getService('log');
|
||||
|
@ -65,6 +66,10 @@ export function MochaReporterProvider({ getService }) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.get('scoutReporter.enabled')) {
|
||||
new ScoutFTRReporter(runner);
|
||||
}
|
||||
}
|
||||
|
||||
onStart = () => {
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { SCOUT_REPORT_OUTPUT_ROOT } from '@kbn/scout-info';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import {
|
||||
generateTestRunId,
|
||||
getTestIDForTitle,
|
||||
ScoutReport,
|
||||
ScoutReportEventAction,
|
||||
datasources,
|
||||
} from '@kbn/scout-reporting';
|
||||
import { getCodeOwnersForFile, getPathsWithOwnersReversed, PathWithOwners } from '@kbn/code-owners';
|
||||
import { Runner, Test } from '../../../fake_mocha_types';
|
||||
|
||||
/**
|
||||
* Configuration options for the Scout Mocha reporter
|
||||
*/
|
||||
export interface ScoutFTRReporterOptions {
|
||||
name?: string;
|
||||
outputPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout Mocha reporter
|
||||
*/
|
||||
export class ScoutFTRReporter {
|
||||
readonly log: ToolingLog;
|
||||
readonly name: string;
|
||||
readonly runId: string;
|
||||
private report: ScoutReport;
|
||||
private readonly pathsWithOwners: PathWithOwners[];
|
||||
|
||||
constructor(private runner: Runner, private reporterOptions: ScoutFTRReporterOptions = {}) {
|
||||
this.log = new ToolingLog({
|
||||
level: 'info',
|
||||
writeTo: process.stdout,
|
||||
});
|
||||
|
||||
this.name = this.reporterOptions.name || 'ftr';
|
||||
this.runId = generateTestRunId();
|
||||
this.log.info(`Scout test run ID: ${this.runId}`);
|
||||
|
||||
this.report = new ScoutReport(this.log);
|
||||
this.pathsWithOwners = getPathsWithOwnersReversed();
|
||||
|
||||
// Register event listeners
|
||||
for (const [eventName, listener] of Object.entries({
|
||||
start: this.onRunStart,
|
||||
end: this.onRunEnd,
|
||||
test: this.onTestStart,
|
||||
'test end': this.onTestEnd,
|
||||
})) {
|
||||
runner.on(eventName, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private getFileOwners(filePath: string): string[] {
|
||||
const concatenatedOwners = getCodeOwnersForFile(filePath, this.pathsWithOwners);
|
||||
|
||||
if (concatenatedOwners === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return concatenatedOwners
|
||||
.replace(/#.+$/, '')
|
||||
.split(',')
|
||||
.filter((value) => value.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root path of this reporter's output
|
||||
*/
|
||||
public get reportRootPath(): string {
|
||||
const outputPath = this.reporterOptions.outputPath || SCOUT_REPORT_OUTPUT_ROOT;
|
||||
return path.join(outputPath, `scout-ftr-${this.runId}`);
|
||||
}
|
||||
|
||||
onRunStart = () => {
|
||||
/**
|
||||
* Root suite execution began (all files have been parsed and hooks/tests are ready for execution)
|
||||
*/
|
||||
this.report.logEvent({
|
||||
...datasources.environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.RUN_BEGIN,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onTestStart = (test: Test) => {
|
||||
/**
|
||||
* Test execution started
|
||||
*/
|
||||
this.report.logEvent({
|
||||
...datasources.environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
suite: {
|
||||
title: test.parent?.fullTitle() || 'unknown',
|
||||
type: test.parent?.root ? 'root' : 'suite',
|
||||
},
|
||||
test: {
|
||||
id: getTestIDForTitle(test.fullTitle()),
|
||||
title: test.title,
|
||||
tags: [],
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_BEGIN,
|
||||
},
|
||||
file: {
|
||||
path: test.file ? path.relative(REPO_ROOT, test.file) : 'unknown',
|
||||
owner: test.file ? this.getFileOwners(path.relative(REPO_ROOT, test.file)) : 'unknown',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onTestEnd = (test: Test) => {
|
||||
/**
|
||||
* Test execution ended
|
||||
*/
|
||||
this.report.logEvent({
|
||||
...datasources.environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
suite: {
|
||||
title: test.parent?.fullTitle() || 'unknown',
|
||||
type: test.parent?.root ? 'root' : 'suite',
|
||||
},
|
||||
test: {
|
||||
id: getTestIDForTitle(test.fullTitle()),
|
||||
title: test.title,
|
||||
tags: [],
|
||||
status: test.isPending() ? 'skipped' : test.isPassed() ? 'passed' : 'failed',
|
||||
duration: test.duration,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_END,
|
||||
error: {
|
||||
message: test.err?.message,
|
||||
stack_trace: test.err?.stack,
|
||||
},
|
||||
},
|
||||
file: {
|
||||
path: test.file ? path.relative(REPO_ROOT, test.file) : 'unknown',
|
||||
owner: test.file ? this.getFileOwners(path.relative(REPO_ROOT, test.file)) : 'unknown',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onRunEnd = () => {
|
||||
/**
|
||||
* Root suite execution has ended
|
||||
*/
|
||||
this.report.logEvent({
|
||||
...datasources.environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
status: this.runner.stats?.failures === 0 ? 'passed' : 'failed',
|
||||
duration: this.runner.stats?.duration || 0,
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.RUN_END,
|
||||
},
|
||||
});
|
||||
|
||||
// Save & conclude the report
|
||||
try {
|
||||
this.report.save(this.reportRootPath);
|
||||
} finally {
|
||||
this.report.conclude();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -37,6 +37,8 @@
|
|||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/mock-idp-utils",
|
||||
"@kbn/code-owners",
|
||||
"@kbn/scout-reporting",
|
||||
"@kbn/scout-info",
|
||||
"@kbn/react-mute-legacy-root-warning",
|
||||
]
|
||||
}
|
||||
|
|
4
scripts/scout_start_servers.js → scripts/scout.js
Normal file → Executable file
4
scripts/scout_start_servers.js → scripts/scout.js
Normal file → Executable file
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
|
@ -8,4 +10,4 @@
|
|||
*/
|
||||
|
||||
require('../src/setup_node_env');
|
||||
require('@kbn/scout').startServersCli();
|
||||
void require('@kbn/scout').cli.run();
|
|
@ -1552,6 +1552,10 @@
|
|||
"@kbn/saved-search-plugin/*": ["src/plugins/saved_search/*"],
|
||||
"@kbn/scout": ["packages/kbn-scout"],
|
||||
"@kbn/scout/*": ["packages/kbn-scout/*"],
|
||||
"@kbn/scout-info": ["packages/kbn-scout-info"],
|
||||
"@kbn/scout-info/*": ["packages/kbn-scout-info/*"],
|
||||
"@kbn/scout-reporting": ["packages/kbn-scout-reporting"],
|
||||
"@kbn/scout-reporting/*": ["packages/kbn-scout-reporting/*"],
|
||||
"@kbn/screenshot-mode-example-plugin": ["examples/screenshot_mode_example"],
|
||||
"@kbn/screenshot-mode-example-plugin/*": ["examples/screenshot_mode_example/*"],
|
||||
"@kbn/screenshot-mode-plugin": ["src/plugins/screenshot_mode"],
|
||||
|
|
|
@ -6907,6 +6907,14 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/scout-info@link:packages/kbn-scout-info":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/scout-reporting@link:packages/kbn-scout-reporting":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/scout@link:packages/kbn-scout":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue