mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[kbn-scout] Scout reporter updates (#206431)](https://github.com/elastic/kibana/pull/206431) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"David Olaru","email":"dolaru@elastic.co"},"sourceCommit":{"committedDate":"2025-01-28T23:08:37Z","message":"[kbn-scout] Scout reporter updates (#206431)\n\n## Summary\r\n\r\n- Centralized Scout reporter settings\r\n- Added owner area and config/test file information to reporter events\r\n- Attempt to upload events at the end of a test run\r\n- Enable Scout reporter test events upload for the `pull request` and\r\n`on merge` pipelines","sha":"fd7053b319f3df2820e3e1879092703635bbc3ae","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"],"title":"[kbn-scout] Scout reporter updates","number":206431,"url":"https://github.com/elastic/kibana/pull/206431","mergeCommit":{"message":"[kbn-scout] Scout reporter updates (#206431)\n\n## Summary\r\n\r\n- Centralized Scout reporter settings\r\n- Added owner area and config/test file information to reporter events\r\n- Attempt to upload events at the end of a test run\r\n- Enable Scout reporter test events upload for the `pull request` and\r\n`on merge` pipelines","sha":"fd7053b319f3df2820e3e1879092703635bbc3ae"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206431","number":206431,"mergeCommit":{"message":"[kbn-scout] Scout reporter updates (#206431)\n\n## Summary\r\n\r\n- Centralized Scout reporter settings\r\n- Added owner area and config/test file information to reporter events\r\n- Attempt to upload events at the end of a test run\r\n- Enable Scout reporter test events upload for the `pull request` and\r\n`on merge` pipelines","sha":"fd7053b319f3df2820e3e1879092703635bbc3ae"}}]}] BACKPORT--> Co-authored-by: David Olaru <dolaru@elastic.co>
This commit is contained in:
parent
37bb66473b
commit
96a4c33070
19 changed files with 287 additions and 111 deletions
|
@ -24,6 +24,7 @@ spec:
|
|||
GITHUB_COMMIT_STATUS_CONTEXT: buildkite/on-merge
|
||||
REPORT_FAILED_TESTS_TO_GITHUB: 'true'
|
||||
ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true'
|
||||
SCOUT_REPORTER_ENABLED: 'true'
|
||||
allow_rebuilds: true
|
||||
branch_configuration: main 7.17 8.15
|
||||
default_branch: main
|
||||
|
|
|
@ -23,6 +23,7 @@ spec:
|
|||
ELASTIC_GITHUB_BUILD_COMMIT_STATUS_ENABLED: 'true'
|
||||
ELASTIC_GITHUB_STEP_COMMIT_STATUS_ENABLED: 'true'
|
||||
GITHUB_BUILD_COMMIT_STATUS_CONTEXT: kibana-ci
|
||||
SCOUT_REPORTER_ENABLED: 'true'
|
||||
allow_rebuilds: true
|
||||
branch_configuration: ''
|
||||
cancel_intermediate_builds: true
|
||||
|
|
|
@ -167,6 +167,17 @@ EOF
|
|||
export TEST_FAILURES_ES_PASSWORD
|
||||
}
|
||||
|
||||
# Scout reporter settings
|
||||
{
|
||||
export SCOUT_REPORTER_ENABLED="${SCOUT_REPORTER_ENABLED:-false}"
|
||||
|
||||
SCOUT_REPORTER_ES_URL="$(vault_get scout/reporter/cluster-credentials es-url)"
|
||||
export SCOUT_REPORTER_ES_URL
|
||||
|
||||
SCOUT_REPORTER_ES_API_KEY="$(vault_get scout/reporter/cluster-credentials es-api-key)"
|
||||
export SCOUT_REPORTER_ES_API_KEY
|
||||
}
|
||||
|
||||
# Setup Bazel Remote/Local Cache Credentials
|
||||
{
|
||||
BAZEL_LOCAL_DEV_CACHE_CREDENTIALS_FILE="$HOME/.kibana-ci-bazel-remote-cache-local-dev.json"
|
||||
|
|
|
@ -7,6 +7,22 @@
|
|||
* 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`;
|
||||
function booleanFromEnv(varName: string, defaultValue: boolean = false): boolean {
|
||||
const envValue = process.env[varName];
|
||||
if (envValue === undefined || envValue.trim().length === 0) return defaultValue;
|
||||
return ['1', 'yes', 'true'].includes(envValue.trim().toLowerCase());
|
||||
}
|
||||
|
||||
export const SCOUT_REPORTER_ENABLED = booleanFromEnv('SCOUT_REPORTER_ENABLED');
|
||||
export const SCOUT_REPORTER_ES_URL = process.env.SCOUT_REPORTER_ES_URL;
|
||||
export const SCOUT_REPORTER_ES_API_KEY = process.env.SCOUT_REPORTER_ES_API_KEY;
|
||||
export const SCOUT_REPORTER_ES_VERIFY_CERTS = booleanFromEnv(
|
||||
'SCOUT_REPORTER_ES_VERIFY_CERTS',
|
||||
true
|
||||
);
|
||||
export const SCOUT_TEST_EVENTS_TEMPLATE_NAME =
|
||||
process.env.SCOUT_TEST_EVENTS_TEMPLATE_NAME || 'scout-test-events';
|
||||
export const SCOUT_TEST_EVENTS_INDEX_PATTERN =
|
||||
process.env.SCOUT_TEST_EVENTS_INDEX_PATTERN || `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-*`;
|
||||
export const SCOUT_TEST_EVENTS_DATA_STREAM_NAME =
|
||||
process.env.SCOUT_TEST_EVENTS_DATA_STREAM_NAME || `${SCOUT_TEST_EVENTS_TEMPLATE_NAME}-kibana`;
|
||||
|
|
|
@ -9,4 +9,5 @@
|
|||
|
||||
export * as cli from './src/cli';
|
||||
export * as datasources from './src/datasources';
|
||||
export * from './src/helpers';
|
||||
export * from './src/reporting';
|
||||
|
|
|
@ -8,8 +8,13 @@
|
|||
*/
|
||||
|
||||
import { Command } from '@kbn/dev-cli-runner';
|
||||
import {
|
||||
SCOUT_REPORTER_ES_API_KEY,
|
||||
SCOUT_REPORTER_ES_URL,
|
||||
SCOUT_REPORTER_ES_VERIFY_CERTS,
|
||||
} from '@kbn/scout-info';
|
||||
import { ScoutReportDataStream } from '../reporting/report/events';
|
||||
import { getValidatedESClient } from './common';
|
||||
import { getValidatedESClient } from '../helpers/elasticsearch';
|
||||
|
||||
export const initializeReportDatastream: Command<void> = {
|
||||
name: 'initialize-report-datastream',
|
||||
|
@ -18,13 +23,14 @@ export const initializeReportDatastream: Command<void> = {
|
|||
string: ['esURL', 'esAPIKey'],
|
||||
boolean: ['verifyTLSCerts'],
|
||||
default: {
|
||||
esURL: process.env.ES_URL,
|
||||
esAPIKey: process.env.ES_API_KEY,
|
||||
esURL: SCOUT_REPORTER_ES_URL,
|
||||
esAPIKey: SCOUT_REPORTER_ES_API_KEY,
|
||||
verifyTLSCerts: SCOUT_REPORTER_ES_VERIFY_CERTS,
|
||||
},
|
||||
help: `
|
||||
--esURL (required) Elasticsearch URL [env: ES_URL]
|
||||
--esAPIKey (required) Elasticsearch API Key [env: ES_API_KEY]
|
||||
--verifyTLSCerts (optional) Verify TLS certificates
|
||||
--esURL (required) Elasticsearch URL [env: SCOUT_REPORTER_ES_URL]
|
||||
--esAPIKey (required) Elasticsearch API Key [env: SCOUT_REPORTER_ES_API_KEY]
|
||||
--verifyTLSCerts (optional) Verify TLS certificates [env: SCOUT_REPORTER_ES_VERIFY_CERTS]
|
||||
`,
|
||||
},
|
||||
run: async ({ flagsReader, log }) => {
|
||||
|
@ -41,7 +47,7 @@ export const initializeReportDatastream: Command<void> = {
|
|||
rejectUnauthorized: flagsReader.boolean('verifyTLSCerts'),
|
||||
},
|
||||
},
|
||||
log
|
||||
{ log, cli: true }
|
||||
);
|
||||
|
||||
// Initialize the report datastream
|
||||
|
|
|
@ -10,8 +10,13 @@
|
|||
import fs from 'node:fs';
|
||||
import { Command } from '@kbn/dev-cli-runner';
|
||||
import { createFlagError } from '@kbn/dev-cli-errors';
|
||||
import {
|
||||
SCOUT_REPORTER_ES_URL,
|
||||
SCOUT_REPORTER_ES_API_KEY,
|
||||
SCOUT_REPORTER_ES_VERIFY_CERTS,
|
||||
} from '@kbn/scout-info';
|
||||
import { ScoutReportDataStream } from '../reporting/report/events';
|
||||
import { getValidatedESClient } from './common';
|
||||
import { getValidatedESClient } from '../helpers/elasticsearch';
|
||||
|
||||
export const uploadEvents: Command<void> = {
|
||||
name: 'upload-events',
|
||||
|
@ -20,14 +25,15 @@ export const uploadEvents: Command<void> = {
|
|||
string: ['eventLogPath', 'esURL', 'esAPIKey'],
|
||||
boolean: ['verifyTLSCerts'],
|
||||
default: {
|
||||
esURL: process.env.ES_URL,
|
||||
esAPIKey: process.env.ES_API_KEY,
|
||||
esURL: SCOUT_REPORTER_ES_URL,
|
||||
esAPIKey: SCOUT_REPORTER_ES_API_KEY,
|
||||
verifyTLSCerts: SCOUT_REPORTER_ES_VERIFY_CERTS,
|
||||
},
|
||||
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
|
||||
--esURL (required) Elasticsearch URL [env: SCOUT_REPORTER_ES_URL]
|
||||
--esAPIKey (required) Elasticsearch API Key [env: SCOUT_REPORTER_ES_API_KEY]
|
||||
--verifyTLSCerts (optional) Verify TLS certificates [env: SCOUT_REPORTER_ES_VERIFY_CERTS]
|
||||
`,
|
||||
},
|
||||
run: async ({ flagsReader, log }) => {
|
||||
|
@ -51,7 +57,7 @@ export const uploadEvents: Command<void> = {
|
|||
rejectUnauthorized: flagsReader.boolean('verifyTLSCerts'),
|
||||
},
|
||||
},
|
||||
log
|
||||
{ log, cli: true }
|
||||
);
|
||||
|
||||
// Event log upload
|
||||
|
|
|
@ -14,22 +14,31 @@ 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
|
||||
* @param esClientOptions Elasticsearch client options
|
||||
* @param helperSettings Settings for this helper
|
||||
* @param helperSettings.log Logger instance
|
||||
* @param helperSettings.cli Set to `true` when invoked from a CLI context
|
||||
* @throws FailError if cluster information cannot be read from the target Elasticsearch instance
|
||||
*/
|
||||
export async function getValidatedESClient(
|
||||
options: ESClientOptions,
|
||||
log: ToolingLog
|
||||
esClientOptions: ESClientOptions,
|
||||
helperSettings: {
|
||||
log?: ToolingLog;
|
||||
cli?: boolean;
|
||||
}
|
||||
): Promise<ESClient> {
|
||||
const es = new ESClient(options);
|
||||
const { log, cli = false } = helperSettings;
|
||||
const es = new ESClient(esClientOptions);
|
||||
|
||||
await es.info().then(
|
||||
(esInfo) => {
|
||||
log.info(`Connected to Elasticsearch node '${esInfo.name}'`);
|
||||
if (log !== undefined) {
|
||||
log.info(`Connected to Elasticsearch node '${esInfo.name}'`);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
throw createFailError(`Failed to connect to Elasticsearch\n${err}`);
|
||||
const msg = `Failed to connect to Elasticsearch\n${err}`;
|
||||
throw cli ? createFailError(msg) : Error(msg);
|
||||
}
|
||||
);
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { ReporterDescription } from 'playwright/test';
|
||||
import { SCOUT_REPORTER_ENABLED } from '@kbn/scout-info';
|
||||
import { ScoutPlaywrightReporterOptions } from './playwright/scout_playwright_reporter';
|
||||
|
||||
export * from './report';
|
||||
|
@ -16,7 +17,9 @@ export * from './report';
|
|||
export const scoutPlaywrightReporter = (
|
||||
options?: ScoutPlaywrightReporterOptions
|
||||
): ReporterDescription => {
|
||||
return ['@kbn/scout-reporting/src/reporting/playwright/events', options];
|
||||
return SCOUT_REPORTER_ENABLED
|
||||
? ['@kbn/scout-reporting/src/reporting/playwright/events', options]
|
||||
: ['null'];
|
||||
};
|
||||
|
||||
// Playwright failed test reporting
|
||||
|
@ -25,5 +28,3 @@ export const scoutFailedTestsReporter = (
|
|||
): ReporterDescription => {
|
||||
return ['@kbn/scout-reporting/src/reporting/playwright/failed_test', options];
|
||||
};
|
||||
|
||||
export { generateTestRunId, getTestIDForTitle } from '../helpers';
|
||||
|
|
|
@ -25,10 +25,18 @@ import stripANSI from 'strip-ansi';
|
|||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import {
|
||||
type CodeOwnersEntry,
|
||||
type CodeOwnerArea,
|
||||
getCodeOwnersEntries,
|
||||
getOwningTeamsForPath,
|
||||
findAreaForCodeOwner,
|
||||
} from '@kbn/code-owners';
|
||||
import { ScoutEventsReport, ScoutReportEventAction } from '../../report';
|
||||
import {
|
||||
ScoutEventsReport,
|
||||
ScoutFileInfo,
|
||||
ScoutReportEventAction,
|
||||
type ScoutTestRunInfo,
|
||||
uploadScoutReportEvents,
|
||||
} from '../../report';
|
||||
import { environmentMetadata } from '../../../datasources';
|
||||
import type { ScoutPlaywrightReporterOptions } from '../scout_playwright_reporter';
|
||||
import { generateTestRunId, getTestIDForTitle } from '../../../helpers';
|
||||
|
@ -41,6 +49,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
readonly name: string;
|
||||
readonly runId: string;
|
||||
private report: ScoutEventsReport;
|
||||
private baseTestRunInfo: ScoutTestRunInfo;
|
||||
private readonly codeOwnersEntries: CodeOwnersEntry[];
|
||||
|
||||
constructor(private reporterOptions: ScoutPlaywrightReporterOptions = {}) {
|
||||
|
@ -54,6 +63,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
this.log.info(`Scout test run ID: ${this.runId}`);
|
||||
|
||||
this.report = new ScoutEventsReport(this.log);
|
||||
this.baseTestRunInfo = { id: this.runId };
|
||||
this.codeOwnersEntries = getCodeOwnersEntries();
|
||||
}
|
||||
|
||||
|
@ -61,6 +71,22 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
return getOwningTeamsForPath(filePath, this.codeOwnersEntries);
|
||||
}
|
||||
|
||||
private getOwnerAreas(owners: string[]): CodeOwnerArea[] {
|
||||
return owners
|
||||
.map((owner) => findAreaForCodeOwner(owner))
|
||||
.filter((area) => area !== undefined) as CodeOwnerArea[];
|
||||
}
|
||||
|
||||
private getScoutFileInfoForPath(filePath: string): ScoutFileInfo {
|
||||
const fileOwners = this.getFileOwners(filePath);
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
owner: fileOwners,
|
||||
area: this.getOwnerAreas(fileOwners),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Root path of this reporter's output
|
||||
*/
|
||||
|
@ -74,16 +100,29 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
return false;
|
||||
}
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
onBegin(config: FullConfig, _: Suite) {
|
||||
// Enrich base test run info with config file info
|
||||
let configInfo: ScoutTestRunInfo['config'];
|
||||
|
||||
if (config.configFile !== undefined) {
|
||||
configInfo = {
|
||||
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, config.configFile)),
|
||||
};
|
||||
}
|
||||
|
||||
this.baseTestRunInfo = {
|
||||
...this.baseTestRunInfo,
|
||||
config: configInfo,
|
||||
};
|
||||
|
||||
// Log event
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
event: {
|
||||
action: ScoutReportEventAction.RUN_BEGIN,
|
||||
},
|
||||
|
@ -98,9 +137,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
|
@ -111,18 +148,15 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
tags: test.tags,
|
||||
annotations: test.annotations,
|
||||
expected_status: test.expectedStatus,
|
||||
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
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) {
|
||||
onStepBegin(test: TestCase, _: TestResult, step: TestStep) {
|
||||
this.report.logEvent({
|
||||
'@timestamp': step.startTime,
|
||||
...environmentMetadata,
|
||||
|
@ -130,9 +164,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
|
@ -147,27 +179,22 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
title: step.titlePath().join(' '),
|
||||
category: step.category,
|
||||
},
|
||||
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
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) {
|
||||
onStepEnd(test: TestCase, _: TestResult, step: TestStep) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
|
@ -183,6 +210,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
category: step.category,
|
||||
duration: step.duration,
|
||||
},
|
||||
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_STEP_END,
|
||||
|
@ -191,10 +219,6 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
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)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -205,9 +229,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
suite: {
|
||||
title: test.parent.titlePath().join(' '),
|
||||
type: test.parent.type,
|
||||
|
@ -220,6 +242,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
expected_status: test.expectedStatus,
|
||||
status: result.status,
|
||||
duration: result.duration,
|
||||
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.location.file)),
|
||||
},
|
||||
event: {
|
||||
action: ScoutReportEventAction.TEST_END,
|
||||
|
@ -228,14 +251,10 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
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) {
|
||||
async onEnd(result: FullResult) {
|
||||
this.report.logEvent({
|
||||
...environmentMetadata,
|
||||
reporter: {
|
||||
|
@ -243,7 +262,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
...this.baseTestRunInfo,
|
||||
status: result.status,
|
||||
duration: result.duration,
|
||||
},
|
||||
|
@ -252,9 +271,13 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
},
|
||||
});
|
||||
|
||||
// Save & conclude the report
|
||||
// Save, upload events & conclude the report
|
||||
try {
|
||||
this.report.save(this.reportRootPath);
|
||||
await uploadScoutReportEvents(this.report.eventLogPath, this.log);
|
||||
} catch (e) {
|
||||
// Log the error but don't propagate it
|
||||
this.log.error(e);
|
||||
} finally {
|
||||
this.report.conclude();
|
||||
}
|
||||
|
@ -271,9 +294,7 @@ export class ScoutPlaywrightReporter implements Reporter {
|
|||
name: this.name,
|
||||
type: 'playwright',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
event: {
|
||||
action: ScoutReportEventAction.ERROR,
|
||||
error: {
|
||||
|
|
|
@ -45,11 +45,23 @@ export interface ScoutReporterInfo {
|
|||
type: 'jest' | 'ftr' | 'playwright';
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout file info
|
||||
*/
|
||||
export interface ScoutFileInfo {
|
||||
path: string;
|
||||
owner: string | string[];
|
||||
area: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout test run info
|
||||
*/
|
||||
export interface ScoutTestRunInfo {
|
||||
id: string;
|
||||
config?: {
|
||||
file?: ScoutFileInfo;
|
||||
};
|
||||
status?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
@ -81,14 +93,7 @@ export interface ScoutTestInfo {
|
|||
category?: string;
|
||||
duration?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scout file info
|
||||
*/
|
||||
export interface ScoutFileInfo {
|
||||
path: string;
|
||||
owner: string | string[];
|
||||
file?: ScoutFileInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,7 +104,6 @@ export interface ScoutReportEvent {
|
|||
buildkite?: BuildkiteMetadata;
|
||||
host?: HostMetadata;
|
||||
event: ScoutReportEventInfo;
|
||||
file?: ScoutFileInfo;
|
||||
labels?: { [id: string]: string };
|
||||
reporter: ScoutReporterInfo;
|
||||
test_run: ScoutTestRunInfo;
|
||||
|
|
|
@ -48,7 +48,7 @@ export const reporterMappings: ClusterPutComponentTemplateRequest = {
|
|||
|
||||
export const testRunMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.test-run',
|
||||
version: 1,
|
||||
version: 2,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
|
@ -78,7 +78,7 @@ export const suiteMappings: ClusterPutComponentTemplateRequest = {
|
|||
|
||||
export const testMappings: ClusterPutComponentTemplateRequest = {
|
||||
name: 'scout-test-event.mappings.test',
|
||||
version: 1,
|
||||
version: 2,
|
||||
template: {
|
||||
mappings: {
|
||||
properties: {
|
||||
|
|
|
@ -12,7 +12,13 @@ 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 {
|
||||
SCOUT_REPORTER_ES_API_KEY,
|
||||
SCOUT_REPORTER_ES_URL,
|
||||
SCOUT_REPORTER_ES_VERIFY_CERTS,
|
||||
SCOUT_TEST_EVENTS_DATA_STREAM_NAME,
|
||||
} from '@kbn/scout-info';
|
||||
import { getValidatedESClient } from '../../../../helpers/elasticsearch';
|
||||
import { ScoutReportEvent } from '../event';
|
||||
import * as componentTemplates from './component_templates';
|
||||
import * as indexTemplates from './index_templates';
|
||||
|
@ -141,3 +147,41 @@ export class ScoutReportDataStream {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload events logged by a Scout reporter to the configured Scout Reporter ES instance
|
||||
*
|
||||
* @param eventLogPath Path to event log file
|
||||
* @param log Logger instance
|
||||
*/
|
||||
export async function uploadScoutReportEvents(eventLogPath: string, log?: ToolingLog) {
|
||||
const logger = log || new ToolingLog();
|
||||
|
||||
const warnSettingWasNotConfigured = (settingName: string) =>
|
||||
logger.warning(`Won't upload Scout reporter events: ${settingName} was not configured`);
|
||||
|
||||
if (SCOUT_REPORTER_ES_URL === undefined) {
|
||||
warnSettingWasNotConfigured('SCOUT_REPORTER_ES_URL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (SCOUT_REPORTER_ES_API_KEY === undefined) {
|
||||
warnSettingWasNotConfigured('SCOUT_REPORTER_ES_API_KEY');
|
||||
return;
|
||||
}
|
||||
|
||||
log?.info(`Connecting to Scout reporter ES URL ${SCOUT_REPORTER_ES_URL}`);
|
||||
const es = await getValidatedESClient(
|
||||
{
|
||||
node: SCOUT_REPORTER_ES_URL,
|
||||
auth: { apiKey: SCOUT_REPORTER_ES_API_KEY },
|
||||
tls: {
|
||||
rejectUnauthorized: SCOUT_REPORTER_ES_VERIFY_CERTS,
|
||||
},
|
||||
},
|
||||
{ log }
|
||||
);
|
||||
|
||||
const reportDataStream = new ScoutReportDataStream(es, logger);
|
||||
await reportDataStream.addEventsFromFile(eventLogPath);
|
||||
}
|
||||
|
|
|
@ -95,6 +95,18 @@ export const buildkiteProperties: Record<PropertyName, MappingProperty> = {
|
|||
},
|
||||
};
|
||||
|
||||
export const fileInfoProperties: Record<PropertyName, MappingProperty> = {
|
||||
path: {
|
||||
type: 'keyword',
|
||||
},
|
||||
owner: {
|
||||
type: 'keyword',
|
||||
},
|
||||
area: {
|
||||
type: 'keyword',
|
||||
},
|
||||
};
|
||||
|
||||
export const reporterProperties: Record<PropertyName, MappingProperty> = {
|
||||
name: {
|
||||
type: 'text',
|
||||
|
@ -114,6 +126,15 @@ export const testRunProperties: Record<PropertyName, MappingProperty> = {
|
|||
duration: {
|
||||
type: 'long',
|
||||
},
|
||||
config: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: {
|
||||
type: 'object',
|
||||
properties: fileInfoProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const suiteProperties: Record<PropertyName, MappingProperty> = {
|
||||
|
@ -169,4 +190,8 @@ export const testProperties: Record<PropertyName, MappingProperty> = {
|
|||
},
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
properties: fileInfoProperties,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { ScoutEventsReport, ScoutReportEventAction } from './events';
|
||||
export * from './events';
|
||||
export { ScoutFailureReport, type TestFailure } from './failed_test';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SCOUT_SERVERS_ROOT } from '@kbn/scout-info';
|
||||
import { SCOUT_REPORTER_ENABLED, SCOUT_SERVERS_ROOT } from '@kbn/scout-info';
|
||||
import { createPlaywrightConfig } from './create_config';
|
||||
import { VALID_CONFIG_MARKER } from '../types';
|
||||
import { generateTestRunId } from '@kbn/scout-reporting';
|
||||
|
@ -48,10 +48,12 @@ describe('createPlaywrightConfig', () => {
|
|||
expect(config.reporter).toEqual([
|
||||
['html', { open: 'never', outputFolder: './output/reports' }],
|
||||
['json', { outputFile: './output/reports/test-results.json' }],
|
||||
[
|
||||
'@kbn/scout-reporting/src/reporting/playwright/events',
|
||||
{ name: 'scout-playwright', runId: testRunId },
|
||||
],
|
||||
SCOUT_REPORTER_ENABLED
|
||||
? [
|
||||
'@kbn/scout-reporting/src/reporting/playwright/events',
|
||||
{ name: 'scout-playwright', runId: testRunId },
|
||||
]
|
||||
: ['null'],
|
||||
[
|
||||
'@kbn/scout-reporting/src/reporting/playwright/failed_test',
|
||||
{ name: 'scout-playwright-failed-tests', runId: testRunId },
|
||||
|
|
|
@ -11,6 +11,7 @@ import { dirname, resolve } from 'path';
|
|||
|
||||
import Joi from 'joi';
|
||||
import type { CustomHelpers } from 'joi';
|
||||
import { SCOUT_REPORTER_ENABLED } from '@kbn/scout-info';
|
||||
|
||||
// valid pattern for ID
|
||||
// enforced camel-case identifiers for consistency
|
||||
|
@ -180,7 +181,7 @@ export const schema = Joi.object()
|
|||
|
||||
scoutReporter: Joi.object()
|
||||
.keys({
|
||||
enabled: Joi.boolean().default(process.env.ENABLE_SCOUT_REPORTER || false),
|
||||
enabled: Joi.boolean().default(SCOUT_REPORTER_ENABLED),
|
||||
})
|
||||
.default(),
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ export function MochaReporterProvider({ getService }) {
|
|||
}
|
||||
|
||||
if (config.get('scoutReporter.enabled')) {
|
||||
new ScoutFTRReporter(runner);
|
||||
new ScoutFTRReporter(runner, config);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,18 +12,24 @@ 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,
|
||||
datasources,
|
||||
ScoutEventsReport,
|
||||
ScoutReportEventAction,
|
||||
type ScoutTestRunInfo,
|
||||
generateTestRunId,
|
||||
getTestIDForTitle,
|
||||
uploadScoutReportEvents,
|
||||
ScoutFileInfo,
|
||||
} from '@kbn/scout-reporting';
|
||||
import {
|
||||
type CodeOwnersEntry,
|
||||
type CodeOwnerArea,
|
||||
getOwningTeamsForPath,
|
||||
getCodeOwnersEntries,
|
||||
type CodeOwnersEntry,
|
||||
findAreaForCodeOwner,
|
||||
} from '@kbn/code-owners';
|
||||
import { Runner, Test } from '../../../fake_mocha_types';
|
||||
import { Config as FTRConfig } from '../../config';
|
||||
|
||||
/**
|
||||
* Configuration options for the Scout Mocha reporter
|
||||
|
@ -41,9 +47,14 @@ export class ScoutFTRReporter {
|
|||
readonly name: string;
|
||||
readonly runId: string;
|
||||
private report: ScoutEventsReport;
|
||||
private readonly baseTestRunInfo: ScoutTestRunInfo;
|
||||
private readonly codeOwnersEntries: CodeOwnersEntry[];
|
||||
|
||||
constructor(private runner: Runner, private reporterOptions: ScoutFTRReporterOptions = {}) {
|
||||
constructor(
|
||||
private runner: Runner,
|
||||
config: FTRConfig,
|
||||
private reporterOptions: ScoutFTRReporterOptions = {}
|
||||
) {
|
||||
this.log = new ToolingLog({
|
||||
level: 'info',
|
||||
writeTo: process.stdout,
|
||||
|
@ -55,6 +66,10 @@ export class ScoutFTRReporter {
|
|||
|
||||
this.report = new ScoutEventsReport(this.log);
|
||||
this.codeOwnersEntries = getCodeOwnersEntries();
|
||||
this.baseTestRunInfo = {
|
||||
id: this.runId,
|
||||
config: { file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, config.path)) },
|
||||
};
|
||||
|
||||
// Register event listeners
|
||||
for (const [eventName, listener] of Object.entries({
|
||||
|
@ -71,6 +86,22 @@ export class ScoutFTRReporter {
|
|||
return getOwningTeamsForPath(filePath, this.codeOwnersEntries);
|
||||
}
|
||||
|
||||
private getOwnerAreas(owners: string[]): CodeOwnerArea[] {
|
||||
return owners
|
||||
.map((owner) => findAreaForCodeOwner(owner))
|
||||
.filter((area) => area !== undefined) as CodeOwnerArea[];
|
||||
}
|
||||
|
||||
private getScoutFileInfoForPath(filePath: string): ScoutFileInfo {
|
||||
const fileOwners = this.getFileOwners(filePath);
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
owner: fileOwners,
|
||||
area: this.getOwnerAreas(fileOwners),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Root path of this reporter's output
|
||||
*/
|
||||
|
@ -89,9 +120,7 @@ export class ScoutFTRReporter {
|
|||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
event: {
|
||||
action: ScoutReportEventAction.RUN_BEGIN,
|
||||
},
|
||||
|
@ -108,9 +137,7 @@ export class ScoutFTRReporter {
|
|||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
suite: {
|
||||
title: test.parent?.fullTitle() || 'unknown',
|
||||
type: test.parent?.root ? 'root' : 'suite',
|
||||
|
@ -119,14 +146,13 @@ export class ScoutFTRReporter {
|
|||
id: getTestIDForTitle(test.fullTitle()),
|
||||
title: test.title,
|
||||
tags: [],
|
||||
file: test.file
|
||||
? this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.file))
|
||||
: undefined,
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -140,9 +166,7 @@ export class ScoutFTRReporter {
|
|||
name: this.name,
|
||||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
},
|
||||
test_run: this.baseTestRunInfo,
|
||||
suite: {
|
||||
title: test.parent?.fullTitle() || 'unknown',
|
||||
type: test.parent?.root ? 'root' : 'suite',
|
||||
|
@ -151,6 +175,9 @@ export class ScoutFTRReporter {
|
|||
id: getTestIDForTitle(test.fullTitle()),
|
||||
title: test.title,
|
||||
tags: [],
|
||||
file: test.file
|
||||
? this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.file))
|
||||
: undefined,
|
||||
status: test.isPending() ? 'skipped' : test.isPassed() ? 'passed' : 'failed',
|
||||
duration: test.duration,
|
||||
},
|
||||
|
@ -161,14 +188,10 @@ export class ScoutFTRReporter {
|
|||
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 = () => {
|
||||
onRunEnd = async () => {
|
||||
/**
|
||||
* Root suite execution has ended
|
||||
*/
|
||||
|
@ -179,7 +202,7 @@ export class ScoutFTRReporter {
|
|||
type: 'ftr',
|
||||
},
|
||||
test_run: {
|
||||
id: this.runId,
|
||||
...this.baseTestRunInfo,
|
||||
status: this.runner.stats?.failures === 0 ? 'passed' : 'failed',
|
||||
duration: this.runner.stats?.duration || 0,
|
||||
},
|
||||
|
@ -191,6 +214,10 @@ export class ScoutFTRReporter {
|
|||
// Save & conclude the report
|
||||
try {
|
||||
this.report.save(this.reportRootPath);
|
||||
await uploadScoutReportEvents(this.report.eventLogPath, this.log);
|
||||
} catch (e) {
|
||||
// Log the error but don't propagate it
|
||||
this.log.error(e);
|
||||
} finally {
|
||||
this.report.conclude();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue