ava 0.16.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/watcher.js CHANGED
@@ -1,357 +1,318 @@
1
1
  'use strict';
2
- var nodePath = require('path');
3
- var debug = require('debug')('ava:watcher');
4
- var diff = require('lodash.difference');
5
- var chokidar = require('chokidar');
6
- var flatten = require('arr-flatten');
7
- var union = require('array-union');
8
- var uniq = require('array-uniq');
9
- var AvaFiles = require('ava-files');
2
+ const nodePath = require('path');
3
+ const debug = require('debug')('ava:watcher');
4
+ const diff = require('lodash.difference');
5
+ const chokidar = require('chokidar');
6
+ const flatten = require('arr-flatten');
7
+ const union = require('array-union');
8
+ const uniq = require('array-uniq');
9
+ const AvaFiles = require('./ava-files');
10
10
 
11
11
  function rethrowAsync(err) {
12
- // Don't swallow exceptions. Note that any expected error should already have
13
- // been logged.
14
- setImmediate(function () {
12
+ // Don't swallow exceptions. Note that any
13
+ // expected error should already have been logged
14
+ setImmediate(() => {
15
15
  throw err;
16
16
  });
17
17
  }
18
18
 
19
- function Watcher(logger, api, files, sources) {
20
- this.debouncer = new Debouncer(this);
21
- this.avaFiles = new AvaFiles({
22
- files: files,
23
- sources: sources
24
- });
25
-
26
- this.isTest = this.avaFiles.makeTestMatcher();
27
-
28
- this.clearLogOnNextRun = true;
29
- this.runVector = 0;
30
- this.run = function (specificFiles) {
31
- if (this.runVector > 0) {
32
- var cleared = this.clearLogOnNextRun && logger.clear();
33
- if (!cleared) {
34
- logger.reset();
35
- logger.section();
36
- }
37
- this.clearLogOnNextRun = true;
38
-
39
- logger.reset();
40
- logger.start();
19
+ class Debouncer {
20
+ constructor(watcher) {
21
+ this.watcher = watcher;
22
+ this.timer = null;
23
+ this.repeat = false;
24
+ }
25
+ debounce() {
26
+ if (this.timer) {
27
+ this.again = true;
28
+ return;
41
29
  }
42
30
 
43
- var currentVector = this.runVector += 1;
31
+ const timer = this.timer = setTimeout(() => {
32
+ this.watcher.busy.then(() => {
33
+ // Do nothing if debouncing was canceled while waiting for the busy
34
+ // promise to fulfil
35
+ if (this.timer !== timer) {
36
+ return;
37
+ }
38
+
39
+ if (this.again) {
40
+ this.timer = null;
41
+ this.again = false;
42
+ this.debounce();
43
+ } else {
44
+ this.watcher.runAfterChanges();
45
+ this.timer = null;
46
+ this.again = false;
47
+ }
48
+ });
49
+ }, 10);
50
+ }
51
+ cancel() {
52
+ if (this.timer) {
53
+ clearTimeout(this.timer);
54
+ this.timer = null;
55
+ this.again = false;
56
+ }
57
+ }
58
+ }
44
59
 
45
- var runOnlyExclusive = false;
60
+ class TestDependency {
61
+ constructor(file, sources) {
62
+ this.file = file;
63
+ this.sources = sources;
64
+ }
65
+ contains(source) {
66
+ return this.sources.indexOf(source) !== -1;
67
+ }
68
+ }
46
69
 
47
- if (specificFiles) {
48
- var exclusiveFiles = specificFiles.filter(function (file) {
49
- return this.filesWithExclusiveTests.indexOf(file) !== -1;
50
- }, this);
70
+ class Watcher {
71
+ constructor(logger, api, files, sources) {
72
+ this.debouncer = new Debouncer(this);
73
+ this.avaFiles = new AvaFiles({
74
+ files,
75
+ sources
76
+ });
51
77
 
52
- runOnlyExclusive = exclusiveFiles.length !== this.filesWithExclusiveTests.length;
78
+ this.clearLogOnNextRun = true;
79
+ this.runVector = 0;
80
+ this.run = specificFiles => {
81
+ if (this.runVector > 0) {
82
+ const cleared = this.clearLogOnNextRun && logger.clear();
83
+ if (!cleared) {
84
+ logger.reset();
85
+ logger.section();
86
+ }
87
+ this.clearLogOnNextRun = true;
53
88
 
54
- if (runOnlyExclusive) {
55
- // The test files that previously contained exclusive tests are always
56
- // run, together with the remaining specific files.
57
- var remainingFiles = diff(specificFiles, exclusiveFiles);
58
- specificFiles = this.filesWithExclusiveTests.concat(remainingFiles);
89
+ logger.reset();
90
+ logger.start();
59
91
  }
60
- }
61
92
 
62
- var self = this;
63
- this.busy = api.run(specificFiles || files, {
64
- runOnlyExclusive: runOnlyExclusive
65
- }).then(function (runStatus) {
66
- runStatus.previousFailCount = self.sumPreviousFailures(currentVector);
67
- logger.finish(runStatus);
93
+ const currentVector = this.runVector += 1;
68
94
 
69
- var badCounts = runStatus.failCount + runStatus.rejectionCount + runStatus.exceptionCount;
70
- self.clearLogOnNextRun = self.clearLogOnNextRun && badCounts === 0;
71
- }, rethrowAsync);
72
- };
95
+ let runOnlyExclusive = false;
73
96
 
74
- this.testDependencies = [];
75
- this.trackTestDependencies(api, sources);
97
+ if (specificFiles) {
98
+ const exclusiveFiles = specificFiles.filter(file => this.filesWithExclusiveTests.indexOf(file) !== -1);
76
99
 
77
- this.filesWithExclusiveTests = [];
78
- this.trackExclusivity(api);
100
+ runOnlyExclusive = exclusiveFiles.length !== this.filesWithExclusiveTests.length;
79
101
 
80
- this.filesWithFailures = [];
81
- this.trackFailures(api);
102
+ if (runOnlyExclusive) {
103
+ // The test files that previously contained exclusive tests are always
104
+ // run, together with the remaining specific files.
105
+ const remainingFiles = diff(specificFiles, exclusiveFiles);
106
+ specificFiles = this.filesWithExclusiveTests.concat(remainingFiles);
107
+ }
108
+ }
82
109
 
83
- this.dirtyStates = {};
84
- this.watchFiles();
85
- this.rerunAll();
86
- }
110
+ this.busy = api.run(specificFiles || files, {runOnlyExclusive})
111
+ .then(runStatus => {
112
+ runStatus.previousFailCount = this.sumPreviousFailures(currentVector);
113
+ logger.finish(runStatus);
87
114
 
88
- module.exports = Watcher;
115
+ const badCounts = runStatus.failCount + runStatus.rejectionCount + runStatus.exceptionCount;
116
+ this.clearLogOnNextRun = this.clearLogOnNextRun && badCounts === 0;
117
+ })
118
+ .catch(rethrowAsync);
119
+ };
89
120
 
90
- Watcher.prototype.watchFiles = function () {
91
- var self = this;
92
- var patterns = this.avaFiles.getChokidarPatterns();
93
-
94
- chokidar.watch(patterns.paths, {
95
- ignored: patterns.ignored,
96
- ignoreInitial: true
97
- }).on('all', function (event, path) {
98
- if (event === 'add' || event === 'change' || event === 'unlink') {
99
- debug('Detected %s of %s', event, path);
100
- self.dirtyStates[path] = event;
101
- self.debouncer.debounce();
102
- }
103
- });
104
- };
121
+ this.testDependencies = [];
122
+ this.trackTestDependencies(api, sources);
105
123
 
106
- Watcher.prototype.trackTestDependencies = function (api) {
107
- var self = this;
108
- var isSource = this.avaFiles.makeSourceMatcher();
124
+ this.filesWithExclusiveTests = [];
125
+ this.trackExclusivity(api);
109
126
 
110
- var relative = function (absPath) {
111
- return nodePath.relative('.', absPath);
112
- };
127
+ this.filesWithFailures = [];
128
+ this.trackFailures(api);
113
129
 
114
- api.on('test-run', function (runStatus) {
115
- runStatus.on('dependencies', function (file, dependencies) {
116
- var sourceDeps = dependencies.map(relative).filter(isSource);
117
- self.updateTestDependencies(file, sourceDeps);
130
+ this.dirtyStates = {};
131
+ this.watchFiles();
132
+ this.rerunAll();
133
+ }
134
+ watchFiles() {
135
+ const patterns = this.avaFiles.getChokidarPatterns();
136
+
137
+ chokidar.watch(patterns.paths, {
138
+ ignored: patterns.ignored,
139
+ ignoreInitial: true
140
+ }).on('all', (event, path) => {
141
+ if (event === 'add' || event === 'change' || event === 'unlink') {
142
+ debug('Detected %s of %s', event, path);
143
+ this.dirtyStates[path] = event;
144
+ this.debouncer.debounce();
145
+ }
118
146
  });
119
- });
120
- };
121
-
122
- Watcher.prototype.updateTestDependencies = function (file, sources) {
123
- if (sources.length === 0) {
124
- this.testDependencies = this.testDependencies.filter(function (dep) {
125
- return dep.file !== file;
147
+ }
148
+ trackTestDependencies(api) {
149
+ const relative = absPath => nodePath.relative(process.cwd(), absPath);
150
+
151
+ api.on('test-run', runStatus => {
152
+ runStatus.on('dependencies', (file, dependencies) => {
153
+ const sourceDeps = dependencies.map(relative).filter(this.avaFiles.isSource);
154
+ this.updateTestDependencies(file, sourceDeps);
155
+ });
126
156
  });
127
-
128
- return;
129
157
  }
130
-
131
- var isUpdate = this.testDependencies.some(function (dep) {
132
- if (dep.file !== file) {
133
- return false;
158
+ updateTestDependencies(file, sources) {
159
+ if (sources.length === 0) {
160
+ this.testDependencies = this.testDependencies.filter(dep => dep.file !== file);
161
+ return;
134
162
  }
135
163
 
136
- dep.sources = sources;
137
-
138
- return true;
139
- });
140
-
141
- if (!isUpdate) {
142
- this.testDependencies.push(new TestDependency(file, sources));
143
- }
144
- };
145
-
146
- Watcher.prototype.trackExclusivity = function (api) {
147
- var self = this;
148
-
149
- api.on('stats', function (stats) {
150
- self.updateExclusivity(stats.file, stats.hasExclusive);
151
- });
152
- };
153
-
154
- Watcher.prototype.updateExclusivity = function (file, hasExclusiveTests) {
155
- var index = this.filesWithExclusiveTests.indexOf(file);
156
-
157
- if (hasExclusiveTests && index === -1) {
158
- this.filesWithExclusiveTests.push(file);
159
- } else if (!hasExclusiveTests && index !== -1) {
160
- this.filesWithExclusiveTests.splice(index, 1);
161
- }
162
- };
163
-
164
- Watcher.prototype.trackFailures = function (api) {
165
- var self = this;
164
+ const isUpdate = this.testDependencies.some(dep => {
165
+ if (dep.file !== file) {
166
+ return false;
167
+ }
166
168
 
167
- api.on('test-run', function (runStatus, files) {
168
- files.forEach(function (file) {
169
- self.pruneFailures(nodePath.relative('.', file));
170
- });
169
+ dep.sources = sources;
171
170
 
172
- var currentVector = self.runVector;
173
- runStatus.on('error', function (err) {
174
- self.countFailure(err.file, currentVector);
175
- });
176
- runStatus.on('test', function (result) {
177
- if (result.error) {
178
- self.countFailure(result.file, currentVector);
179
- }
171
+ return true;
180
172
  });
181
- });
182
- };
183
-
184
- Watcher.prototype.pruneFailures = function (file) {
185
- this.filesWithFailures = this.filesWithFailures.filter(function (state) {
186
- return state.file !== file;
187
- });
188
- };
189
173
 
190
- Watcher.prototype.countFailure = function (file, vector) {
191
- var isUpdate = this.filesWithFailures.some(function (state) {
192
- if (state.file !== file) {
193
- return false;
174
+ if (!isUpdate) {
175
+ this.testDependencies.push(new TestDependency(file, sources));
194
176
  }
195
-
196
- state.count++;
197
- return true;
198
- });
199
-
200
- if (!isUpdate) {
201
- this.filesWithFailures.push({
202
- file: file,
203
- vector: vector,
204
- count: 1
177
+ }
178
+ trackExclusivity(api) {
179
+ api.on('stats', stats => {
180
+ this.updateExclusivity(stats.file, stats.hasExclusive);
205
181
  });
206
182
  }
207
- };
208
-
209
- Watcher.prototype.sumPreviousFailures = function (beforeVector) {
210
- var total = 0;
183
+ updateExclusivity(file, hasExclusiveTests) {
184
+ const index = this.filesWithExclusiveTests.indexOf(file);
211
185
 
212
- this.filesWithFailures.forEach(function (state) {
213
- if (state.vector < beforeVector) {
214
- total += state.count;
186
+ if (hasExclusiveTests && index === -1) {
187
+ this.filesWithExclusiveTests.push(file);
188
+ } else if (!hasExclusiveTests && index !== -1) {
189
+ this.filesWithExclusiveTests.splice(index, 1);
215
190
  }
216
- });
217
-
218
- return total;
219
- };
220
-
221
- Watcher.prototype.cleanUnlinkedTests = function (unlinkedTests) {
222
- unlinkedTests.forEach(function (testFile) {
223
- this.updateTestDependencies(testFile, []);
224
- this.updateExclusivity(testFile, false);
225
- this.pruneFailures(testFile);
226
- }, this);
227
- };
228
-
229
- Watcher.prototype.observeStdin = function (stdin) {
230
- var self = this;
191
+ }
192
+ trackFailures(api) {
193
+ api.on('test-run', (runStatus, files) => {
194
+ files.forEach(file => {
195
+ this.pruneFailures(nodePath.relative(process.cwd(), file));
196
+ });
197
+
198
+ const currentVector = this.runVector;
199
+ runStatus.on('error', err => {
200
+ this.countFailure(err.file, currentVector);
201
+ });
202
+ runStatus.on('test', result => {
203
+ if (result.error) {
204
+ this.countFailure(result.file, currentVector);
205
+ }
206
+ });
207
+ });
208
+ }
209
+ pruneFailures(file) {
210
+ this.filesWithFailures = this.filesWithFailures.filter(state => state.file !== file);
211
+ }
212
+ countFailure(file, vector) {
213
+ const isUpdate = this.filesWithFailures.some(state => {
214
+ if (state.file !== file) {
215
+ return false;
216
+ }
231
217
 
232
- stdin.resume();
233
- stdin.setEncoding('utf8');
218
+ state.count++;
219
+ return true;
220
+ });
234
221
 
235
- stdin.on('data', function (data) {
236
- data = data.trim().toLowerCase();
237
- if (data !== 'r' && data !== 'rs') {
238
- return;
222
+ if (!isUpdate) {
223
+ this.filesWithFailures.push({
224
+ file,
225
+ vector,
226
+ count: 1
227
+ });
239
228
  }
229
+ }
230
+ sumPreviousFailures(beforeVector) {
231
+ let total = 0;
240
232
 
241
- // Cancel the debouncer, it might rerun specific tests whereas *all* tests
242
- // need to be rerun.
243
- self.debouncer.cancel();
244
- self.busy.then(function () {
245
- // Cancel the debouncer again, it might have restarted while waiting for
246
- // the busy promise to fulfil.
247
- self.debouncer.cancel();
248
- self.clearLogOnNextRun = false;
249
- self.rerunAll();
233
+ this.filesWithFailures.forEach(state => {
234
+ if (state.vector < beforeVector) {
235
+ total += state.count;
236
+ }
250
237
  });
251
- });
252
- };
253
-
254
- Watcher.prototype.rerunAll = function () {
255
- this.dirtyStates = {};
256
- this.run();
257
- };
258
-
259
- Watcher.prototype.runAfterChanges = function () {
260
- var dirtyStates = this.dirtyStates;
261
- this.dirtyStates = {};
262
-
263
- var dirtyPaths = Object.keys(dirtyStates);
264
- var dirtyTests = dirtyPaths.filter(this.isTest);
265
- var dirtySources = diff(dirtyPaths, dirtyTests);
266
- var addedOrChangedTests = dirtyTests.filter(function (path) {
267
- return dirtyStates[path] !== 'unlink';
268
- });
269
- var unlinkedTests = diff(dirtyTests, addedOrChangedTests);
270
238
 
271
- this.cleanUnlinkedTests(unlinkedTests);
272
- // No need to rerun tests if the only change is that tests were deleted.
273
- if (unlinkedTests.length === dirtyPaths.length) {
274
- return;
239
+ return total;
275
240
  }
276
-
277
- if (dirtySources.length === 0) {
278
- // Run any new or changed tests.
279
- this.run(addedOrChangedTests);
280
- return;
241
+ cleanUnlinkedTests(unlinkedTests) {
242
+ unlinkedTests.forEach(testFile => {
243
+ this.updateTestDependencies(testFile, []);
244
+ this.updateExclusivity(testFile, false);
245
+ this.pruneFailures(testFile);
246
+ });
281
247
  }
248
+ observeStdin(stdin) {
249
+ stdin.resume();
250
+ stdin.setEncoding('utf8');
282
251
 
283
- // Try to find tests that depend on the changed source files.
284
- var testsBySource = dirtySources.map(function (path) {
285
- return this.testDependencies.filter(function (dep) {
286
- return dep.contains(path);
287
- }).map(function (dep) {
288
- debug('%s is a dependency of %s', path, dep.file);
289
- return dep.file;
290
- });
291
- }, this).filter(function (tests) {
292
- return tests.length > 0;
293
- });
252
+ stdin.on('data', data => {
253
+ data = data.trim().toLowerCase();
254
+ if (data !== 'r' && data !== 'rs') {
255
+ return;
256
+ }
294
257
 
295
- // Rerun all tests if source files were changed that could not be traced to
296
- // specific tests.
297
- if (testsBySource.length !== dirtySources.length) {
298
- debug('Sources remain that cannot be traced to specific tests. Rerunning all tests');
258
+ // Cancel the debouncer, it might rerun specific tests whereas *all* tests
259
+ // need to be rerun
260
+ this.debouncer.cancel();
261
+ this.busy.then(() => {
262
+ // Cancel the debouncer again, it might have restarted while waiting for
263
+ // the busy promise to fulfil
264
+ this.debouncer.cancel();
265
+ this.clearLogOnNextRun = false;
266
+ this.rerunAll();
267
+ });
268
+ });
269
+ }
270
+ rerunAll() {
271
+ this.dirtyStates = {};
299
272
  this.run();
300
- return;
301
273
  }
274
+ runAfterChanges() {
275
+ const dirtyStates = this.dirtyStates;
276
+ this.dirtyStates = {};
302
277
 
303
- // Run all affected tests.
304
- this.run(union(addedOrChangedTests, uniq(flatten(testsBySource))));
305
- };
306
-
307
- function Debouncer(watcher) {
308
- this.watcher = watcher;
309
- this.timer = null;
310
- this.repeat = false;
311
- }
278
+ const dirtyPaths = Object.keys(dirtyStates);
279
+ const dirtyTests = dirtyPaths.filter(this.avaFiles.isTest);
280
+ const dirtySources = diff(dirtyPaths, dirtyTests);
281
+ const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink');
282
+ const unlinkedTests = diff(dirtyTests, addedOrChangedTests);
312
283
 
313
- Debouncer.prototype.debounce = function () {
314
- if (this.timer) {
315
- this.again = true;
316
- return;
317
- }
284
+ this.cleanUnlinkedTests(unlinkedTests);
318
285
 
319
- var self = this;
286
+ // No need to rerun tests if the only change is that tests were deleted
287
+ if (unlinkedTests.length === dirtyPaths.length) {
288
+ return;
289
+ }
320
290
 
321
- var timer = this.timer = setTimeout(function () {
322
- self.watcher.busy.then(function () {
323
- // Do nothing if debouncing was canceled while waiting for the busy
324
- // promise to fulfil.
325
- if (self.timer !== timer) {
326
- return;
327
- }
291
+ if (dirtySources.length === 0) {
292
+ // Run any new or changed tests
293
+ this.run(addedOrChangedTests);
294
+ return;
295
+ }
328
296
 
329
- if (self.again) {
330
- self.timer = null;
331
- self.again = false;
332
- self.debounce();
333
- } else {
334
- self.watcher.runAfterChanges();
335
- self.timer = null;
336
- self.again = false;
337
- }
338
- });
339
- }, 10);
340
- };
297
+ // Try to find tests that depend on the changed source files
298
+ const testsBySource = dirtySources.map(path => {
299
+ return this.testDependencies.filter(dep => dep.contains(path)).map(dep => {
300
+ debug('%s is a dependency of %s', path, dep.file);
301
+ return dep.file;
302
+ });
303
+ }, this).filter(tests => tests.length > 0);
304
+
305
+ // Rerun all tests if source files were changed that could not be traced to
306
+ // specific tests
307
+ if (testsBySource.length !== dirtySources.length) {
308
+ debug('Sources remain that cannot be traced to specific tests. Rerunning all tests');
309
+ this.run();
310
+ return;
311
+ }
341
312
 
342
- Debouncer.prototype.cancel = function () {
343
- if (this.timer) {
344
- clearTimeout(this.timer);
345
- this.timer = null;
346
- this.again = false;
313
+ // Run all affected tests
314
+ this.run(union(addedOrChangedTests, uniq(flatten(testsBySource))));
347
315
  }
348
- };
349
-
350
- function TestDependency(file, sources) {
351
- this.file = file;
352
- this.sources = sources;
353
316
  }
354
317
 
355
- TestDependency.prototype.contains = function (source) {
356
- return this.sources.indexOf(source) !== -1;
357
- };
318
+ module.exports = Watcher;