Skip to content
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

refactor(@jest/reporters): improve annotation formatting of GitHubActionsReporter #12826

Merged
merged 22 commits into from May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[@jest/reporters]` Improve `GitHubActionsReporter`s annotation format ([#12826](https://github.com/facebook/jest/pull/12826))

### Fixes

### Chore & Maintenance
Expand All @@ -12,7 +14,7 @@

### Features

- `[jest-circus]` Add `failing` test modifier that inverts the behaviour of tests ([#12610](https://github.com/facebook/jest/pull/12610))
- `[jest-circus]` Add `failing` test modifier that inverts the behavior of tests ([#12610](https://github.com/facebook/jest/pull/12610))
- `[jest-environment-node, jest-environment-jsdom]` Allow specifying `customExportConditions` ([#12774](https://github.com/facebook/jest/pull/12774))

### Fixes
Expand Down
10 changes: 5 additions & 5 deletions packages/jest-message-util/src/index.ts
Expand Up @@ -234,11 +234,11 @@ const removeInternalStackEntries = (
});
};

const formatPaths = (
config: StackTraceConfig,
relativeTestPath: string | null,
export const formatPath = (
line: string,
) => {
config: StackTraceConfig,
relativeTestPath: string | null = null,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps fine to have null as default, it just enables more styling.

): string => {
// Extract the file path from the trace line.
const match = line.match(/(^\s*at .*?\(?)([^()]+)(:[0-9]+:[0-9]+\)?.*$)/);
if (!match) {
Expand Down Expand Up @@ -317,7 +317,7 @@ export const formatStackTrace = (
.filter(Boolean)
.map(
line =>
STACK_INDENT + formatPaths(config, relativeTestPath, trimPaths(line)),
STACK_INDENT + formatPath(trimPaths(line), config, relativeTestPath),
)
.join('\n');

Expand Down
1 change: 1 addition & 0 deletions packages/jest-reporters/package.json
Expand Up @@ -29,6 +29,7 @@
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.0",
"istanbul-reports": "^3.1.3",
"jest-message-util": "^28.1.0",
"jest-util": "^28.1.0",
"jest-worker": "^28.1.0",
"slash": "^3.0.0",
Expand Down
105 changes: 65 additions & 40 deletions packages/jest-reporters/src/GitHubActionsReporter.ts
Expand Up @@ -6,54 +6,79 @@
*/

import stripAnsi = require('strip-ansi');
import type {
AggregatedResult,
TestContext,
TestResult,
} from '@jest/test-result';
import type {Test, TestResult} from '@jest/test-result';
import type {Config} from '@jest/types';
import {
formatPath,
getStackTraceLines,
getTopFrame,
separateMessageFromStack,
} from 'jest-message-util';
import BaseReporter from './BaseReporter';

const lineAndColumnInStackTrace = /^.*?:([0-9]+):([0-9]+).*$/;
type AnnotationOptions = {
file?: string;
line?: number | string;
message: string;
title: string;
type: 'error' | 'warning';
};

function replaceEntities(s: string): string {
// https://github.com/actions/toolkit/blob/b4639928698a6bfe1c4bdae4b2bfdad1cb75016d/packages/core/src/command.ts#L80-L85
const substitutions: Array<[RegExp, string]> = [
[/%/g, '%25'],
[/\r/g, '%0D'],
[/\n/g, '%0A'],
];
return substitutions.reduce((acc, sub) => acc.replace(...sub), s);
}
const titleSeparator = ' \u203A ';

export default class GitHubActionsReporter extends BaseReporter {
static readonly filename = __filename;

override onRunComplete(
_testContexts?: Set<TestContext>,
aggregatedResults?: AggregatedResult,
): void {
const messages = getMessages(aggregatedResults?.testResults);
onTestFileResult({context}: Test, {testResults}: TestResult): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onTestFileResult is needed to annotate retries. I was trying to use onTestCaseResult first, but it reports retried tests as failures. (By the way, strangely TestContext from onTestCaseResult has wrong rootDir, but all is good with onTestFileResult.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, strangely TestContext from onTestCaseResult has wrong rootDir, but all is good with onTestFileResult.)

can you file an issue for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have to figure out what is the root of that. Locally all was fine, but not on CI. Strange thing. Let’s see.

testResults.forEach(result => {
const title = [...result.ancestorTitles, result.title].join(
titleSeparator,
);

result.retryReasons?.forEach((retryReason, index) => {
this.#createAnnotation({
...this.#getMessageDetails(retryReason, context.config),
title: `RETRY ${index + 1}: ${title}`,
type: 'warning',
});
});

for (const message of messages) {
this.log(message);
}
result.failureMessages.forEach(failureMessage => {
this.#createAnnotation({
...this.#getMessageDetails(failureMessage, context.config),
title,
type: 'error',
});
});
});
}
}

function getMessages(results: Array<TestResult> | undefined) {
if (!results) return [];

return results.flatMap(({testFilePath, testResults}) =>
testResults
.filter(r => r.status === 'failed')
.flatMap(r => r.failureMessages)
.map(m => stripAnsi(m))
.map(m => replaceEntities(m))
.map(m => lineAndColumnInStackTrace.exec(m))
.filter((m): m is RegExpExecArray => m !== null)
.map(
([message, line, col]) =>
`\n::error file=${testFilePath},line=${line},col=${col}::${message}`,
),
);
#getMessageDetails(failureMessage: string, config: Config.ProjectConfig) {
const {message, stack} = separateMessageFromStack(failureMessage);

const stackLines = getStackTraceLines(stack);
const topFrame = getTopFrame(stackLines);

const normalizedStackLines = stackLines.map(line =>
formatPath(line, config),
);
const messageText = [message, ...normalizedStackLines].join('\n');

return {
file: topFrame?.file,
line: topFrame?.line,
message: messageText,
};
}

#createAnnotation({file, line, message, title, type}: AnnotationOptions) {
message = stripAnsi(
// copied from: https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts
message.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'),
);

this.log(
`\n::${type} file=${file},line=${line},title=${title}::${message}`,
);
}
}
118 changes: 0 additions & 118 deletions packages/jest-reporters/src/__tests__/GitHubActionsReporter.test.js

This file was deleted.