12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577 |
- /* eslint-disable promise/no-callback-in-promise */
- 'use strict';
- const path = require('path');
- const EventEmitter = require('events');
- const PassThrough = require('stream').PassThrough;
- const defaultIgnore = require('ignore-by-default').directories();
- const lolex = require('lolex');
- const proxyquire = require('proxyquire');
- const sinon = require('sinon');
- const test = require('tap').test;
- const AvaFiles = require('../lib/ava-files');
- const setImmediate = require('../lib/now-and-timers').setImmediate;
- // Helper to make using beforeEach less arduous
- function makeGroup(test) {
- return (desc, fn) => {
- test(desc, t => {
- const beforeEach = fn => {
- t.beforeEach(done => {
- fn();
- done();
- });
- };
- const pending = [];
- const test = (name, fn) => {
- pending.push(t.test(name, fn));
- };
- fn(beforeEach, test, makeGroup(test));
- return Promise.all(pending);
- });
- };
- }
- const group = makeGroup(test);
- group('chokidar', (beforeEach, test, group) => {
- let chokidar;
- let debug;
- let reporter;
- let api;
- let avaFiles;
- let Subject;
- let runStatus;
- let resetRunStatus;
- let clock;
- let chokidarEmitter;
- let stdin;
- let files;
- let defaultApiOptions;
- function proxyWatcher(opts) {
- return proxyquire.noCallThru().load('../lib/watcher', opts ||
- {
- chokidar,
- debug(name) {
- return function () {
- const args = [name];
- args.push.apply(args, arguments);
- debug.apply(null, args);
- };
- },
- './ava-files': avaFiles
- });
- }
- beforeEach(() => {
- chokidar = {
- watch: sinon.stub()
- };
- debug = sinon.spy();
- reporter = {
- endRun: sinon.spy()
- };
- api = {
- on() {},
- run: sinon.stub()
- };
- resetRunStatus = () => {
- runStatus = {
- stats: {
- byFile: new Map(),
- declaredTests: 0,
- failedHooks: 0,
- failedTests: 0,
- failedWorkers: 0,
- files,
- finishedWorkers: 0,
- internalErrors: 0,
- remainingTests: 0,
- passedKnownFailingTests: 0,
- passedTests: 0,
- selectedTests: 0,
- skippedTests: 0,
- timeouts: 0,
- todoTests: 0,
- uncaughtExceptions: 0,
- unhandledRejections: 0
- }
- };
- return runStatus;
- };
- if (clock) {
- clock.uninstall();
- }
- clock = lolex.install({
- toFake: [
- 'setImmediate',
- 'setTimeout',
- 'clearTimeout'
- ]
- });
- chokidarEmitter = new EventEmitter();
- chokidar.watch.returns(chokidarEmitter);
- avaFiles = AvaFiles;
- api.run.returns(new Promise(() => {}));
- files = [
- 'test.js',
- 'test-*.js',
- 'test'
- ];
- defaultApiOptions = {
- clearLogOnNextRun: false,
- previousFailures: 0,
- runOnlyExclusive: false,
- runVector: 1,
- updateSnapshots: false
- };
- resetRunStatus();
- stdin = new PassThrough();
- stdin.pause();
- Subject = proxyWatcher();
- });
- const start = (specificFiles, sources) => new Subject(reporter, api, specificFiles || files, sources || []);
- const emitChokidar = (event, path) => {
- chokidarEmitter.emit('all', event, path);
- };
- const add = path => {
- emitChokidar('add', path || 'source.js');
- };
- const change = path => {
- emitChokidar('change', path || 'source.js');
- };
- const unlink = path => {
- emitChokidar('unlink', path || 'source.js');
- };
- const delay = () => new Promise(resolve => {
- setImmediate(resolve);
- });
- // Advance the clock to get past the debounce timeout, then wait for a promise
- // to be resolved to get past the `busy.then()` delay
- const debounce = times => {
- times = times >= 0 ? times : 1;
- clock.next();
- return delay().then(() => {
- if (times > 1) {
- return debounce(times - 1);
- }
- });
- };
- test('watches for default source file changes, as well as test files', t => {
- t.plan(2);
- start();
- t.ok(chokidar.watch.calledOnce);
- t.strictDeepEqual(chokidar.watch.firstCall.args, [
- ['package.json', '**/*.js', '**/*.snap'].concat(files),
- {
- ignored: defaultIgnore.map(dir => `${dir}/**/*`),
- ignoreInitial: true
- }
- ]);
- });
- test('watched source files are configurable', t => {
- t.plan(2);
- start(null, ['foo.js', '!bar.js', 'baz.js', '!qux.js']);
- t.ok(chokidar.watch.calledOnce);
- t.strictDeepEqual(chokidar.watch.firstCall.args, [
- ['foo.js', 'baz.js'].concat(files),
- {
- ignored: defaultIgnore.map(dir => `${dir}/**/*`).concat('bar.js', 'qux.js'),
- ignoreInitial: true
- }
- ]);
- });
- test('configured sources can override default ignore patterns', t => {
- t.plan(2);
- start(null, ['node_modules/foo/*.js']);
- t.ok(chokidar.watch.calledOnce);
- t.strictDeepEqual(chokidar.watch.firstCall.args, [
- ['node_modules/foo/*.js'].concat(files),
- {
- ignored: defaultIgnore.map(dir => `${dir}/**/*`).concat('!node_modules/foo/*.js'),
- ignoreInitial: true
- }
- ]);
- });
- test('starts running the initial tests', t => {
- t.plan(4);
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- start();
- t.ok(api.run.calledOnce);
- t.strictDeepEqual(api.run.firstCall.args, [files, defaultApiOptions]);
- // The endRun method is only called after the run promise fulfils
- t.ok(reporter.endRun.notCalled);
- done();
- return delay().then(() => {
- t.ok(reporter.endRun.calledOnce);
- });
- });
- [
- {
- label: 'is added',
- fire: add,
- event: 'add'
- },
- {
- label: 'changes',
- fire: change,
- event: 'change'
- },
- {
- label: 'is removed',
- fire: unlink,
- event: 'unlink'
- }
- ].forEach(variant => {
- test(`logs a debug message when a file is ${variant.label}`, t => {
- t.plan(2);
- start();
- variant.fire('file.js');
- t.ok(debug.calledOnce);
- t.strictDeepEqual(debug.firstCall.args, ['ava:watcher', 'Detected %s of %s', variant.event, 'file.js']);
- });
- });
- [
- {
- label: 'is added',
- fire: add
- },
- {
- label: 'changes',
- fire: change
- },
- {
- label: 'is removed',
- fire: unlink
- }
- ].forEach(variant => {
- test(`reruns initial tests when a source file ${variant.label}`, t => {
- t.plan(4);
- api.run.returns(Promise.resolve(runStatus));
- start();
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- variant.fire();
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- // No explicit files are provided
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- // Finish is only called after the run promise fulfils
- t.ok(reporter.endRun.calledOnce);
- resetRunStatus();
- done();
- return delay();
- }).then(() => {
- t.ok(reporter.endRun.calledTwice);
- });
- });
- });
- [
- {
- label: 'failures',
- prop: 'failedTests'
- },
- {
- label: 'rejections',
- prop: 'unhandledRejections'
- },
- {
- label: 'exceptions',
- prop: 'uncaughtExceptions'
- }
- ].forEach(variant => {
- test(`does not clear log if the previous run had ${variant.label}`, t => {
- t.plan(2);
- runStatus.stats[variant.prop] = 1;
- api.run.returns(Promise.resolve(runStatus));
- start();
- api.run.returns(Promise.resolve(resetRunStatus()));
- change();
- return debounce().then(() => {
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: false,
- runVector: 2
- })]);
- change();
- return debounce();
- }).then(() => {
- t.strictDeepEqual(api.run.thirdCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 3
- })]);
- });
- });
- });
- test('debounces by 100ms', t => {
- t.plan(1);
- api.run.returns(Promise.resolve(runStatus));
- start();
- change();
- const before = clock.now;
- return debounce().then(() => {
- t.is(clock.now - before, 100);
- });
- });
- test('debounces again if changes occur in the interval', t => {
- t.plan(4);
- api.run.returns(Promise.resolve(runStatus));
- start();
- change();
- change();
- const before = clock.now;
- return debounce().then(() => {
- change();
- return debounce();
- }).then(() => {
- t.is(clock.now - before, 150);
- change();
- return debounce();
- }).then(() => {
- t.is(clock.now - before, 175);
- change();
- return debounce();
- }).then(() => {
- t.is(clock.now - before, 187);
- change();
- return debounce();
- }).then(() => {
- t.is(clock.now - before, 197);
- });
- });
- test('only reruns tests once the initial run has finished', t => {
- t.plan(2);
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- start();
- change();
- clock.next();
- return delay().then(() => {
- t.ok(api.run.calledOnce);
- done();
- return delay();
- }).then(() => {
- t.ok(api.run.calledTwice);
- });
- });
- test('only reruns tests once the previous run has finished', t => {
- t.plan(3);
- api.run.returns(Promise.resolve(runStatus));
- start();
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- change();
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- change();
- clock.next();
- return delay();
- }).then(() => {
- t.ok(api.run.calledTwice);
- done();
- return delay();
- }).then(() => {
- t.ok(api.run.calledThrice);
- });
- });
- [
- {
- label: 'is added',
- fire: add
- },
- {
- label: 'changes',
- fire: change
- }
- ].forEach(variant => {
- test(`(re)runs a test file when it ${variant.label}`, t => {
- t.plan(4);
- api.run.returns(Promise.resolve(runStatus));
- start();
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- variant.fire('test.js');
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- // The `test.js` file is provided
- t.strictDeepEqual(api.run.secondCall.args, [['test.js'], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- // The endRun method is only called after the run promise fulfills
- t.ok(reporter.endRun.calledOnce);
- resetRunStatus();
- done();
- return delay();
- }).then(() => {
- t.ok(reporter.endRun.calledTwice);
- });
- });
- });
- test('(re)runs several test files when they are added or changed', t => {
- t.plan(2);
- api.run.returns(Promise.resolve(runStatus));
- start();
- add('test-one.js');
- change('test-two.js');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- // The test files are provided
- t.strictDeepEqual(api.run.secondCall.args, [['test-one.js', 'test-two.js'], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('reruns initial tests if both source and test files are added or changed', t => {
- t.plan(2);
- api.run.returns(Promise.resolve(runStatus));
- start();
- add('test.js');
- unlink('source.js');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- // No explicit files are provided
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('does nothing if tests are deleted', t => {
- t.plan(1);
- api.run.returns(Promise.resolve(runStatus));
- start();
- unlink('test.js');
- return debounce().then(() => {
- t.ok(api.run.calledOnce);
- });
- });
- test('determines whether changed files are tests based on the initial files patterns', t => {
- t.plan(2);
- files = ['foo-{bar,baz}.js'];
- api.run.returns(Promise.resolve(runStatus));
- start();
- add('foo-bar.js');
- add('foo-baz.js');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [['foo-bar.js', 'foo-baz.js'], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('initial exclude patterns override whether something is a test file', t => {
- t.plan(2);
- avaFiles = function (options) {
- const ret = new AvaFiles(options);
- // Note: There is no way for users to actually set exclude patterns yet.
- // This test just validates that internal updates to the default excludes pattern will be obeyed.
- ret.excludePatterns = ['!*bar*'];
- return ret;
- };
- Subject = proxyWatcher();
- files = ['foo-{bar,baz}.js'];
- api.run.returns(Promise.resolve(runStatus));
- start();
- add('foo-bar.js');
- add('foo-baz.js');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- // `foo-bar.js` is excluded from being a test file, thus the initial tests
- // are run
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('test files must end in .js', t => {
- t.plan(2);
- files = ['foo.bar'];
- api.run.returns(Promise.resolve(runStatus));
- start();
- add('foo.bar');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- // `foo.bar` cannot be a test file, thus the initial tests are run
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('test files must not start with an underscore', t => {
- t.plan(2);
- api.files = ['_foo.bar'];
- api.run.returns(Promise.resolve(runStatus));
- start();
- add('_foo.bar');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- // `_foo.bar` cannot be a test file, thus the initial tests are run
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('files patterns may match directories', t => {
- t.plan(2);
- files = ['dir', 'another-dir/*/deeper'];
- api.run.returns(Promise.resolve(runStatus));
- start();
- add(path.join('dir', 'test.js'));
- add(path.join('dir', 'nested', 'test.js'));
- add(path.join('another-dir', 'nested', 'deeper', 'test.js'));
- return debounce(3).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [
- [
- path.join('dir', 'test.js'),
- path.join('dir', 'nested', 'test.js'),
- path.join('another-dir', 'nested', 'deeper', 'test.js')
- ],
- Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })
- ]);
- });
- });
- test('exclude patterns override directory matches', t => {
- t.plan(2);
- avaFiles = function (options) {
- const ret = new AvaFiles(options);
- // Note: There is no way for users to actually set exclude patterns yet.
- // This test just validates that internal updates to the default excludes pattern will be obeyed.
- ret.excludePatterns = ['!**/exclude/**'];
- return ret;
- };
- Subject = proxyWatcher();
- files = ['dir'];
- api.run.returns(Promise.resolve(runStatus));
- start();
- add(path.join('dir', 'exclude', 'foo.js'));
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- // `dir/exclude/foo.js` is excluded from being a test file, thus the initial
- // tests are run
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- ['r', 'rs'].forEach(input => {
- test(`reruns initial tests when "${input}" is entered on stdin`, t => {
- t.plan(4);
- api.run.returns(Promise.resolve(runStatus));
- start().observeStdin(stdin);
- stdin.write(`${input}\n`);
- return delay().then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- runVector: 2
- })]);
- stdin.write(`\t${input} \n`);
- return delay();
- }).then(() => {
- t.ok(api.run.calledThrice);
- t.strictDeepEqual(api.run.thirdCall.args, [files, Object.assign({}, defaultApiOptions, {
- runVector: 3
- })]);
- });
- });
- });
- test(`reruns previous tests and update snapshots when "u" is entered on stdin`, t => {
- const options = Object.assign({}, defaultApiOptions, {updateSnapshots: true});
- const previousFiles = ['test.js'];
- t.plan(4);
- api.run.returns(Promise.resolve(runStatus));
- start(previousFiles).observeStdin(stdin);
- stdin.write(`u\n`);
- return delay().then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [previousFiles, Object.assign({}, options, {
- runVector: 2
- })]);
- stdin.write(`\tu \n`);
- return delay();
- }).then(() => {
- t.ok(api.run.calledThrice);
- t.strictDeepEqual(api.run.thirdCall.args, [previousFiles, Object.assign({}, options, {
- runVector: 3
- })]);
- });
- });
- ['r', 'rs', 'u'].forEach(input => {
- test(`entering "${input}" on stdin prevents the log from being cleared`, t => {
- t.plan(2);
- api.run.returns(Promise.resolve(runStatus));
- start().observeStdin(stdin);
- stdin.write(`${input}\n`);
- return delay().then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: false,
- runVector: 2,
- updateSnapshots: input === 'u'
- })]);
- });
- });
- test(`entering "${input}" on stdin cancels any debouncing`, t => {
- t.plan(7);
- api.run.returns(Promise.resolve(runStatus));
- start().observeStdin(stdin);
- let before = clock.now;
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- add();
- stdin.write(`${input}\n`);
- return delay().then(() => {
- // Processing "rs" caused a new run
- t.ok(api.run.calledTwice);
- // Try to advance the clock. This is *after* input was processed. The
- // debounce timeout should have been canceled, so the clock can't have
- // advanced.
- clock.next();
- t.is(before, clock.now);
- add();
- // Advance clock *before* input is received. Note that the previous run
- // hasn't finished yet.
- clock.next();
- stdin.write(`${input}\n`);
- return delay();
- }).then(() => {
- // No new runs yet
- t.ok(api.run.calledTwice);
- // Though the clock has advanced
- t.is(clock.now - before, 100);
- before = clock.now;
- const previous = done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- // Finish the previous run
- previous();
- return delay();
- }).then(() => {
- // There's only one new run
- t.ok(api.run.calledThrice);
- stdin.write(`${input}\n`);
- return delay();
- }).then(() => {
- add();
- // Finish the previous run. This should cause a new run due to the
- // input.
- done();
- return delay();
- }).then(() => {
- // Again there's only one new run
- t.is(api.run.callCount, 4);
- // Try to advance the clock. This is *after* input was processed. The
- // debounce timeout should have been canceled, so the clock can't have
- // advanced.
- clock.next();
- t.is(before, clock.now);
- });
- });
- });
- test('does nothing if anything other than "rs" is entered on stdin', t => {
- t.plan(1);
- api.run.returns(Promise.resolve(runStatus));
- start().observeStdin(stdin);
- stdin.write('foo\n');
- return debounce().then(() => {
- t.ok(api.run.calledOnce);
- });
- });
- test('ignores unexpected events from chokidar', t => {
- t.plan(1);
- api.run.returns(Promise.resolve(runStatus));
- start();
- emitChokidar('foo');
- return debounce().then(() => {
- t.ok(api.run.calledOnce);
- });
- });
- test('initial run rejects', t => {
- t.plan(1);
- const expected = new Error();
- api.run.returns(Promise.reject(expected));
- start();
- return delay().then(() => {
- // The error is rethrown asynchronously, using setImmediate. The clock has
- // faked setTimeout, so if we call clock.next() it'll invoke and rethrow
- // the error, which can then be caught here.
- try {
- clock.next();
- } catch (err) {
- t.is(err, expected);
- }
- });
- });
- test('subsequent run rejects', t => {
- t.plan(1);
- api.run.returns(Promise.resolve(runStatus));
- start();
- const expected = new Error();
- api.run.returns(Promise.reject(expected));
- add();
- return debounce().then(() => {
- // The error is rethrown asynchronously, using setImmediate. The clock has
- // faked setTimeout, so if we call clock.next() it'll invoke and rethrow
- // the error, which can then be caught here.
- try {
- clock.next();
- } catch (err) {
- t.is(err, expected);
- }
- });
- });
- group('tracks test dependencies', (beforeEach, test) => {
- let apiEmitter;
- let runStatus;
- let runStatusEmitter;
- beforeEach(() => {
- apiEmitter = new EventEmitter();
- api.on = (event, fn) => {
- apiEmitter.on(event, fn);
- };
- runStatusEmitter = new EventEmitter();
- runStatus = {
- stats: {
- byFile: new Map(),
- declaredTests: 0,
- failedHooks: 0,
- failedTests: 0,
- failedWorkers: 0,
- files,
- finishedWorkers: 0,
- internalErrors: 0,
- remainingTests: 0,
- passedKnownFailingTests: 0,
- passedTests: 0,
- selectedTests: 0,
- skippedTests: 0,
- timeouts: 0,
- todoTests: 0,
- uncaughtExceptions: 0,
- unhandledRejections: 0
- },
- on(event, fn) {
- runStatusEmitter.on(event, fn);
- }
- };
- });
- const emitDependencies = (testFile, dependencies) => {
- runStatusEmitter.emit('stateChange', {type: 'dependencies', testFile, dependencies});
- };
- const seed = sources => {
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- const watcher = start(null, sources);
- const files = [path.join('test', '1.js'), path.join('test', '2.js')];
- const absFiles = files.map(relFile => path.resolve(relFile));
- apiEmitter.emit('run', {
- files: absFiles,
- status: runStatus
- });
- emitDependencies(files[0], [path.resolve('dep-1.js'), path.resolve('dep-3.js')]);
- emitDependencies(files[1], [path.resolve('dep-2.js'), path.resolve('dep-3.js')]);
- done();
- api.run.returns(new Promise(() => {}));
- return watcher;
- };
- test('runs specific tests that depend on changed sources', t => {
- t.plan(2);
- seed();
- change('dep-1.js');
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[path.join('test', '1.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('reruns all tests if a source cannot be mapped to a particular test', t => {
- t.plan(2);
- seed();
- change('cannot-be-mapped.js');
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('runs changed tests and tests that depend on changed sources', t => {
- t.plan(2);
- seed();
- change('dep-1.js');
- change(path.join('test', '2.js'));
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [
- [path.join('test', '2.js'), path.join('test', '1.js')],
- Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })
- ]);
- });
- });
- test('avoids duplication when both a test and a source dependency change', t => {
- t.plan(2);
- seed();
- change(path.join('test', '1.js'));
- change('dep-1.js');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[path.join('test', '1.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('stops tracking unlinked tests', t => {
- t.plan(2);
- seed();
- unlink(path.join('test', '1.js'));
- change('dep-3.js');
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[path.join('test', '2.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('updates test dependencies', t => {
- t.plan(2);
- seed();
- emitDependencies(path.join('test', '1.js'), [path.resolve('dep-4.js')]);
- change('dep-4.js');
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[path.join('test', '1.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- [
- {
- desc: 'only tracks source dependencies',
- sources: ['dep-1.js']
- },
- {
- desc: 'exclusion patterns affect tracked source dependencies',
- sources: ['!dep-2.js']
- }
- ].forEach(variant => {
- test(variant.desc, t => {
- t.plan(2);
- seed(variant.sources);
- // `dep-2.js` isn't treated as a source and therefore it's not tracked as
- // a dependency for `test/2.js`. Pretend Chokidar detected a change to
- // verify (normally Chokidar would also be ignoring this file but hey).
- change('dep-2.js');
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- // Expect all tests to be rerun since `dep-2.js` is not a tracked
- // dependency
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- });
- test('uses default source patterns', t => {
- t.plan(4);
- seed();
- emitDependencies(path.join('test', '1.js'), [path.resolve('package.json'), path.resolve('index.js'), path.resolve('lib/util.js')]);
- emitDependencies(path.join('test', '2.js'), [path.resolve('foo.bar')]);
- change('package.json');
- change('index.js');
- change(path.join('lib', 'util.js'));
- api.run.returns(Promise.resolve(runStatus));
- return debounce(3).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[path.join('test', '1.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- change('foo.bar');
- return debounce();
- }).then(() => {
- t.ok(api.run.calledThrice);
- // Expect all tests to be rerun since `foo.bar` is not a tracked
- // dependency
- t.strictDeepEqual(api.run.thirdCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 3
- })]);
- });
- });
- test('uses default exclusion patterns', t => {
- t.plan(2);
- // Ensure each directory is treated as containing sources
- seed(['**/*']);
- // Synthesize an excluded file for each directory that's ignored by
- // default. Apply deeper nesting for each file.
- const excludedFiles = defaultIgnore.map((dir, index) => {
- let relPath = dir;
- for (let i = index; i >= 0; i--) {
- relPath = path.join(relPath, String(i));
- }
- return `${relPath}.js`;
- });
- // Ensure `test/1.js` also depends on the excluded files
- emitDependencies(
- path.join('test', '1.js'),
- excludedFiles.map(relPath => path.resolve(relPath)).concat('dep-1.js')
- );
- // Modify all excluded files
- excludedFiles.forEach(x => change(x));
- return debounce(excludedFiles.length).then(() => {
- t.ok(api.run.calledTwice);
- // Since the excluded files are not tracked as a dependency, all tests
- // are expected to be rerun
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('allows default exclusion patterns to be overriden', t => {
- t.plan(2);
- seed(['node_modules/foo/*.js']);
- const dep = path.join('node_modules', 'foo', 'index.js');
- emitDependencies(path.join('test', '1.js'), [path.resolve(dep)]);
- change(dep);
- return debounce(1).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[path.join('test', '1.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('ignores dependencies outside of the current working directory', t => {
- t.plan(4);
- seed(['**/*.js', '..foo.js']);
- emitDependencies(path.join('test', '1.js'), [path.resolve('../outside.js')]);
- emitDependencies(path.join('test', '2.js'), [path.resolve('..foo.js')]);
- // Pretend Chokidar detected a change to verify (normally Chokidar would
- // also be ignoring this file but hey)
- change(path.join('..', 'outside.js'));
- api.run.returns(Promise.resolve(runStatus));
- return debounce().then(() => {
- t.ok(api.run.calledTwice);
- // If `../outside.js` was tracked as a dependency of test/1.js this would
- // have caused `test/1.js` to be rerun. Instead expect all tests to be
- // rerun. This is somewhat artifical: normally changes to `../outside.js`
- // wouldn't even be picked up. However this lets us test dependency
- // tracking without directly inspecting the internal state of the
- // watcher.
- t.strictDeepEqual(api.run.secondCall.args, [files, Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- change('..foo.js');
- return debounce();
- }).then(() => {
- t.ok(api.run.calledThrice);
- t.strictDeepEqual(api.run.thirdCall.args, [[path.join('test', '2.js')], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 3
- })]);
- });
- });
- test('logs a debug message when a dependent test is found', t => {
- t.plan(2);
- seed();
- change('dep-1.js');
- return debounce().then(() => {
- t.ok(debug.calledTwice);
- t.strictDeepEqual(debug.secondCall.args, ['ava:watcher', '%s is a dependency of %s', 'dep-1.js', path.join('test', '1.js')]);
- });
- });
- test('logs a debug message when sources remain without dependent tests', t => {
- t.plan(3);
- seed();
- change('cannot-be-mapped.js');
- return debounce().then(() => {
- t.ok(debug.calledThrice);
- t.strictDeepEqual(debug.secondCall.args, ['ava:watcher', 'Sources remain that cannot be traced to specific tests: %O', ['cannot-be-mapped.js']]);
- t.strictDeepEqual(debug.thirdCall.args, ['ava:watcher', 'Rerunning all tests']);
- });
- });
- });
- group('.only is sticky', (beforeEach, test) => {
- let apiEmitter;
- let runStatus;
- let runStatusEmitter;
- beforeEach(() => {
- apiEmitter = new EventEmitter();
- api.on = (event, fn) => {
- apiEmitter.on(event, fn);
- };
- runStatusEmitter = new EventEmitter();
- runStatus = {
- stats: {
- byFile: new Map(),
- declaredTests: 0,
- failedHooks: 0,
- failedTests: 0,
- failedWorkers: 0,
- files,
- finishedWorkers: 0,
- internalErrors: 0,
- remainingTests: 0,
- passedKnownFailingTests: 0,
- passedTests: 0,
- selectedTests: 0,
- skippedTests: 0,
- timeouts: 0,
- todoTests: 0,
- uncaughtExceptions: 0,
- unhandledRejections: 0
- },
- on(event, fn) {
- runStatusEmitter.on(event, fn);
- }
- };
- });
- const emitStats = (testFile, hasExclusive) => {
- runStatus.stats.byFile.set(testFile, {
- declaredTests: 2,
- failedHooks: 0,
- failedTests: 0,
- internalErrors: 0,
- remainingTests: 0,
- passedKnownFailingTests: 0,
- passedTests: 0,
- selectedTests: hasExclusive ? 1 : 2,
- skippedTests: 0,
- todoTests: 0,
- uncaughtExceptions: 0,
- unhandledRejections: 0
- });
- runStatusEmitter.emit('stateChange', {type: 'worker-finished', testFile});
- };
- const t1 = path.join('test', '1.js');
- const t2 = path.join('test', '2.js');
- const t3 = path.join('test', '3.js');
- const t4 = path.join('test', '4.js');
- const seed = () => {
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- const watcher = start();
- apiEmitter.emit('run', {
- files: [t1, t2, t3, t4],
- status: runStatus
- });
- emitStats(t1, true);
- emitStats(t2, true);
- emitStats(t3, false);
- emitStats(t4, false);
- done();
- api.run.returns(new Promise(() => {}));
- return watcher;
- };
- test('changed test files (none of which previously contained .only) are run in exclusive mode', t => {
- const options = Object.assign({}, defaultApiOptions, {runOnlyExclusive: true});
- t.plan(2);
- seed();
- change(t3);
- change(t4);
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[t1, t2, t3, t4], Object.assign({}, options, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('changed test files (comprising some, but not all, files that previously contained .only) are run in exclusive mode', t => {
- const options = Object.assign({}, defaultApiOptions, {runOnlyExclusive: true});
- t.plan(2);
- seed();
- change(t1);
- change(t4);
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[t1, t2, t4], Object.assign({}, options, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('changed test files (comprising all files that previously contained .only) are run in regular mode', t => {
- t.plan(2);
- seed();
- change(t1);
- change(t2);
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[t1, t2], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('once no test files contain .only, further changed test files are run in regular mode', t => {
- t.plan(2);
- seed();
- emitStats(t1, false);
- emitStats(t2, false);
- change(t3);
- change(t4);
- return debounce(2).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[t3, t4], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('once test files containing .only are removed, further changed test files are run in regular mode', t => {
- t.plan(2);
- seed();
- unlink(t1);
- unlink(t2);
- change(t3);
- change(t4);
- return debounce(4).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[t3, t4], Object.assign({}, defaultApiOptions, {
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- });
- group('tracks previous failures', (beforeEach, test) => {
- let apiEmitter;
- let runStatus;
- let runStatusEmitter;
- beforeEach(() => {
- apiEmitter = new EventEmitter();
- api.on = (event, fn) => {
- apiEmitter.on(event, fn);
- };
- runStatusEmitter = new EventEmitter();
- runStatus = {
- stats: {
- byFile: new Map(),
- declaredTests: 0,
- failedHooks: 0,
- failedTests: 0,
- failedWorkers: 0,
- files,
- finishedWorkers: 0,
- internalErrors: 0,
- remainingTests: 0,
- passedKnownFailingTests: 0,
- passedTests: 0,
- selectedTests: 0,
- skippedTests: 0,
- timeouts: 0,
- todoTests: 0,
- uncaughtExceptions: 0,
- unhandledRejections: 0
- },
- on(event, fn) {
- runStatusEmitter.on(event, fn);
- }
- };
- });
- const seed = seedFailures => {
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- const watcher = start();
- const files = [path.join('test', '1.js'), path.join('test', '2.js')];
- apiEmitter.emit('run', {
- files,
- status: runStatus
- });
- if (seedFailures) {
- seedFailures(files);
- }
- done();
- api.run.returns(new Promise(() => {}));
- return watcher;
- };
- const rerun = function (file) {
- runStatus = {on: runStatus.on};
- let done;
- api.run.returns(new Promise(resolve => {
- done = () => {
- resolve(runStatus);
- };
- }));
- change(file);
- return debounce().then(() => {
- apiEmitter.emit('run', {
- files: [file],
- status: runStatus
- });
- done();
- api.run.returns(new Promise(() => {}));
- });
- };
- test('runs with previousFailures set to number of prevous failures', t => {
- t.plan(2);
- let other;
- seed(files => {
- runStatusEmitter.emit('stateChange', {
- type: 'test-failed',
- testFile: files[0]
- });
- runStatusEmitter.emit('stateChange', {
- type: 'uncaught-exception',
- testFile: files[0]
- });
- other = files[1];
- });
- return rerun(other).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[other], Object.assign({}, defaultApiOptions, {
- previousFailures: 2,
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('tracks failures from multiple files', t => {
- t.plan(2);
- let first;
- seed(files => {
- runStatusEmitter.emit('stateChange', {
- type: 'test-failed',
- testFile: files[0]
- });
- runStatusEmitter.emit('stateChange', {
- type: 'test-failed',
- testFile: files[1]
- });
- first = files[0];
- });
- return rerun(first).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[first], Object.assign({}, defaultApiOptions, {
- previousFailures: 1,
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('previous failures don\'t count when that file is rerun', t => {
- t.plan(2);
- let same;
- seed(files => {
- runStatusEmitter.emit('stateChange', {
- type: 'test-failed',
- testFile: files[0]
- });
- runStatusEmitter.emit('stateChange', {
- type: 'uncaught-exception',
- testFile: files[0]
- });
- same = files[0];
- });
- return rerun(same).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[same], Object.assign({}, defaultApiOptions, {
- previousFailures: 0,
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- test('previous failures don\'t count when that file is deleted', t => {
- t.plan(2);
- let same;
- let other;
- seed(files => {
- runStatusEmitter.emit('stateChange', {
- type: 'test-failed',
- testFile: files[0]
- });
- runStatusEmitter.emit('stateChange', {
- type: 'uncaught-exception',
- testFile: files[0]
- });
- same = files[0];
- other = files[1];
- });
- unlink(same);
- return debounce().then(() => rerun(other)).then(() => {
- t.ok(api.run.calledTwice);
- t.strictDeepEqual(api.run.secondCall.args, [[other], Object.assign({}, defaultApiOptions, {
- previousFailures: 0,
- clearLogOnNextRun: true,
- runVector: 2
- })]);
- });
- });
- });
- });
|