|
1 | | -import fs from 'node:fs'; |
2 | | -import test from 'ava'; |
3 | | -import tempy from 'tempy'; |
| 1 | +import fs, {promises as fsPromises} from 'node:fs'; |
| 2 | +import path from 'node:path'; |
| 3 | +import {fileURLToPath} from 'node:url'; |
| 4 | +import {test} from 'node:test'; |
| 5 | +import assert from 'node:assert/strict'; |
| 6 | +import {setTimeout as delay} from 'node:timers/promises'; |
| 7 | +import {tmpdir} from 'node:os'; |
4 | 8 | import {writeJsonFile, writeJsonFileSync} from './index.js'; |
5 | 9 |
|
6 | | -test('async', async t => { |
7 | | - const temporaryFile = tempy.file(); |
8 | | - await writeJsonFile(temporaryFile, {foo: true}, {indent: 2}); |
9 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true\n}\n'); |
| 10 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 11 | + |
| 12 | +function temporaryFile() { |
| 13 | + return path.join(tmpdir(), `write-json-file-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`); |
| 14 | +} |
| 15 | + |
| 16 | +test('async', async () => { |
| 17 | + const temporaryFilePath = temporaryFile(); |
| 18 | + await writeJsonFile(temporaryFilePath, {foo: true}, {indent: 2}); |
| 19 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true\n}\n'); |
| 20 | + fs.unlinkSync(temporaryFilePath); |
| 21 | +}); |
| 22 | + |
| 23 | +test('sync', () => { |
| 24 | + const temporaryFilePath = temporaryFile(); |
| 25 | + writeJsonFileSync(temporaryFilePath, {foo: true}, {detectIndent: true, indent: 2}); |
| 26 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true\n}\n'); |
| 27 | + fs.unlinkSync(temporaryFilePath); |
10 | 28 | }); |
11 | 29 |
|
12 | | -test('sync', t => { |
13 | | - const temporaryFile = tempy.file(); |
14 | | - writeJsonFileSync(temporaryFile, {foo: true}, {detectIndent: true, indent: 2}); |
15 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true\n}\n'); |
| 30 | +test('detect indent', async () => { |
| 31 | + const temporaryFilePath = temporaryFile(); |
| 32 | + await writeJsonFile(temporaryFilePath, {foo: true}, {indent: 2}); |
| 33 | + await writeJsonFile(temporaryFilePath, {foo: true, bar: true, foobar: true}, {detectIndent: true}); |
| 34 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n'); |
| 35 | + fs.unlinkSync(temporaryFilePath); |
16 | 36 | }); |
17 | 37 |
|
18 | | -test('detect indent', async t => { |
19 | | - const temporaryFile = tempy.file(); |
20 | | - await writeJsonFile(temporaryFile, {foo: true}, {indent: 2}); |
21 | | - await writeJsonFile(temporaryFile, {foo: true, bar: true, foobar: true}, {detectIndent: true}); |
22 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n'); |
| 38 | +test('detect indent synchronously', () => { |
| 39 | + const temporaryFilePath = temporaryFile(); |
| 40 | + writeJsonFileSync(temporaryFilePath, {foo: true}, {indent: 2}); |
| 41 | + writeJsonFileSync(temporaryFilePath, {foo: true, bar: true, foobar: true}, {detectIndent: true}); |
| 42 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n'); |
| 43 | + fs.unlinkSync(temporaryFilePath); |
23 | 44 | }); |
24 | 45 |
|
25 | | -test('detect indent synchronously', t => { |
26 | | - const temporaryFile = tempy.file(); |
27 | | - writeJsonFileSync(temporaryFile, {foo: true}, {indent: 2}); |
28 | | - writeJsonFileSync(temporaryFile, {foo: true, bar: true, foobar: true}, {detectIndent: true}); |
29 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n'); |
| 46 | +test('fall back to default indent if file doesn\'t exist', async () => { |
| 47 | + const temporaryFilePath = temporaryFile(); |
| 48 | + await writeJsonFile(temporaryFilePath, {foo: true, bar: true, foobar: true}, {detectIndent: true}); |
| 49 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"foo": true,\n\t"bar": true,\n\t"foobar": true\n}\n'); |
| 50 | + fs.unlinkSync(temporaryFilePath); |
30 | 51 | }); |
31 | 52 |
|
32 | | -test('fall back to default indent if file doesn\'t exist', async t => { |
33 | | - const temporaryFile = tempy.file(); |
34 | | - await writeJsonFile(temporaryFile, {foo: true, bar: true, foobar: true}, {detectIndent: true}); |
35 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"foo": true,\n\t"bar": true,\n\t"foobar": true\n}\n'); |
| 53 | +test('async - {sortKeys: true}', async () => { |
| 54 | + const temporaryFilePath = temporaryFile(); |
| 55 | + await writeJsonFile(temporaryFilePath, {c: true, b: true, a: true}, {sortKeys: true}); |
| 56 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"a": true,\n\t"b": true,\n\t"c": true\n}\n'); |
| 57 | + |
| 58 | + await writeJsonFile(temporaryFilePath, ['c', 'b', 'a'], {sortKeys: true}); |
| 59 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '[\n\t"c",\n\t"b",\n\t"a"\n]\n'); |
| 60 | + fs.unlinkSync(temporaryFilePath); |
36 | 61 | }); |
37 | 62 |
|
38 | | -test('async - {sortKeys: true}', async t => { |
39 | | - const temporaryFile = tempy.file(); |
40 | | - await writeJsonFile(temporaryFile, {c: true, b: true, a: true}, {sortKeys: true}); |
41 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"a": true,\n\t"b": true,\n\t"c": true\n}\n'); |
| 63 | +test('async - {sortKeys: false}', async () => { |
| 64 | + const temporaryFilePath = temporaryFile(); |
| 65 | + await writeJsonFile(temporaryFilePath, {c: true, b: true, a: true}, {sortKeys: false}); |
| 66 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"c": true,\n\t"b": true,\n\t"a": true\n}\n'); |
| 67 | + fs.unlinkSync(temporaryFilePath); |
| 68 | +}); |
42 | 69 |
|
43 | | - await writeJsonFile(temporaryFile, ['c', 'b', 'a'], {sortKeys: true}); |
44 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '[\n\t"c",\n\t"b",\n\t"a"\n]\n'); |
| 70 | +test('async - `replacer` option', async () => { |
| 71 | + const temporaryFilePath = temporaryFile(); |
| 72 | + await writeJsonFile(temporaryFilePath, {foo: true, bar: true}, {replacer: ['foo']}); |
| 73 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"foo": true\n}\n'); |
| 74 | + fs.unlinkSync(temporaryFilePath); |
45 | 75 | }); |
46 | 76 |
|
47 | | -test('async - {sortKeys: false}', async t => { |
48 | | - const temporaryFile = tempy.file(); |
49 | | - await writeJsonFile(temporaryFile, {c: true, b: true, a: true}, {sortKeys: false}); |
50 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"c": true,\n\t"b": true,\n\t"a": true\n}\n'); |
| 77 | +test('sync - `replacer` option', () => { |
| 78 | + const temporaryFilePath = temporaryFile(); |
| 79 | + writeJsonFileSync(temporaryFilePath, {foo: true, bar: true}, {replacer: ['foo']}); |
| 80 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"foo": true\n}\n'); |
| 81 | + fs.unlinkSync(temporaryFilePath); |
51 | 82 | }); |
52 | 83 |
|
53 | | -test('async - `replacer` option', async t => { |
54 | | - const temporaryFile = tempy.file(); |
55 | | - await writeJsonFile(temporaryFile, {foo: true, bar: true}, {replacer: ['foo']}); |
56 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"foo": true\n}\n'); |
| 84 | +test('async - respect trailing newline at the end of the file', async () => { |
| 85 | + const temporaryFilePath = temporaryFile(); |
| 86 | + fs.writeFileSync(temporaryFilePath, JSON.stringify({foo: true})); |
| 87 | + await writeJsonFile(temporaryFilePath, {bar: true}); |
| 88 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"bar": true\n}'); |
| 89 | + fs.unlinkSync(temporaryFilePath); |
57 | 90 | }); |
58 | 91 |
|
59 | | -test('sync - `replacer` option', t => { |
60 | | - const temporaryFile = tempy.file(); |
61 | | - writeJsonFileSync(temporaryFile, {foo: true, bar: true}, {replacer: ['foo']}); |
62 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"foo": true\n}\n'); |
| 92 | +test('sync - respect trailing newline at the end of the file', () => { |
| 93 | + const temporaryFilePath = temporaryFile(); |
| 94 | + fs.writeFileSync(temporaryFilePath, JSON.stringify({foo: true})); |
| 95 | + writeJsonFileSync(temporaryFilePath, {bar: true}); |
| 96 | + assert.equal(fs.readFileSync(temporaryFilePath, 'utf8'), '{\n\t"bar": true\n}'); |
| 97 | + fs.unlinkSync(temporaryFilePath); |
63 | 98 | }); |
64 | 99 |
|
65 | | -test('async - respect trailing newline at the end of the file', async t => { |
66 | | - const temporaryFile = tempy.file(); |
67 | | - fs.writeFileSync(temporaryFile, JSON.stringify({foo: true})); |
68 | | - await writeJsonFile(temporaryFile, {bar: true}); |
69 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"bar": true\n}'); |
| 100 | +test('concurrent writes', async () => { |
| 101 | + /* eslint-disable no-await-in-loop */ |
| 102 | + const testDir = path.join(__dirname, 'test-temp-concurrent'); |
| 103 | + const targetFile = path.join(testDir, 'concurrent-test.json'); |
| 104 | + const sourceFile = path.join(testDir, 'source.json'); |
| 105 | + |
| 106 | + await fsPromises.rm(testDir, {recursive: true, force: true}); |
| 107 | + await fsPromises.mkdir(testDir, {recursive: true}); |
| 108 | + |
| 109 | + await fsPromises.writeFile(sourceFile, JSON.stringify({test: 'data', value: 1})); |
| 110 | + |
| 111 | + const errors = []; |
| 112 | + let successCount = 0; |
| 113 | + |
| 114 | + async function worker(id) { |
| 115 | + for (let index = 0; index < 100; index++) { |
| 116 | + try { |
| 117 | + const data = JSON.parse(await fsPromises.readFile(sourceFile, 'utf8')); |
| 118 | + data.worker = id; |
| 119 | + data.iteration = index; |
| 120 | + await writeJsonFile(targetFile, data); |
| 121 | + successCount++; |
| 122 | + } catch (error) { |
| 123 | + errors.push({ |
| 124 | + worker: id, iteration: index, error: error.message, code: error.code, |
| 125 | + }); |
| 126 | + } |
| 127 | + |
| 128 | + await delay(Math.random() * 10); |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + const workers = []; |
| 133 | + for (let index = 0; index < 5; index++) { |
| 134 | + workers.push(worker(index)); |
| 135 | + } |
| 136 | + |
| 137 | + await Promise.all(workers); |
| 138 | + |
| 139 | + assert.equal(successCount, 500, `Expected 500 successful writes, got ${successCount}`); |
| 140 | + assert.equal(errors.length, 0, `Expected no errors, got ${errors.length}`); |
| 141 | + |
| 142 | + await fsPromises.rm(testDir, {recursive: true, force: true}); |
| 143 | + /* eslint-enable no-await-in-loop */ |
70 | 144 | }); |
71 | 145 |
|
72 | | -test('sync - respect trailing newline at the end of the file', t => { |
73 | | - const temporaryFile = tempy.file(); |
74 | | - fs.writeFileSync(temporaryFile, JSON.stringify({foo: true})); |
75 | | - writeJsonFileSync(temporaryFile, {bar: true}); |
76 | | - t.is(fs.readFileSync(temporaryFile, 'utf8'), '{\n\t"bar": true\n}'); |
| 146 | +test('aggressive concurrent writes', async () => { |
| 147 | + /* eslint-disable no-await-in-loop */ |
| 148 | + const testDir = path.join(__dirname, 'test-temp-aggressive'); |
| 149 | + const sourceFile = path.join(testDir, 'bar.json'); |
| 150 | + const targetFile = path.join(testDir, 'foo.json'); |
| 151 | + |
| 152 | + await fsPromises.rm(testDir, {recursive: true, force: true}); |
| 153 | + await fsPromises.mkdir(testDir, {recursive: true}); |
| 154 | + |
| 155 | + await fsPromises.writeFile(sourceFile, JSON.stringify({test: 'data', value: 1})); |
| 156 | + |
| 157 | + let errorCount = 0; |
| 158 | + let successCount = 0; |
| 159 | + const maxIterations = 100; |
| 160 | + |
| 161 | + async function run() { |
| 162 | + await delay(10); |
| 163 | + const file = await fsPromises.readFile(sourceFile, 'utf8'); |
| 164 | + const data = JSON.parse(file); |
| 165 | + await writeJsonFile(targetFile, data); |
| 166 | + } |
| 167 | + |
| 168 | + async function worker() { |
| 169 | + for (let index = 0; index < maxIterations; index++) { |
| 170 | + try { |
| 171 | + await run(); |
| 172 | + successCount++; |
| 173 | + } catch { |
| 174 | + errorCount++; |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + const workers = []; |
| 180 | + for (let index = 0; index < 3; index++) { |
| 181 | + workers.push(worker()); |
| 182 | + } |
| 183 | + |
| 184 | + await Promise.all(workers); |
| 185 | + |
| 186 | + assert.equal(successCount, maxIterations * 3, `Expected ${maxIterations * 3} successful writes, got ${successCount}`); |
| 187 | + assert.equal(errorCount, 0, `Expected no errors, got ${errorCount}`); |
| 188 | + |
| 189 | + await fsPromises.rm(testDir, {recursive: true, force: true}); |
| 190 | + /* eslint-enable no-await-in-loop */ |
77 | 191 | }); |
0 commit comments