Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2f924e9
[scout] Add Jest events reporter
dolaru Mar 14, 2025
5bb1713
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Mar 14, 2025
70bea6a
Plug custom reporter into base Jest presets
dolaru Mar 14, 2025
6a2d87a
Merge branch 'main' into scout-jest-reporter
dolaru Mar 17, 2025
1ac5c35
Pass additional Scout reporter info via Jest globals
dolaru Mar 18, 2025
58e4e1f
JS-friendly reporter loading in Jest presets
dolaru Mar 18, 2025
5a5f3a2
Merge branch 'main' into scout-jest-reporter
dolaru Mar 18, 2025
cf010fe
Scout metadata in Jest globals for some AppEx QA packages
dolaru Mar 18, 2025
8ebed2f
Handle situations where a test context is not defined
dolaru Mar 19, 2025
399c5ea
Test run config file path relative to repo root
dolaru Mar 19, 2025
3ac169d
Other way around 🙈
dolaru Mar 19, 2025
3ba5e5a
Merge branch 'main' into scout-jest-reporter
dolaru Mar 19, 2025
618b6fe
Merge branch 'main' into scout-jest-reporter
csr Mar 24, 2025
decdff1
Merge branch 'main' into scout-jest-reporter
csr Mar 26, 2025
d1c46d4
Merge branch 'main' into scout-jest-reporter
csr Mar 28, 2025
547b6ed
Merge branch 'main' into scout-jest-reporter
dolaru Apr 1, 2025
a0b5673
Merge branch 'main' into scout-jest-reporter
dolaru Apr 2, 2025
314abb1
Remove config file path value from Jest configs
dolaru Apr 2, 2025
aaf8ebd
Pass default config category through reporter options at preset level
dolaru Apr 2, 2025
c2787af
Load Jest config path info from environment variables
dolaru Apr 2, 2025
78f6a9e
Strip ANSI codes from failure messages
dolaru Apr 2, 2025
c29fddc
Merge branch 'main' into scout-jest-reporter
dolaru Apr 2, 2025
8b9d473
Abandon mechanism of passing data via Jest globals 🧹🗑️
dolaru Apr 2, 2025
bddc2cc
Merge branch 'main' into scout-jest-reporter
dolaru Apr 3, 2025
fae0431
Move Jest config path collection from constructor to `onRunStart`
dolaru Apr 3, 2025
4660bf3
Merge branch 'main' into scout-jest-reporter
dolaru Apr 3, 2025
4667776
Fix `JEST_CONFIG_PATH` definition
dolaru Apr 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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".
*/

require('@kbn/babel-register').install();
module.exports = require('./reporter').ScoutJestReporter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { ScoutTestRunConfigCategory } from '@kbn/scout-info';

