UNPKG

4.99 kBJavaScriptView Raw
1const chokidar = require('chokidar')
2const EE = require('events')
3const Minipass = require('minipass')
4const bin = require.resolve('../bin/run.js')
5const {spawn} = require('child_process')
6const onExit = require('signal-exit')
7const {writeFileSync, readFileSync} = require('fs')
8const {stringify} = require('tap-yaml')
9const {resolve} = require('path')
10
11class Watch extends Minipass {
12 constructor (options) {
13 if (!options.coverage)
14 throw new Error('--watch requires coverage to be enabled')
15 super()
16 this.args = [bin, ...options._.parsed, '--no-watch']
17 this.positionals = [...options._]
18 this.log('initial test run', this.args)
19 this.proc = spawn(process.execPath, this.args, {
20 stdio: 'inherit'
21 })
22 this.proc.on('close', () => this.main())
23 const saveFolder = 'node_modules/.cache/tap'
24 require('../settings.js').mkdirRecursiveSync(saveFolder)
25 this.saveFile = saveFolder + '/watch-' + process.pid
26 /* istanbul ignore next */
27 onExit(() => require('../settings.js').rmdirRecursiveSync(this.saveFile))
28 this.index = null
29 this.indexFile = '.nyc_output/processinfo/index.json'
30 this.fileList = []
31 this.queue = []
32 this.watcher = null
33 this.env = { ...process.env }
34 }
35
36 readIndex () {
37 this.index = JSON.parse(readFileSync(this.indexFile, 'utf8'))
38 }
39
40 kill (signal) {
41 if (this.proc)
42 this.proc.kill(signal)
43 }
44
45 watchList () {
46 if (!this.index)
47 this.readIndex()
48 // externalIds are the relative path to a test file
49 // the files object keys are fully resolved.
50 // If a test is covered, it'll show up in both!
51 // Since a covered test was definitely included in its own
52 // test run, don't add it a second time, so we don't get
53 // two chokidar events for the same file change.
54 const cwd = process.cwd()
55 const fileSet = new Set(Object.keys(this.index.files))
56 Object.keys(this.index.externalIds)
57 .filter(f => !fileSet.has(resolve(f)))
58 .forEach(f => fileSet.add(f))
59 return [...fileSet]
60 }
61
62 pause () {
63 if (this.watcher)
64 this.watcher.close()
65 this.watcher = null
66 }
67
68 resume () {
69 if (!this.watcher)
70 this.watch()
71 }
72
73 main () {
74 this.emit('main')
75 this.proc = null
76 this.fileList = this.watchList()
77 this.watch()
78 }
79
80 watch () {
81 this.pause()
82 const sawAdd = new Map()
83 const watcher = this.watcher = chokidar.watch(this.fileList)
84 // ignore the first crop of add events, since we already ran the tests
85 watcher.on('all', (ev, file) => {
86 if (ev === 'add' && !sawAdd.get(file))
87 sawAdd.set(file, true)
88 else
89 this.onChange(ev, file)
90 })
91 return watcher
92 }
93
94 onChange (ev, file) {
95 const tests = this.testsFromChange(file)
96
97 this.queue.push(...tests)
98 this.log(ev + ' ' + file)
99
100 if (this.proc)
101 return this.log('test in progress, queuing for next run')
102
103 this.run()
104 }
105
106 run (env) {
107 const set = [...new Set(this.queue)]
108 this.log('running tests', set)
109 writeFileSync(this.saveFile, set.join('\n') + '\n')
110 this.queue.length = 0
111
112 this.proc = spawn(process.execPath, [
113 ...this.args, '--save=' + this.saveFile, '--nyc-arg=--no-clean'
114 ], {
115 stdio: 'inherit',
116 env: this.env,
117 })
118 this.proc.on('close', (code, signal) => this.onClose(code, signal))
119 this.emit('process', this.proc)
120 }
121
122 onClose (code, signal) {
123 this.readIndex()
124 this.proc = null
125
126 // only add if it's not already there as either a test or included file
127 const newFileList = this.watchList().filter(f =>
128 !this.fileList.includes(f) &&
129 !this.fileList.includes(resolve(f)))
130
131 this.fileList.push(...newFileList)
132 this.resume()
133 newFileList.forEach(f => this.watcher.add(f))
134
135 // if there are any failures (especially, from a bail out)
136 // then add those, but ignore if it's not there.
137 const leftover = (() => {
138 try {
139 return fs.readFileSync(saveFile, 'utf8').trim().split('\n')
140 } catch (er) {
141 return []
142 }
143 })()
144 // run again if something was added during the process
145 const runAgain = this.queue.length
146 this.queue.push(...leftover)
147
148 if (runAgain)
149 this.run()
150 else
151 this.emit('afterProcess', {code, signal})
152 }
153
154 log (msg, arg) {
155 if (arg && typeof arg !== 'string')
156 msg += '\n' + stringify(arg)
157 this.write(msg + '\n')
158 }
159
160 testsFromChange (file) {
161 return this.index.externalIds[file] ? [file]
162 : this.testsFromFile(file)
163 }
164
165 testsFromFile (file) {
166 const reducer = (set, uuid) => {
167 for (let process = this.index.processes[uuid];
168 process;
169 process = process.parent && this.index.processes[process.parent]) {
170 if (process.externalId)
171 set.add(process.externalId)
172 }
173 return set
174 }
175 const procs = this.index.files[file] || /* istanbul ignore next */ []
176 return [...procs.reduce(reducer, new Set())]
177 }
178}
179
180module.exports = {Watch}