-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[scout] Add Jest events reporter #214662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[scout] Add Jest events reporter #214662
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 5bb1713
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine 70bea6a
Plug custom reporter into base Jest presets
dolaru 6a2d87a
Merge branch 'main' into scout-jest-reporter
dolaru 1ac5c35
Pass additional Scout reporter info via Jest globals
dolaru 58e4e1f
JS-friendly reporter loading in Jest presets
dolaru 5a5f3a2
Merge branch 'main' into scout-jest-reporter
dolaru cf010fe
Scout metadata in Jest globals for some AppEx QA packages
dolaru 8ebed2f
Handle situations where a test context is not defined
dolaru 399c5ea
Test run config file path relative to repo root
dolaru 3ac169d
Other way around 🙈
dolaru 3ba5e5a
Merge branch 'main' into scout-jest-reporter
dolaru 618b6fe
Merge branch 'main' into scout-jest-reporter
csr decdff1
Merge branch 'main' into scout-jest-reporter
csr d1c46d4
Merge branch 'main' into scout-jest-reporter
csr 547b6ed
Merge branch 'main' into scout-jest-reporter
dolaru a0b5673
Merge branch 'main' into scout-jest-reporter
dolaru 314abb1
Remove config file path value from Jest configs
dolaru aaf8ebd
Pass default config category through reporter options at preset level
dolaru c2787af
Load Jest config path info from environment variables
dolaru 78f6a9e
Strip ANSI codes from failure messages
dolaru c29fddc
Merge branch 'main' into scout-jest-reporter
dolaru 8b9d473
Abandon mechanism of passing data via Jest globals 🧹🗑️
dolaru bddc2cc
Merge branch 'main' into scout-jest-reporter
dolaru fae0431
Move Jest config path collection from constructor to `onRunStart`
dolaru 4660bf3
Merge branch 'main' into scout-jest-reporter
dolaru 4667776
Fix `JEST_CONFIG_PATH` definition
dolaru File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
src/platform/packages/private/kbn-scout-reporting/src/reporting/jest/index.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
20 changes: 20 additions & 0 deletions
20
src/platform/packages/private/kbn-scout-reporting/src/reporting/jest/options.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| runId?: string; | ||
| configCategory?: ScoutTestRunConfigCategory; | ||
| outputPath?: string; | ||
| } | ||
247 changes: 247 additions & 0 deletions
247
src/platform/packages/private/kbn-scout-reporting/src/reporting/jest/reporter.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
|
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 | ||
|
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, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
|
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); | ||
|
csr marked this conversation as resolved.
|
||
| } finally { | ||
| this.report.conclude(); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.