diff --git a/lib/cli.js b/lib/cli.js index b5d3c3497..e0e28d6fc 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -39,7 +39,7 @@ exports.run = async () => { // eslint-disable-line complexity Options --watch, -w Re-run tests when tests and source files change - --match, -m Only run tests with matching title (Can be repeated) + --match, -m Only run tests with matching title/hash (Can be repeated) --update-snapshots, -u Update snapshots --fail-fast Stop after first test failure --timeout, -T Set global timeout (milliseconds or human-readable, e.g. 10s, 2m) diff --git a/lib/reporters/colors.js b/lib/reporters/colors.js index a03dbaead..994cbaa8f 100644 --- a/lib/reporters/colors.js +++ b/lib/reporters/colors.js @@ -12,5 +12,6 @@ module.exports = { errorSource: chalk.gray, errorStack: chalk.gray, stack: chalk.red, + hash: chalk.dim, information: chalk.magenta }; diff --git a/lib/reporters/verbose.js b/lib/reporters/verbose.js index 9f80b0a9e..6f79df75d 100644 --- a/lib/reporters/verbose.js +++ b/lib/reporters/verbose.js @@ -125,16 +125,16 @@ class VerboseReporter { break; case 'hook-finished': if (evt.logs.length > 0) { - this.lineWriter.writeLine(` ${this.prefixTitle(evt.testFile, evt.title)}`); + this.lineWriter.writeLine(` ${evt.hash} ${this.prefixTitle(evt.testFile, evt.title)}`); this.writeLogs(evt); } break; case 'selected-test': if (evt.skip) { - this.lineWriter.writeLine(colors.skip(`- ${this.prefixTitle(evt.testFile, evt.title)}`)); + this.lineWriter.writeLine(colors.skip(`- ${evt.hash} ${this.prefixTitle(evt.testFile, evt.title)}`)); } else if (evt.todo) { - this.lineWriter.writeLine(colors.todo(`- ${this.prefixTitle(evt.testFile, evt.title)}`)); + this.lineWriter.writeLine(colors.todo(`- ${evt.hash} ${this.prefixTitle(evt.testFile, evt.title)}`)); } break; @@ -293,15 +293,15 @@ class VerboseReporter { writeTestSummary(evt) { if (evt.type === 'hook-failed' || evt.type === 'test-failed') { - this.lineWriter.writeLine(`${colors.error(figures.cross)} ${this.prefixTitle(evt.testFile, evt.title)} ${colors.error(evt.err.message)}`); + this.lineWriter.writeLine(`${colors.error(figures.cross)} ${colors.hash(evt.hash)} ${this.prefixTitle(evt.testFile, evt.title)} ${colors.error(evt.err.message)}`); } else if (evt.knownFailing) { - this.lineWriter.writeLine(`${colors.error(figures.tick)} ${colors.error(this.prefixTitle(evt.testFile, evt.title))}`); + this.lineWriter.writeLine(`${colors.error(figures.tick)} ${colors.hash(evt.hash)} ${colors.error(this.prefixTitle(evt.testFile, evt.title))}`); } else { // Display duration only over a threshold const threshold = 100; const duration = evt.duration > threshold ? colors.duration(' (' + prettyMs(evt.duration) + ')') : ''; - this.lineWriter.writeLine(`${colors.pass(figures.tick)} ${this.prefixTitle(evt.testFile, evt.title)}${duration}`); + this.lineWriter.writeLine(`${colors.pass(figures.tick)} ${colors.hash(evt.hash)} ${this.prefixTitle(evt.testFile, evt.title)}${duration}`); } this.writeLogs(evt); diff --git a/lib/runner.js b/lib/runner.js index 18af02f7a..b42f234bf 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,6 +1,7 @@ 'use strict'; const Emittery = require('emittery'); const matcher = require('matcher'); +const objectHash = require('object-hash'); const ContextRef = require('./context-ref'); const createChain = require('./create-chain'); const snapshotManager = require('./snapshot-manager'); @@ -79,9 +80,13 @@ class Runner extends Emittery { uniqueTestTitles.add(specifiedTitle); } + const specifiedHash = this.getTitleHash(specifiedTitle); + if (this.match.length > 0) { // --match selects TODO tests. - if (matcher([specifiedTitle], this.match).length === 1) { + const titleMatched = matcher([specifiedTitle], this.match).length === 1; + const hashMatched = matcher([specifiedHash], this.match).length === 1; + if (titleMatched || hashMatched) { metadata.exclusive = true; this.runOnlyExclusive = true; } @@ -90,6 +95,7 @@ class Runner extends Emittery { this.tasks.todo.push({title: specifiedTitle, metadata}); this.emit('stateChange', { type: 'declared-test', + hash: specifiedHash, title: specifiedTitle, knownFailing: false, todo: true @@ -126,7 +132,10 @@ class Runner extends Emittery { } } + const hash = this.getTitleHash(title); + const task = { + hash, title, implementation, args, @@ -136,7 +145,9 @@ class Runner extends Emittery { if (metadata.type === 'test') { if (this.match.length > 0) { // --match overrides .only() - task.metadata.exclusive = matcher([title], this.match).length === 1; + const titleMatched = matcher([title], this.match).length === 1; + const hashMatched = matcher([hash], this.match).length === 1; + task.metadata.exclusive = titleMatched || hashMatched; } if (task.metadata.exclusive) { @@ -146,6 +157,7 @@ class Runner extends Emittery { this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task); this.emit('stateChange', { type: 'declared-test', + hash, title, knownFailing: metadata.failing, todo: false @@ -166,6 +178,10 @@ class Runner extends Emittery { }, meta); } + getTitleHash(title) { + return title ? objectHash(title).substring(0, 10) : undefined; + } + compareTestSnapshot(options) { if (!this.snapshots) { this.snapshots = snapshotManager.load({ @@ -276,6 +292,7 @@ class Runner extends Emittery { compareTestSnapshot: this.boundCompareTestSnapshot, updateSnapshots: this.updateSnapshots, metadata: task.metadata, + hash: this.getTitleHash(`${task.title}${titleSuffix || ''}`), title: `${task.title}${titleSuffix || ''}` })); const outcome = await this.runMultiple(hooks, this.serial); @@ -283,6 +300,7 @@ class Runner extends Emittery { if (result.passed) { this.emit('stateChange', { type: 'hook-finished', + hash: result.hash, title: result.title, duration: result.duration, logs: result.logs @@ -290,6 +308,7 @@ class Runner extends Emittery { } else { this.emit('stateChange', { type: 'hook-failed', + hash: result.hash, title: result.title, err: serializeError('Hook failure', true, result.error), duration: result.duration, @@ -316,6 +335,7 @@ class Runner extends Emittery { compareTestSnapshot: this.boundCompareTestSnapshot, updateSnapshots: this.updateSnapshots, metadata: task.metadata, + hash: task.hash, title: task.title }); @@ -323,6 +343,7 @@ class Runner extends Emittery { if (result.passed) { this.emit('stateChange', { type: 'test-passed', + hash: result.hash, title: result.title, duration: result.duration, knownFailing: result.metadata.failing, @@ -332,6 +353,7 @@ class Runner extends Emittery { } else { this.emit('stateChange', { type: 'test-failed', + hash: result.hash, title: result.title, err: serializeError('Test failure', true, result.error), duration: result.duration, @@ -356,6 +378,7 @@ class Runner extends Emittery { this.emit('stateChange', { type: 'selected-test', + hash: task.hash, title: task.title, knownFailing: task.metadata.failing, skip: task.metadata.skipped, @@ -374,6 +397,7 @@ class Runner extends Emittery { this.emit('stateChange', { type: 'selected-test', + hash: task.hash, title: task.title, knownFailing: task.metadata.failing, skip: task.metadata.skipped, @@ -396,6 +420,7 @@ class Runner extends Emittery { this.emit('stateChange', { type: 'selected-test', + hash: task.hash, title: task.title, knownFailing: false, skip: false, diff --git a/lib/test.js b/lib/test.js index 10b277ec8..a0cc5929b 100644 --- a/lib/test.js +++ b/lib/test.js @@ -102,6 +102,7 @@ class Test { this.failWithoutAssertions = options.failWithoutAssertions; this.fn = options.fn; this.metadata = options.metadata; + this.hash = options.hash; this.title = options.title; this.logs = []; @@ -481,6 +482,7 @@ class Test { logs: this.logs, metadata: this.metadata, passed, + hash: this.hash, title: this.title }; } diff --git a/package-lock.json b/package-lock.json index d5b03fcb8..910d1ccc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6283,6 +6283,12 @@ } } }, + "object-hash": { + "version": "1.3.1", + "resolved": "http://sinopia.samsungmtv.com/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", diff --git a/package.json b/package.json index 3dd582f64..6746efa5a 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "git-branch": "^2.0.1", "has-ansi": "^3.0.0", "lolex": "^4.1.0", + "object-hash": "^1.3.1", "proxyquire": "^2.1.0", "react": "^16.8.6", "react-test-renderer": "^16.8.6", diff --git a/test/api.js b/test/api.js index b26984fce..5337d4077 100644 --- a/test/api.js +++ b/test/api.js @@ -94,11 +94,13 @@ test('fail-fast mode - single file & serial', t => { if (evt.type === 'test-failed') { tests.push({ ok: false, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'test-passed') { tests.push({ ok: true, + hash: evt.hash, title: evt.title }); } @@ -137,12 +139,14 @@ test('fail-fast mode - multiple files & serial', t => { tests.push({ ok: false, testFile: evt.testFile, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'test-passed') { tests.push({ ok: true, testFile: evt.testFile, + hash: evt.hash, title: evt.title }); } @@ -183,12 +187,14 @@ test('fail-fast mode - multiple files & interrupt', t => { tests.push({ ok: false, testFile: evt.testFile, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'test-passed') { tests.push({ ok: true, testFile: evt.testFile, + hash: evt.hash, title: evt.title }); } @@ -237,11 +243,13 @@ test('fail-fast mode - crash & serial', t => { if (evt.type === 'test-failed') { tests.push({ ok: false, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'test-passed') { tests.push({ ok: true, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'worker-failed') { @@ -279,11 +287,13 @@ test('fail-fast mode - timeout & serial', t => { if (evt.type === 'test-failed') { tests.push({ ok: false, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'test-passed') { tests.push({ ok: true, + hash: evt.hash, title: evt.title }); } else if (evt.type === 'timeout') {