1 | const chokidar = require('chokidar')
|
2 | const EE = require('events')
|
3 | const Minipass = require('minipass')
|
4 | const bin = require.resolve('../bin/run.js')
|
5 | const {spawn} = require('child_process')
|
6 | const onExit = require('signal-exit')
|
7 | const {writeFileSync, readFileSync} = require('fs')
|
8 | const {stringify} = require('tap-yaml')
|
9 | const {resolve} = require('path')
|
10 |
|
11 | class 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 |
|
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 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
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 |
|
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 |
|
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 |
|
136 |
|
137 | const leftover = (() => {
|
138 | try {
|
139 | return fs.readFileSync(saveFile, 'utf8').trim().split('\n')
|
140 | } catch (er) {
|
141 | return []
|
142 | }
|
143 | })()
|
144 |
|
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 |
|
180 | module.exports = {Watch}
|