/**
* Configuration options for the Scout Jest reporter
*/
export interface ScoutJestReporterOptions {
name?: string;
Comment thread
dolaru marked this conversation as resolved.
runId?: string;
configCategory?: ScoutTestRunConfigCategory;
outputPath?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* 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 { Config, AggregatedResult, TestContext, ReporterOnStartOptions } from '@jest/reporters';
import { BaseReporter } from '@jest/reporters';
import { TestResult } from '@jest/types';
import { ToolingLog } from '@kbn/tooling-log';
import {
type CodeOwnerArea,
CodeOwnersEntry,
findAreaForCodeOwner,
getCodeOwnersEntries,
getOwningTeamsForPath,
} from '@kbn/code-owners';
import { SCOUT_REPORT_OUTPUT_ROOT } from '@kbn/scout-info';
import path from 'node:path';
import { REPO_ROOT } from '@kbn/repo-info';
import stripAnsi from 'strip-ansi';
import { ScoutJestReporterOptions } from './options';
import {
datasources,
generateTestRunId,
getTestIDForTitle,
ScoutEventsReport,
ScoutFileInfo,
ScoutReportEventAction,
type ScoutTestRunInfo,
uploadScoutReportEvents,
} from '../../..';

/**
* Scout Jest reporter
*/
export class ScoutJestReporter extends BaseReporter {
Comment thread
dolaru marked this conversation as resolved.
name: string;
readonly scoutLog: ToolingLog;
readonly runId: string;
private report: ScoutEventsReport;
private baseTestRunInfo: ScoutTestRunInfo;
private readonly codeOwnersEntries: CodeOwnersEntry[];

constructor(
_jestGlobalConfig: Config.GlobalConfig,
private reporterOptions: ScoutJestReporterOptions
) {
super();
this.scoutLog = new ToolingLog({
level: 'info',
writeTo: process.stdout,
});

this.name = this.reporterOptions.name || 'unknown';
this.runId = this.reporterOptions.runId || generateTestRunId();
this.scoutLog.info(`Scout test run ID: ${this.runId}`);

this.report = new ScoutEventsReport(this.scoutLog);
this.baseTestRunInfo = {
id: this.runId,
config: {
category: reporterOptions.configCategory,
},
};

this.codeOwnersEntries = getCodeOwnersEntries();
}

private getFileOwners(filePath: string): string[] {
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
*/
public get reportRootPath(): string {
const outputPath = this.reporterOptions.outputPath || SCOUT_REPORT_OUTPUT_ROOT;
return path.join(outputPath, `scout-jest-${this.runId}`);
}

/**
* Separate the error message from the stack trace in a Jest failure message
Comment thread
dolaru marked this conversation as resolved.
*
* If the message doesn't contain a stack trace, it'll return unmodified.
*
* @param message Jest failure message
*/
parseJestFailureMessage(message: string) {
const match = message.match(/(?<message>^.+?)(\r\n|\r|\n)(?=.+?at)\s(?<stack_trace>.+$)/s);

return match === null
? { message }
: (match.groups as { message: string; stack_trace?: string });
}

/**
* Log a Jest test result as a Scout reporter event
*
* @param test Jest test information
* @param test.result Jest test result
* @param test.filePath Jest test file path
*
*/
logTestResult(test: { result: TestResult.AssertionResult; filePath: string }): void {
const suiteTitle = test.result.ancestorTitles.join(' ');
const parsedErrorMessages: string[] = [];
const parsedStackTraces: string[] = [];

test.result.failureMessages
.map((message) => this.parseJestFailureMessage(stripAnsi(message)))
.forEach((parsed) => {
if (parsed.message) {
parsedErrorMessages.push(parsed.message);
}
if (parsed.stack_trace) {
parsedStackTraces.push(parsed.stack_trace);
}
});

this.report.logEvent({
...datasources.environmentMetadata,
reporter: {
name: this.name,
type: 'jest',
},
test_run: this.baseTestRunInfo,
suite: {
title: suiteTitle || 'unknown',
type: test.result.ancestorTitles.length <= 1 ? 'root' : 'suite',
},
test: {
id: getTestIDForTitle(test.result.fullName),
title: test.result.title,
tags: [],
file: this.getScoutFileInfoForPath(path.relative(REPO_ROOT, test.filePath)),
status: test.result.status === 'pending' ? 'skipped' : test.result.status,
duration: test.result.duration || 0,
},
event: {
action: ScoutReportEventAction.TEST_END,
error: {
message:
parsedErrorMessages.length > 0
? parsedErrorMessages.join('\n--- NEXT ERROR ---\n')
: undefined,
stack_trace:
parsedStackTraces.length > 0
? parsedStackTraces.join('\n--- NEXT STACK TRACE ---\n')
: undefined,
},
},
});
}

onRunStart(results: AggregatedResult, _options?: ReporterOnStartOptions): void {
/**
* Test execution started
*/
// Look for Jest config path in environment variables
// Must do it here rather than the constructor as the reporter object is created when the Jest config is evaluated
// and the JEST_CONFIG_PATH environment variable might not be set
this.baseTestRunInfo = {
...this.baseTestRunInfo,
config: {
...this.baseTestRunInfo.config,
file:
process.env.JEST_CONFIG_PATH !== undefined
? this.getScoutFileInfoForPath(path.relative(REPO_ROOT, process.env.JEST_CONFIG_PATH))
: undefined,
},
};

// Log "run start" event
this.report.logEvent({
...datasources.environmentMetadata,
'@timestamp': new Date(results.startTime),
reporter: {
name: this.name,
type: 'jest',
},
test_run: this.baseTestRunInfo,
event: {
action: ScoutReportEventAction.RUN_BEGIN,
},
});
}

Comment thread
csr marked this conversation as resolved.
async onRunComplete(_testContexts: Set<TestContext>, results: AggregatedResult): Promise<void> {
/**
* Test execution ended
*/
// Turn test results into events in bulk
results.testResults.forEach((suite) => {
suite.testResults.forEach((testResult) => {
this.logTestResult({ result: testResult, filePath: suite.testFilePath });
});
});

// Log "run end" event
this.report.logEvent({
...datasources.environmentMetadata,
reporter: {
name: this.name,
type: 'jest',
},
test_run: {
...this.baseTestRunInfo,
status: results.numFailedTests === 0 ? 'passed' : 'failed',
duration: Date.now() - results.startTime || 0,
},
event: {
action: ScoutReportEventAction.RUN_END,
},
});

// Save & conclude the report
try {
this.report.save(this.reportRootPath);
await uploadScoutReportEvents(this.report.eventLogPath, this.scoutLog);
} catch (e) {
// Log the error but don't propagate it
this.scoutLog.error(e);
Comment thread
csr marked this conversation as resolved.
} finally {
this.report.conclude();
}
}
}
8 changes: 8 additions & 0 deletions src/platform/packages/shared/kbn-test/jest-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ module.exports = {
],
]
: []),
...(['1', 'yes', 'true'].includes(process.env.SCOUT_REPORTER_ENABLED)
? [
[
'<rootDir>/src/platform/packages/private/kbn-scout-reporting/src/reporting/jest',
{ name: 'Jest tests (unit)', configCategory: 'unit-test' },
],
]
: []),
],

