Skip to content

Commit

Permalink
added a context-based matching algorithm (jest-community#593)
Browse files Browse the repository at this point in the history
  • Loading branch information
connectdotz authored Jun 30, 2020
1 parent 06c24af commit 5adeea4
Show file tree
Hide file tree
Showing 13 changed files with 983 additions and 444 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Bug-fixes within the same version aren't needed
* upgrade all dependencies to the latest, except istanbul-lib-xxx, which requires more code change and will be handled in a separate coverage PR. - @connectdotz
* code base clean up: migrate from tslint to eslint and adopted the latest recommended coding style, adopt semi-colon, added more ci check... - @connectdotz
* resolve coverage map merge issue, upgrade istanbul dependencies to the latest and move to async coverageMap update. @connectdotz
* introducing a new matching algorithm to use context relationship in matching test blocks and assertions - @connectdotz
-->

### 3.2.0
Expand Down
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: 'tests/.*\\.ts$',
testRegex: 'tests/.*\\.test\\.ts$',
collectCoverageFrom: ['src/**/*.ts'],
automock: true,
moduleFileExtensions: ['ts', 'js', 'json'],
unmockedModulePathPatterns: [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.0",
"jest-editor-support": "^27.2.0",
"jest-editor-support": "^28.0.0-beta",
"jest-snapshot": "^25.5.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/JestExt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class JestExt {
this.pluginSettings = updatedSettings;

this.jestWorkspace.rootPath = updatedSettings.rootPath;
this.jestWorkspace.pathToJest = pathToJest(updatedSettings);
this.jestWorkspace.jestCommandLine = pathToJest(updatedSettings);
this.jestWorkspace.pathToConfig = pathToConfig(updatedSettings);

// debug
Expand Down
17 changes: 11 additions & 6 deletions src/TestResults/TestResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import { FileCoverage } from 'istanbul-lib-coverage';
import * as path from 'path';
import { cleanAnsi } from '../helpers';

interface Position {
export interface Location {
/** Zero-based column number */
column: number;

/** Zero-based line number */
line: number;
}

export interface TestResult {
export interface LocationRange {
start: Location;
end: Location;
}

export interface TestResult extends LocationRange {
name: string;
start: Position;
end: Position;

status: TestReconciliationState;
shortMessage?: string;
Expand Down Expand Up @@ -88,7 +91,9 @@ export const coverageMapWithLowerCaseWindowsDriveLetters = (data: JestTotalResul
*
* @param data Parsed JSON results
*/
export const resultsWithLowerCaseWindowsDriveLetters = (data: JestTotalResults) => {
export const resultsWithLowerCaseWindowsDriveLetters = (
data: JestTotalResults
): JestTotalResults => {
if (path.sep === '\\') {
return {
...data,
Expand All @@ -103,7 +108,7 @@ export const resultsWithLowerCaseWindowsDriveLetters = (data: JestTotalResults)
/**
* Removes ANSI escape sequence characters from test results in order to get clean messages
*/
export const resultsWithoutAnsiEscapeSequence = (data: JestTotalResults) => {
export const resultsWithoutAnsiEscapeSequence = (data: JestTotalResults): JestTotalResults => {
if (!data || !data.testResults) {
return data;
}
Expand Down
153 changes: 22 additions & 131 deletions src/TestResults/TestResultProvider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import {
TestReconciler,
JestTotalResults,
TestAssertionStatus,
TestFileAssertionStatus,
ItBlock,
} from 'jest-editor-support';
import { TestReconciler, JestTotalResults, TestFileAssertionStatus } from 'jest-editor-support';
import { TestReconciliationState } from './TestReconciliationState';
import { TestResult } from './TestResult';
import { parseTest } from '../TestParser';
import * as match from './match-by-context';

interface TestResultsMap {
[filePath: string]: TestResult[];
Expand All @@ -24,9 +19,6 @@ interface SortedTestResultsMap {
[filePath: string]: SortedTestResults;
}

type IsMatched = (test: ItBlock, assertion: TestAssertionStatus) => boolean;
type OnMatchError = (test: ItBlock, matched: TestAssertionStatus[]) => string | undefined;

export class TestResultProvider {
verbose: boolean;
private reconciler: TestReconciler;
Expand All @@ -39,141 +31,40 @@ export class TestResultProvider {
this.verbose = verbose;
}

resetCache() {
resetCache(): void {
this.resultsByFilePath = {};
this.sortedResultsByFilePath = {};
}

getResults(filePath: string): TestResult[] {
const toMatchResult = (test: ItBlock, assertion?: TestAssertionStatus, err?: string) => ({
// Note the shift from one-based to zero-based line number and columns
name: test.name,
start: {
column: test.start.column - 1,
line: test.start.line - 1,
},
end: {
column: test.end.column - 1,
line: test.end.line - 1,
},

status: assertion ? assertion.status : TestReconciliationState.Unknown,
shortMessage: assertion ? assertion.shortMessage : err,
terseMessage: assertion ? assertion.terseMessage : undefined,
lineNumberOfError:
assertion &&
assertion.line &&
assertion.line >= test.start.line &&
assertion.line <= test.end.line
? assertion.line - 1
: test.end.line - 1,
});

const matchTests = (
_itBlocks: ItBlock[],
_assertions: TestAssertionStatus[],
_isMatched: IsMatched[],
_onMatchError?: OnMatchError,
trackRemaining?: boolean
): [TestResult[], ItBlock[], TestAssertionStatus[]] => {
const results: TestResult[] = [];
const remainingAssertions = Array.from(_assertions);
const remainingTests: ItBlock[] = [];
const _trackRemaining = trackRemaining === undefined ? true : trackRemaining;

_itBlocks.forEach((test) => {
const matched = remainingAssertions.filter((a) => _isMatched.every((m) => m(test, a)));
if (matched.length === 1) {
const aIndex = remainingAssertions.indexOf(matched[0]);
if (aIndex < 0) {
throw new Error(`can't find assertion in the list`);
}
results.push(toMatchResult(test, matched[0]));
if (_trackRemaining) {
remainingAssertions.splice(aIndex, 1);
}
return;
}

let err: string;
if (_onMatchError) {
err = _onMatchError(test, matched);
}
// if there is an error string, create a test result with it
if (err) {
results.push(toMatchResult(test, undefined, err));
return;
}

if (_trackRemaining) {
remainingTests.push(test);
}
});
return [results, remainingTests, remainingAssertions];
};

if (this.resultsByFilePath[filePath]) {
return this.resultsByFilePath[filePath];
}
const matchPos = (t: ItBlock, a: TestAssertionStatus): boolean =>
(a.line !== undefined && a.line >= t.start.line && a.line <= t.end.line) ||
(a.location && a.location.line >= t.start.line && a.location.line <= t.end.line);

const matchName = (t: ItBlock, a: TestAssertionStatus): boolean => t.name === a.title;
const templateLiteralPattern = /\${.*?}/; // template literal pattern
const matchTemplateLiteral = (t: ItBlock, a: TestAssertionStatus): boolean => {
if (!t.name.match(templateLiteralPattern)) {
return false;
}
const parts = t.name.split(templateLiteralPattern);
const r = parts.every((p) => a.title.includes(p));
return r;
};
const onMatchError: OnMatchError = (t: ItBlock, match: TestAssertionStatus[]) => {
let err: string;
if (match.length <= 0 && t.name.match(templateLiteralPattern)) {
err = 'no test result found, could be caused by template literals?';
}
if (match.length > 1) {
err =
'found multiple potential matches, could be caused by duplicate test names or template literals?';
}
if (err && this.verbose) {
// tslint:disable-next-line: no-console
console.log(`'${t.name}' failed to find test result: ${err}`);
}
return err;
};

let { itBlocks } = parseTest(filePath);
let assertions = this.reconciler.assertionsForTestFile(filePath) || [];
const totalResult: TestResult[] = [];
let matchResult: TestResult[] = [];

if (assertions.length > 0 && itBlocks.length > 0) {
const algorithms: Array<[IsMatched[], OnMatchError]> = [
[[matchName, matchPos], undefined],
[[matchTemplateLiteral, matchPos], undefined],
[[matchTemplateLiteral], undefined],
[[matchName], onMatchError],
];
for (const [matchers, onError] of algorithms) {
let result: TestResult[];
[result, itBlocks, assertions] = matchTests(itBlocks, assertions, matchers, onError);
totalResult.push(...result);
if (itBlocks.length <= 0 || assertions.length <= 0) {
break;
try {
const assertions = this.reconciler.assertionsForTestFile(filePath);
if (!assertions) {
if (this.verbose) {
console.log(`no assertion found, perhaps not a test file? '${filePath}'`);
}
} else if (assertions.length <= 0) {
// no assertion, all tests are unknown
const { itBlocks } = parseTest(filePath);
matchResult = itBlocks.map((t) => match.toMatchResult(t, 'no assertion found'));
} else {
const { root } = parseTest(filePath);
matchResult = match.matchTestAssertions(filePath, root, assertions, this.verbose);
}
} catch (e) {
console.warn(`failed to get test result for ${filePath}:`, e);
}

// convert remaining itBlocks to unmatched result
itBlocks.forEach((t) => totalResult.push(toMatchResult(t)));

this.resultsByFilePath[filePath] = totalResult;
return totalResult;
this.resultsByFilePath[filePath] = matchResult;
return matchResult;
}

getSortedResults(filePath: string) {
getSortedResults(filePath: string): SortedTestResults {
if (this.sortedResultsByFilePath[filePath]) {
return this.sortedResultsByFilePath[filePath];
}
Expand Down Expand Up @@ -207,7 +98,7 @@ export class TestResultProvider {
return this.reconciler.updateFileWithJestStatus(data);
}

removeCachedResults(filePath: string) {
removeCachedResults(filePath: string): void {
this.resultsByFilePath[filePath] = null;
this.sortedResultsByFilePath[filePath] = null;
}
Expand Down
Loading

0 comments on commit 5adeea4

Please sign in to comment.