From 11bf22d79e090c8e28bb84b7adb4b1ce158bbdb7 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:01:05 +0700 Subject: [PATCH 1/4] test: use a scoped options object --- test.mjs | 78 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/test.mjs b/test.mjs index f1b25c54..cfed3fea 100644 --- a/test.mjs +++ b/test.mjs @@ -37,7 +37,6 @@ const allWatchers = []; const PERM_ARR = 0o755; // rwe, r+e, r+e const TEST_TIMEOUT = 8000; let subdirId = 0; -let options; let currentDir; let slowerDelay; @@ -101,7 +100,7 @@ const getGlobPath = (subPath) => { }; currentDir = getFixturePath(''); -const chokidar_watch = (path = currentDir, opts = options) => { +const chokidar_watch = (path = currentDir, opts) => { const wt = chokidar.watch(path, opts); allWatchers.push(wt); return wt; @@ -156,6 +155,7 @@ const dateNow = () => Date.now().toString(); const runTests = (baseopts) => { let macosFswatch; let win32Polling; + let options; baseopts.persistent = true; @@ -180,7 +180,7 @@ const runTests = (baseopts) => { options.alwaysStat = true; readySpy = sinon.spy(function readySpy(){}); rawSpy = sinon.spy(function rawSpy(){}); - watcher = chokidar_watch().on(EV.READY, readySpy).on(EV.RAW, rawSpy); + watcher = chokidar_watch(currentDir, options).on(EV.READY, readySpy).on(EV.RAW, rawSpy); await waitForWatcher(watcher); }); afterEach(async () => { @@ -293,7 +293,7 @@ const runTests = (baseopts) => { await delay(); readySpy.resetHistory(); - watcher2 = chokidar_watch().on(EV.READY, readySpy).on(EV.RAW, rawSpy); + watcher2 = chokidar_watch(currentDir, options).on(EV.READY, readySpy).on(EV.RAW, rawSpy); const spy = await aspy(watcher2, EV.ADD, null, true); const filesToWrite = [ @@ -586,7 +586,7 @@ const runTests = (baseopts) => { describe('watch individual files', () => { it('should emit `ready` when three files were added', async () => { const readySpy = sinon.spy(function readySpy(){}); - const watcher = chokidar_watch().on(EV.READY, readySpy); + const watcher = chokidar_watch(currentDir, options).on(EV.READY, readySpy); const path1 = getFixturePath('add1.txt'); const path2 = getFixturePath('add2.txt'); const path3 = getFixturePath('add3.txt'); @@ -868,7 +868,7 @@ const runTests = (baseopts) => { const filePath = getFixturePath('nota[glob].txt'); await write(filePath, 'b'); await delay(); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.have.been.calledWith(EV.ADD, filePath); await delay(); @@ -994,7 +994,7 @@ const runTests = (baseopts) => { it('should not recurse indefinitely on circular symlinks', async () => { await fs_symlink(currentDir, getFixturePath('subdir/circular'), isWindows ? 'dir' : null); return new Promise((resolve, reject) => { - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); watcher.on(EV.ERROR, resolve()); watcher.on(EV.READY, reject('The watcher becomes ready, although he watches a circular symlink.')); }) @@ -1010,7 +1010,7 @@ const runTests = (baseopts) => { }); it('should follow newly created symlinks', async () => { options.ignoreInitial = true; - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); const spy = await aspy(watcher, EV.ALL); await delay(); await fs_symlink(getFixturePath('subdir'), getFixturePath('link'), isWindows ? 'dir' : null); @@ -1049,7 +1049,7 @@ const runTests = (baseopts) => { // Create symlink in linkPath const linkPath = getFixturePath('link'); fs.symlinkSync(getFixturePath('subdir'), linkPath); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await delay(300); setTimeout(() => { fs.writeFileSync(getFixturePath('subdir/add.txt'), dateNow()); @@ -1124,7 +1124,7 @@ const runTests = (baseopts) => { spy.should.have.been.calledWith(EV.CHANGE, testPath); }); it('should throw if provided any non-string paths', () => { - expect(chokidar_watch.bind(null, [[currentDir], /notastring/])) + expect(chokidar_watch.bind(null, [[currentDir], /notastring/], options)) .to.throw(TypeError, /non-string/i); }); }); @@ -1157,7 +1157,7 @@ const runTests = (baseopts) => { describe('true', () => { beforeEach(() => { options.ignoreInitial = true; }); it('should ignore initial add events', async () => { - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); const spy = await aspy(watcher, EV.ADD); await delay(); spy.should.not.have.been.called; @@ -1172,7 +1172,7 @@ const runTests = (baseopts) => { it('should notice when a file appears in an empty directory', async () => { const testDir = getFixturePath('subdir'); const testPath = getFixturePath('subdir/add.txt'); - const spy = await aspy(chokidar_watch(), EV.ADD); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ADD); spy.should.not.have.been.called; await fs_mkdir(testDir, PERM_ARR); await write(testPath, dateNow()); @@ -1182,7 +1182,7 @@ const runTests = (baseopts) => { }); it('should emit a change on a preexisting file as a change', async () => { const testPath = getFixturePath('change.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.not.have.been.called; await write(testPath, dateNow()); await waitFor([spy.withArgs(EV.CHANGE, testPath)]); @@ -1195,7 +1195,7 @@ const runTests = (baseopts) => { await fs_mkdir(getFixturePath('subdir'), PERM_ARR); await delay(200); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, dateNow()); await waitFor([spy]); @@ -1223,7 +1223,7 @@ const runTests = (baseopts) => { }); it('should not choke on an ignored watch path', async () => { options.ignored = () => { return true; }; - await waitForWatcher(chokidar_watch()); + await waitForWatcher(chokidar_watch(currentDir, options)); }); it('should ignore the contents of ignored dirs', async () => { const testDir = getFixturePath('subdir'); @@ -1272,7 +1272,7 @@ const runTests = (baseopts) => { }); it('should not recurse if depth is 0', async () => { options.depth = 0; - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); const spy = await aspy(watcher, EV.ALL); await write(getFixturePath('subdir/add.txt'), dateNow()); await waitFor([[spy, 4]]); @@ -1288,7 +1288,7 @@ const runTests = (baseopts) => { const addPath = getFixturePath('subdir/add.txt'); const changePath = getFixturePath('change.txt'); const ignoredPath = getFixturePath('subdir/subsub/ab.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await delay(); await write(getFixturePath('change.txt'), dateNow()); await write(addPath, dateNow()); @@ -1306,7 +1306,7 @@ const runTests = (baseopts) => { options.depth = 1; await fs_symlink(getFixturePath('subdir'), getFixturePath('link'), isWindows ? 'dir' : null); await delay(); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('link')); spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('link/subsub')); spy.should.have.been.calledWith(EV.ADD, getFixturePath('link/add.txt')); @@ -1318,7 +1318,7 @@ const runTests = (baseopts) => { options.ignoreInitial = true; const linkPath = getFixturePath('link'); const dirPath = getFixturePath('link/subsub'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await fs_symlink(getFixturePath('subdir'), linkPath, isWindows ? 'dir' : null); await waitFor([[spy, 3], spy.withArgs(EV.ADD_DIR, dirPath)]); spy.should.have.been.calledWith(EV.ADD_DIR, linkPath); @@ -1329,7 +1329,7 @@ const runTests = (baseopts) => { it('should correctly handle dir events when depth is 0', async () => { options.depth = 0; const subdir2 = getFixturePath('subdir2'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); const addSpy = spy.withArgs(EV.ADD_DIR); const unlinkSpy = spy.withArgs(EV.UNLINK_DIR); spy.should.have.been.calledWith(EV.ADD_DIR, currentDir); @@ -1351,7 +1351,7 @@ const runTests = (baseopts) => { options.ignoreInitial = true; }); it('should ignore vim/emacs/Sublime swapfiles', async () => { - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(getFixturePath('.change.txt.swp'), 'a'); // vim await write(getFixturePath('add.txt~'), 'a'); // vim/emacs await write(getFixturePath('.subl5f4.tmp'), 'a'); // sublime @@ -1370,7 +1370,7 @@ const runTests = (baseopts) => { options.ignoreInitial = false; await write(getFixturePath('old.txt~'), 'a'); await delay(); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.not.have.been.calledWith(getFixturePath('old.txt')); spy.should.not.have.been.calledWith(getFixturePath('old.txt~')); }); @@ -1478,7 +1478,7 @@ const runTests = (baseopts) => { }); it('should not watch files without read permissions', async () => { if (isWindows) return true; - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.not.have.been.calledWith(EV.ADD, filePath); await write(filePath, dateNow()); @@ -1489,7 +1489,7 @@ const runTests = (baseopts) => { describe('true', () => { beforeEach(() => { options.ignorePermissionErrors = true; }); it('should watch unreadable files if possible', async () => { - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.have.been.calledWith(EV.ADD, filePath); }); it('should not choke on non-existent files', async () => { @@ -1505,20 +1505,20 @@ const runTests = (baseopts) => { }); it('should use default options if none given', () => { options.awaitWriteFinish = true; - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); expect(watcher.options.awaitWriteFinish.pollInterval).to.equal(100); expect(watcher.options.awaitWriteFinish.stabilityThreshold).to.equal(2000); }); it('should not emit add event before a file is fully written', async () => { const testPath = getFixturePath('add.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'hello'); await delay(200); spy.should.not.have.been.calledWith(EV.ADD); }); it('should wait for the file to be fully written before emitting the add event', async () => { const testPath = getFixturePath('add.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'hello'); await delay(300); @@ -1528,7 +1528,7 @@ const runTests = (baseopts) => { }); it('should emit with the final stats', async () => { const testPath = getFixturePath('add.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'hello '); await delay(300); @@ -1540,7 +1540,7 @@ const runTests = (baseopts) => { }); it('should not emit change event while a file has not been fully written', async () => { const testPath = getFixturePath('add.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'hello'); await delay(100); await write(testPath, 'edit'); @@ -1549,14 +1549,14 @@ const runTests = (baseopts) => { }); it('should not emit change event before an existing file is fully updated', async () => { const testPath = getFixturePath('change.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'hello'); await delay(300); spy.should.not.have.been.calledWith(EV.CHANGE, testPath); }); it('should wait for an existing file to be fully updated before emitting the change event', async () => { const testPath = getFixturePath('change.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); fs.writeFile(testPath, 'hello', () => {}); await delay(300); @@ -1566,7 +1566,7 @@ const runTests = (baseopts) => { }); it('should emit change event after the file is fully written', async () => { const testPath = getFixturePath('add.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await delay(); await write(testPath, 'hello'); @@ -1578,7 +1578,7 @@ const runTests = (baseopts) => { }); it('should not raise any event for a file that was deleted before fully written', async () => { const testPath = getFixturePath('add.txt'); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'hello'); await delay(400); await fs_unlink(testPath); @@ -1592,7 +1592,7 @@ const runTests = (baseopts) => { await fs_mkdir(options.cwd); await delay(200); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await delay(400); await write(testPath, 'hello'); @@ -1602,7 +1602,7 @@ const runTests = (baseopts) => { }); it('should still emit initial add events', async () => { options.ignoreInitial = false; - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); spy.should.have.been.calledWith(EV.ADD); spy.should.have.been.calledWith(EV.ADD_DIR); }); @@ -1614,7 +1614,7 @@ const runTests = (baseopts) => { await delay(); await write(testPath, 'hello'); await delay(); - const spy = await aspy(chokidar_watch(), EV.ALL); + const spy = await aspy(chokidar_watch(currentDir, options), EV.ALL); await write(testPath, 'edit'); await delay(); await fs_unlink(testPath); @@ -1669,7 +1669,7 @@ const runTests = (baseopts) => { it('should handle unlink that happens while waiting for stat to return', (done) => { const spy = sinon.spy(); const testPath = getFixturePath('add.txt'); - chokidar_watch() + chokidar_watch(currentDir, options) .on(EV.ALL, spy) .on(EV.READY, () => { fs.writeFile(testPath, 'hello', simpleCb); @@ -1705,7 +1705,7 @@ const runTests = (baseopts) => { const expected = {}; expected[sysPath.dirname(currentDir)] = [subdirId.toString()]; expected[currentDir] = ['change.txt', 'unlink.txt']; - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); await waitForWatcher(watcher); expect(watcher.getWatched()).to.deep.equal(expected); }); @@ -1717,7 +1717,7 @@ const runTests = (baseopts) => { 'subdir': [] }; await fs_mkdir(getFixturePath('subdir'), PERM_ARR); - const watcher = chokidar_watch(); + const watcher = chokidar_watch(currentDir, options); await waitForWatcher(watcher); expect(watcher.getWatched()).to.deep.equal(expected); }); From 962d959ab1829978ebf12399c1e4554db86626a3 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:52:12 +0700 Subject: [PATCH 2/4] chore: remove node 16 support node 16.x is incapable of running the test suite thanks to one or more bugs which never had their fixes backported. Primarily, the inability to have nested `beforeEach` was fixed in all later versions but not in 16. --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index f1675d5b..2c1bbd4c 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - node: [16, 18, 20, 22] + node: [18, 20, 22] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4 From 8feeb0a7cd2ade578aeaefd58d2540dbfbcc1d8d Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:58:19 +0700 Subject: [PATCH 3/4] test: wait for timer to avoid hanging tests --- test.mjs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test.mjs b/test.mjs index cfed3fea..f043cce5 100644 --- a/test.mjs +++ b/test.mjs @@ -106,12 +106,13 @@ const chokidar_watch = (path = currentDir, opts) => { return wt; }; -const waitFor = async (spies) => { +const waitFor = (spies) => { if (spies.length === 0) throw new TypeError('SPies zero'); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('timeout')); }, TEST_TIMEOUT); + let checkTimer; const isSpyReady = (spy) => { if (Array.isArray(spy)) { return spy[0].callCount >= spy[1]; @@ -119,11 +120,13 @@ const waitFor = async (spies) => { return spy.callCount >= 1; }; const checkSpiesReady = () => { + clearTimeout(checkTimer); + if (spies.every(isSpyReady)) { clearTimeout(timeout); resolve(); } else { - setTimeout(checkSpiesReady, 20); + checkTimer = setTimeout(checkSpiesReady, 20); } }; checkSpiesReady(); @@ -1399,10 +1402,13 @@ const runTests = (baseopts) => { await fs_mkdir(testDir, PERM_ARR); const watcher = chokidar_watch('.', options); - setTimeout(() => { - watcher.on(EV.ADD_DIR, spy); - fs_rename(testDir, renamedDir); - }, 1000); + await new Promise((resolve) => { + setTimeout(() => { + watcher.on(EV.ADD_DIR, spy); + fs_rename(testDir, renamedDir); + resolve(); + }, 1000); + }); await waitFor([spy]); spy.should.have.been.calledOnce; From 08561139d1d225b257db1fc5b04d716a03851c91 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:55:43 +0700 Subject: [PATCH 4/4] test: clear check timer to avoid hangs --- test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test.mjs b/test.mjs index f043cce5..8bc1585b 100644 --- a/test.mjs +++ b/test.mjs @@ -109,10 +109,11 @@ const chokidar_watch = (path = currentDir, opts) => { const waitFor = (spies) => { if (spies.length === 0) throw new TypeError('SPies zero'); return new Promise((resolve, reject) => { + let checkTimer; const timeout = setTimeout(() => { + clearTimeout(checkTimer); reject(new Error('timeout')); }, TEST_TIMEOUT); - let checkTimer; const isSpyReady = (spy) => { if (Array.isArray(spy)) { return spy[0].callCount >= spy[1];