// The paths to modules that run some code to configure or set up the testing environment before each test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ module.exports = {
],
]
: []),
...(['1', 'yes', 'true'].includes(process.env.SCOUT_REPORTER_ENABLED)
? [
[
'<rootDir>/src/platform/packages/private/kbn-scout-reporting/src/reporting/jest',
{ name: 'Jest tests (integration)', configCategory: 'unit-integration-test' },
],
]
: []),
],
coverageReporters: !!process.env.CI
? [['json', { file: 'jest-integration.json' }]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ module.exports = {
testGroupType: 'Jest Integration Tests',
},
],
...(['1', 'yes', 'true'].includes(process.env.SCOUT_REPORTER_ENABLED)
? [
[
'<rootDir>/src/platform/packages/private/kbn-scout-reporting/src/reporting/jest',
{ name: 'Jest tests (integration, node)', configCategory: 'unit-integration-test' },
],
]
: []),
],
coverageReporters: !!process.env.CI
? [['json', { file: 'jest-integration.json' }]]
Expand Down
11 changes: 11 additions & 0 deletions src/platform/packages/shared/kbn-test/src/jest/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { createFailError } from '@kbn/dev-cli-errors';
import { REPO_ROOT } from '@kbn/repo-info';
import { map } from 'lodash';
import getopts from 'getopts';
import { SCOUT_REPORTER_ENABLED } from '@kbn/scout-info';
import jestFlags from './jest_flags.json';

// yarn test:jest src/core/server/saved_objects
Expand Down Expand Up @@ -144,12 +145,22 @@ export function runJest(configName = 'jest.config.js') {
}

log.info('yarn jest', process.argv.slice(2).join(' '));

if (SCOUT_REPORTER_ENABLED) {
// Expose Jest config file path via environment variables
process.env.JEST_CONFIG_PATH = configPath;
}
}

if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = 'test';
}

if (SCOUT_REPORTER_ENABLED && argv.config) {
// Expose Jest config file path via environment variables
process.env.JEST_CONFIG_PATH = argv.config;
}

run().then(() => {
// Success means that tests finished, doesn't mean they passed.
reportTime(runStartTime, 'total', {
Expand Down