diff --git a/package-lock.json b/package-lock.json index 0b7d52a3..837a3586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", + "fast-check": "^3.0.1", "jest": "^28.1.1", "jest-extended": "^3.0.1", "jest-junit": "^14.0.0", @@ -3162,6 +3163,22 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/fast-check": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.1.1.tgz", + "integrity": "sha512-3vtXinVyuUKCKFKYcwXhGE6NtGWkqF8Yh3rvMZNzmwz8EPrgoc/v4pDdLHyLnCyCI5MZpZZkDEwFyXyEONOxpA==", + "dev": true, + "dependencies": { + "pure-rand": "^5.0.1" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5430,6 +5447,16 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.1.tgz", + "integrity": "sha512-ksWccjmXOHU2gJBnH0cK1lSYdvSZ0zLoCMSz/nTGh6hDvCSgcRxDyIcOBD6KNxFz3xhMPm/T267Tbe2JRymKEQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8899,6 +8926,15 @@ "jest-util": "^28.1.3" } }, + "fast-check": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.1.1.tgz", + "integrity": "sha512-3vtXinVyuUKCKFKYcwXhGE6NtGWkqF8Yh3rvMZNzmwz8EPrgoc/v4pDdLHyLnCyCI5MZpZZkDEwFyXyEONOxpA==", + "dev": true, + "requires": { + "pure-rand": "^5.0.1" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10567,6 +10603,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "pure-rand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.1.tgz", + "integrity": "sha512-ksWccjmXOHU2gJBnH0cK1lSYdvSZ0zLoCMSz/nTGh6hDvCSgcRxDyIcOBD6KNxFz3xhMPm/T267Tbe2JRymKEQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 3f5da66b..5e59d200 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", "typedoc": "^0.22.15", - "typescript": "^4.5.2" + "typescript": "^4.5.2", + "fast-check": "^3.0.1" } } diff --git a/tests/EncryptedFS.concurrent.test.ts b/tests/EncryptedFS.concurrent.test.ts index 1da8e900..0b2de4bf 100644 --- a/tests/EncryptedFS.concurrent.test.ts +++ b/tests/EncryptedFS.concurrent.test.ts @@ -6,23 +6,40 @@ import pathNode from 'path'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { code as errno } from 'errno'; import { DB } from '@matrixai/db'; +import * as fc from 'fast-check'; import EncryptedFS from '@/EncryptedFS'; import { ErrorEncryptedFSError } from '@/errors'; import * as utils from '@/utils'; import * as constants from '@/constants'; import INodeManager from '@/inodes/INodeManager'; import { promise } from '@/utils'; -import { expectReason, sleep } from './utils'; +import { expectError, expectReason, sleep } from './utils'; describe(`${EncryptedFS.name} Concurrency`, () => { const logger = new Logger(`${EncryptedFS.name} Concurrency`, LogLevel.WARN, [ new StreamHandler(), ]); const dbKey: Buffer = utils.generateKeySync(256); + const interruptAfterTimeLimit = globalThis.defaultTimeout - 2000; let dataDir: string; let db: DB; let iNodeMgr: INodeManager; let efs: EncryptedFS; + + const scheduleCall = ( + s: fc.Scheduler, + f: () => Promise, + label: string = 'scheduled call', + ) => s.schedule(Promise.resolve(label)).then(() => f()); + + const totalINodes = async (iNodeMgr: INodeManager) => { + let counter = 0; + for await (const _ of iNodeMgr.getAll()) { + counter += 1; + } + return counter; + }; + beforeEach(async () => { dataDir = await fs.promises.mkdtemp( pathNode.join(os.tmpdir(), 'encryptedfs-test-'), @@ -57,6 +74,57 @@ describe(`${EncryptedFS.name} Concurrency`, () => { }); }); describe('concurrent inode creation', () => { + test('EncryptedFS.open, EncryptedFS.mknod and EncryptedFS.mkdir', async () => { + const path1 = pathNode.join('dir', 'file1'); + + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const prom = Promise.allSettled([ + scheduleCall( + s, + () => efs.mknod(path1, constants.S_IFREG, 0, 0), + 'mknod 1', + ), + scheduleCall( + s, + () => efs.mknod(path1, constants.S_IFREG, 0, 0), + 'mknod 2', + ), + scheduleCall( + s, + () => efs.open(path1, constants.O_RDWR | constants.O_CREAT), + 'open 1', + ), + scheduleCall( + s, + () => efs.open(path1, constants.O_RDWR | constants.O_CREAT), + 'open 2', + ), + scheduleCall(s, () => efs.mkdir(path1), 'mkdir 1'), + scheduleCall(s, () => efs.mkdir(path1), 'mkdir 2'), + ]); + await s.waitAll(); + const results = await prom; + results.map((item) => { + if (item.status !== 'fulfilled') { + // Should fail as a normal FS error + expectReason(item, ErrorEncryptedFSError); + } + }); + // Should have at least 1 success + expect(results.some((item) => item.status === 'fulfilled')).toBe( + true, + ); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.open', async () => { // Only one call wins the race to create the file await Promise.all([ @@ -240,120 +308,151 @@ describe(`${EncryptedFS.name} Concurrency`, () => { }); describe('concurrent file writes', () => { test('EncryptedFS.write on multiple file descriptors', async () => { - // Concurrent writes of the same length results in "last write wins" - let fds: Array = [ - await efs.open('test', constants.O_RDWR | constants.O_CREAT), - await efs.open('test', constants.O_RDWR | constants.O_CREAT), - ]; - let contents = ['one', 'two']; - let promises: Array>; - promises = []; - for (let i = 0; i < 2; i++) { - promises.push(efs.write(fds[i], contents[i])); - } - await Promise.all(promises); - expect(['one', 'two']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), - ); - for (const fd of fds) { - await efs.close(fd); - } // Concurrent writes of different length results in "last write wins" or a merge - fds = [ - await efs.open('test', constants.O_RDWR | constants.O_CREAT), - await efs.open('test', constants.O_RDWR | constants.O_CREAT), - ]; - contents = ['one1', 'two']; - promises = []; - for (let i = 0; i < 2; i++) { - promises.push(efs.write(fds[i], contents[i])); - } - expect(['one1', 'two', 'two1']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), + const contents = ['one', 'two', 'one1', 'two2']; + await fc.assert( + fc.asyncProperty(fc.scheduler(), async (s) => { + const fds: Array = [ + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + ]; + + // Concurrent writes of the same length results in "last write wins" + const prom = Promise.all([ + scheduleCall(s, () => efs.write(fds[0], contents[0]), 'write 1'), + scheduleCall(s, () => efs.write(fds[1], contents[1]), 'write 2'), + scheduleCall(s, () => efs.write(fds[2], contents[2]), 'write 3'), + scheduleCall(s, () => efs.write(fds[3], contents[3]), 'write 4'), + ]); + await s.waitAll(); + await prom; + + expect(['one', 'two', 'one1', 'one2', 'two2', 'two1']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + for (const fd of fds) { + await efs.close(fd); + } + }), + { numRuns: 50, interruptAfterTimeLimit }, ); - for (const fd of fds) { - await efs.close(fd); - } }); test('EncryptedFS.write on the same file descriptor', async () => { - await efs.writeFile('test', ''); - const fd = await efs.open('test', 'w'); - await Promise.all([ - efs.write(fd, Buffer.from('aaa')), - efs.write(fd, Buffer.from('bbb')), - ]); - expect(['aaabbb', 'bbbaaa']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), + await fc.assert( + fc.asyncProperty(fc.scheduler(), async (s) => { + await efs.writeFile('test', ''); + const fd = await efs.open('test', 'w'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.write(fd, 'aaa'), 'write 1'), + scheduleCall(s, () => efs.write(fd, 'bbb'), 'write 2'), + ]); + await s.waitAll(); + await prom; + + expect(['aaabbb', 'bbbaaa']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + await efs.close(fd); + }), + { numRuns: 20, interruptAfterTimeLimit }, ); - await efs.close(fd); }); test('EncryptedFS.writeFile', async () => { - let promises: Array>; - // Concurrent writes of the same length results in "last write wins" - promises = []; - for (const data of ['one', 'two']) { - promises.push(efs.writeFile('test', data)); - } - await Promise.all(promises); - expect(['one', 'two']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), - ); - // Concurrent writes of different length results in "last write wins" or a merge - for (let i = 0; i < 10; i++) { - promises = []; - for (const data of ['one1', 'two']) { - promises.push(efs.writeFile('test', data)); - } - await Promise.all(promises); - expect(['one1', 'two', 'two1']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), - ); - } - // Explicit last write wins - promises = [ - (async () => { - // One is written last - await sleep(0); - return efs.writeFile('test', 'one'); - })(), - efs.writeFile('test', 'two'), - ]; - await Promise.all(promises); - expect(['one']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), - ); - promises = [ - efs.writeFile('test', 'one'), - (async () => { - // Two1 is written last - await sleep(0); - return efs.writeFile('test', 'two1'); - })(), - ]; - await Promise.all(promises); - expect(['two1']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), + await fc.assert( + fc.asyncProperty(fc.scheduler(), async (s) => { + // Concurrent writes of different length results in "last write wins" or a merge + await efs.writeFile('test', ''); + + const prom = Promise.all([ + scheduleCall(s, () => efs.writeFile('test', 'one'), 'writeFile 1'), + scheduleCall(s, () => efs.writeFile('test', 'one1'), 'writeFile 2'), + scheduleCall(s, () => efs.writeFile('test', 'two'), 'writeFile 2'), + scheduleCall(s, () => efs.writeFile('test', 'two2'), 'writeFile 2'), + ]); + await s.waitAll(); + await prom; + + expect(['one', 'two', 'one1', 'one2', 'two2', 'two1']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + expect(await totalINodes(iNodeMgr)).toEqual(2); + }), + { numRuns: 50, interruptAfterTimeLimit }, ); - const inodeDatas: Array = []; - for await (const inodeData of iNodeMgr.getAll()) { - inodeDatas.push(inodeData); - } - expect(inodeDatas).toStrictEqual([ - { ino: 1, type: 'Directory', gc: false }, - { ino: 2, type: 'File', gc: false }, - ]); }); test('EncryptedFS.appendFile', async () => { - await efs.writeFile('test', 'original'); - // Concurrent appends results in mutually exclusive writes - const promises = [ - efs.appendFile('test', 'one'), - efs.appendFile('test', 'two'), - ]; - await Promise.all(promises); - // Either order of appending is acceptable - expect(['originalonetwo', 'originaltwoone']).toContainEqual( - await efs.readFile('test', { encoding: 'utf-8' }), + await fc.assert( + fc.asyncProperty(fc.scheduler(), async (s) => { + // Concurrent appends results in mutually exclusive writes + await efs.writeFile('test', 'original'); + + const prom = Promise.all([ + scheduleCall( + s, + () => efs.appendFile('test', 'one'), + 'appendFile 1', + ), + scheduleCall( + s, + () => efs.appendFile('test', 'two'), + 'appendFile 2', + ), + ]); + await s.waitAll(); + await prom; + + // Either order of appending is acceptable + expect(['originalonetwo', 'originaltwoone']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + }), + { numRuns: 20, interruptAfterTimeLimit }, + ); + }); + test('EncryptedFS.fallocate, EncryptedFS.writeFile, EncryptedFS.write and EncryptedFS.createWriteStream ', async () => { + const path1 = pathNode.join('dir', 'file1'); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.fallocate(fd, 0, 100), 'fallocate'), + scheduleCall(s, () => efs.writeFile(path1, 'test'), 'writeFile'), + scheduleCall(s, () => efs.write(fd, 'test'), 'write'), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(i.toString()); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream', + ), + ]); + await s.waitAll(); + await prom; + expect((await efs.stat(path1)).size).toBe(100); + const contents = await efs.readFile(path1); + expect(contents.length).toBeGreaterThanOrEqual(4); + expect(contents.length).toBeLessThanOrEqual(100); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, ); }); test('EncryptedFS.fallocate and EncryptedFS.writeFile', async () => { @@ -656,6 +755,50 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(contents.length).toEqual(10); } }); + test('EncryptedFS.truncate and EncryptedFS.writeFile, EncryptedFS.write and EncryptedFS.createWriteStream', async () => { + const path1 = pathNode.join('dir', 'file1'); + const phrase = 'The quick brown fox jumped over the lazy dog'; + const phraseSplit = phrase.split(' '); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.truncate(path1, 27), 'truncate'), + scheduleCall(s, () => efs.writeFile(path1, phrase), 'writeFile'), + scheduleCall(s, () => efs.write(fd, phrase), 'write'), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phraseSplit) { + writeStream.write(i + ' '); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream', + ), + ]); + await s.waitAll(); + await prom; + const contents = await efs.readFile(path1); + expect(contents.length).toBeGreaterThanOrEqual(27); + expect(contents.length).toBeLessThanOrEqual(45); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.truncate and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); await efs.mkdir('dir'); @@ -965,6 +1108,50 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(contents.length).toEqual(27); } }); + test('EncryptedFS.ftruncate and EncryptedFS.writeFile, EncryptedFS.write and EncryptedFS.createWriteStream', async () => { + const path1 = pathNode.join('dir', 'file1'); + const phrase = 'The quick brown fox jumped over the lazy dog'; + const phraseSplit = phrase.split(' '); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.ftruncate(fd, 27), 'ftruncate'), + scheduleCall(s, () => efs.writeFile(path1, phrase), 'writeFile'), + scheduleCall(s, () => efs.write(fd, phrase), 'write'), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phraseSplit) { + writeStream.write(i + ' '); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream', + ), + ]); + await s.waitAll(); + await prom; + const contents = await efs.readFile(path1); + expect(contents.length).toBeGreaterThanOrEqual(27); + expect(contents.length).toBeLessThanOrEqual(45); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.ftruncate and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); await efs.mkdir('dir'); @@ -1274,6 +1461,33 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(contents.length).toEqual(27); } }); + test('EncryptedFS.utimes, EncryptedFS.futimes and EncryptedFS.writeFile', async () => { + const path1 = pathNode.join('dir', 'file1'); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile(fd, 'test'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.utimes(path1, 0, 0), 'utimes file'), + scheduleCall(s, () => efs.utimes('dir', 0, 0), 'utimes dir'), + scheduleCall(s, () => efs.futimes(fd, 0, 0), 'futimes'), + scheduleCall(s, () => efs.writeFile(path1, 'test'), 'writeFile'), + ]); + await s.waitAll(); + await prom; + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.utimes and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); const nowTime = Date.now(); @@ -1428,6 +1642,62 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); } }); + test('EncryptedFS.lseek, EncryptedFS.writeFile, EncryptedFS.writeFile with fd, EncryptedFS.write, EncryptedFS.readFile, EncryptedFS.read, and seeking position', async () => { + const path1 = pathNode.join('dir', 'file1'); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + + const buffer = Buffer.alloc(45); + const prom = Promise.all([ + scheduleCall( + s, + () => efs.lseek(fd, 20, constants.SEEK_CUR), + 'seek move', + ), + scheduleCall( + s, + () => efs.writeFile(path1, 'test'), + 'writeFile path', + ), + scheduleCall(s, () => efs.writeFile(fd, 'test'), 'writeFile fd'), + scheduleCall(s, () => efs.write(fd, 'test'), 'write'), + scheduleCall(s, () => efs.readFile(path1), 'readFile'), + scheduleCall( + s, + () => efs.read(fd, buffer, undefined, 44), + 'read', + ), + scheduleCall( + s, + () => efs.lseek(fd, 15, constants.SEEK_SET), + 'seek set', + ), + ]); + await s.waitAll(); + await prom; + const stat = await efs.stat(path1); + expect(stat.size).toBeGreaterThanOrEqual(4); + expect(stat.size).toBeLessThanOrEqual(80); + const contents = await efs.readFile(path1); + expect(contents.length).toBeGreaterThanOrEqual(4); + expect(contents.length).toBeLessThanOrEqual(80); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.lseek and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); await efs.mkdir('dir'); @@ -1756,6 +2026,110 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(pos).toEqual(15); } }); + test('EncryptedFS.createReadStream, EncryptedFS.createWriteStream, EncryptedFS.write, EncryptedFS.read', async () => { + const path1 = pathNode.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'.repeat(5); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile(path1, dataB); + + const buffer = Buffer.alloc(110); + const prom = Promise.all([ + scheduleCall( + s, + async () => { + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); + }); + return await readProm; + }, + 'readStream 1', + ), + scheduleCall( + s, + async () => { + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); + }); + return await readProm; + }, + 'readStream 2', + ), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream 1', + ), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream 2', + ), + scheduleCall(s, () => efs.write(fd, dataB), 'write'), + scheduleCall( + s, + () => efs.read(fd, buffer, undefined, 100), + 'read', + ), + ]); + await s.waitAll(); + await prom; + const stat = await efs.stat(path1); + expect(stat.size).toEqual(50); + const contents = await efs.readFile(path1); + expect(contents.length).toEqual(50); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.createReadStream and EncryptedFS.createWriteStream', async () => { const path1 = utils.pathJoin('dir', 'file1'); const dataA = 'AAAAA'; @@ -2205,6 +2579,63 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(stat.size).toEqual(50); } }); + test('EncryptedFS.unlink, EncryptedFS.writeFile, EncryptedFS.open, EncryptedFS.write and EncryptedFS.createWriteStream', async () => { + const path1 = pathNode.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'.repeat(5); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile(path1, dataB); + + const prom = Promise.all([ + scheduleCall(s, () => efs.unlink(path1), 'unlink'), + scheduleCall(s, () => efs.writeFile(path1, 'test'), 'writeFile'), + scheduleCall( + s, + async () => { + let fd: FdIndex; + try { + fd = await efs.open(path1, 'r+'); + await efs.close(fd!); + } catch (e) { + // Ignore FS errors + if (!(e instanceof ErrorEncryptedFSError)) throw e; + } + }, + 'open', + ), + scheduleCall(s, () => efs.write(fd, 'test'), 'write'), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream', + ), + ]); + await s.waitAll(); + // Expecting no transaction errors + await prom; + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.unlink and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); await efs.mkdir('dir'); @@ -2430,6 +2861,63 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(await efs.exists(path1)).toEqual(false); } }); + test('EncryptedFS.appendFIle, EncryptedFS.writeFile, EncryptedFS.writeFile with fd, EncryptedFS.write, EncryptedFS.createReadStream', async () => { + const path1 = pathNode.join('dir', 'file1'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile(path1, ''); + + const prom = Promise.all([ + scheduleCall( + s, + () => efs.appendFile(path1, dataA), + 'appendFile path', + ), + scheduleCall(s, () => efs.appendFile(fd, dataA), 'appendFile fd'), + scheduleCall( + s, + () => efs.writeFile(path1, dataB), + 'writeFile path', + ), + scheduleCall(s, () => efs.writeFile(fd, dataB), 'writeFile fd'), + scheduleCall(s, () => efs.write(fd, dataB), 'write'), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'readStream', + ), + ]); + await s.waitAll(); + // Expecting no transaction errors + await prom; + const stat = await efs.stat(path1); + expect(stat.size).toEqual(100); + const contents = await efs.readFile(path1); + expect(contents.length).toEqual(100); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.appendFIle and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); const dataA = 'A'.repeat(10); @@ -2726,6 +3214,60 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(stat.size).toEqual(50); } }); + test('EncryptedFS.copyFile, EncryptedFS.writeFile, EncryptedFS.writeFile with fd, EncryptedFS.write and EncryptedFS.createWriteStream', async () => { + const path1 = pathNode.join('dir', 'file1'); + const path2 = utils.pathJoin('dir', 'file2'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile(path1, ''); + await efs.writeFile(path1, dataA); + + const prom = Promise.all([ + scheduleCall(s, () => efs.copyFile(path1, path2), 'copyFile'), + scheduleCall( + s, + () => efs.writeFile(path1, dataB), + 'writeFile path', + ), + scheduleCall(s, () => efs.writeFile(fd, dataB), 'writeFile fd'), + scheduleCall(s, () => efs.write(fd, dataB), 'write'), + scheduleCall( + s, + async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + }, + 'writeStream', + ), + ]); + await s.waitAll(); + // Expecting no transaction errors + await prom; + const stat = await efs.stat(path1); + expect(stat.size).toEqual(100); + const contents = await efs.readFile(path1); + expect(contents.length).toEqual(100); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.copyFile and EncryptedFS.writeFile', async () => { const path1 = utils.pathJoin('dir', 'file1'); const path2 = utils.pathJoin('dir', 'file2'); @@ -3022,59 +3564,90 @@ describe(`${EncryptedFS.name} Concurrency`, () => { expect(stat.size).toBeLessThanOrEqual(50); }); test('EncryptedFS.readFile and EncryptedFS.writeFile', async () => { - const path1 = utils.pathJoin('dir', 'file1'); - await efs.mkdir('dir'); + const path1 = pathNode.join('dir', 'file1'); const dataA = 'AAAAA'; const dataB = 'BBBBB'; - await efs.writeFile(path1, dataA); - - let results = await Promise.allSettled([ - (async () => { - return await efs.writeFile(path1, dataB); - })(), - (async () => { - return (await efs.readFile(path1)).toString(); - })(), - ]); - - if (results[1].status === 'fulfilled' && results[1].value[0] === 'A') { - expect(results).toStrictEqual([ - { status: 'fulfilled', value: undefined }, - { status: 'fulfilled', value: dataA }, - ]); - } else { - expect(results).toStrictEqual([ - { status: 'fulfilled', value: undefined }, - { status: 'fulfilled', value: dataB }, - ]); - } - - // Cleaning up - await efs.rmdir('dir', { recursive: true }); - await efs.mkdir('dir'); - await efs.writeFile(path1, dataA); - - results = await Promise.allSettled([ - (async () => { - await sleep(100); - return await efs.writeFile(path1, dataB); - })(), - (async () => { - return (await efs.readFile(path1)).toString(); - })(), - ]); - - if (results[1].status === 'fulfilled' && results[1].value[0] === 'A') { - expect(results).toStrictEqual([ - { status: 'fulfilled', value: undefined }, - { status: 'fulfilled', value: dataA }, - ]); - } else { - expect(results).toStrictEqual([ - { status: 'fulfilled', value: undefined }, - { status: 'fulfilled', value: dataB }, - ]); - } + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + + const prom = Promise.allSettled([ + scheduleCall(s, () => efs.writeFile(path1, dataB), 'writeFile'), + scheduleCall( + s, + async () => { + return (await efs.readFile(path1)).toString(); + }, + 'readFile', + ), + ]); + await s.waitAll(); + // Expecting no transaction errors + const results = await prom; + if ( + results[1].status === 'fulfilled' && + results[1].value[0] === 'A' + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: dataA }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: dataB }, + ]); + } + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { numRuns: 20, interruptAfterTimeLimit }, + ); + }); + test('EncryptedFS.read and EncryptedFS.write', async () => { + const path1 = pathNode.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'; + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + const fd1 = await efs.open(path1, 'r+'); + const fd2 = await efs.open(path1, 'r+'); + const buffer1 = Buffer.alloc(100); + const buffer2 = Buffer.alloc(100); + + const prom = Promise.all([ + scheduleCall(s, () => efs.write(fd1, dataB), 'write fd1'), + scheduleCall( + s, + () => efs.read(fd1, buffer1, undefined, 100), + 'read fd1', + ), + scheduleCall( + s, + () => efs.read(fd2, buffer2, undefined, 100), + 'read fd2', + ), + ]); + await s.waitAll(); + // Expecting no transaction errors + await prom; + + await efs.close(fd1); + await efs.close(fd2); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { numRuns: 20, interruptAfterTimeLimit }, + ); }); test('EncryptedFS.read and EncryptedFS.write with different fd', async () => { const path1 = utils.pathJoin('dir', 'file1'); @@ -3215,6 +3788,41 @@ describe(`${EncryptedFS.name} Concurrency`, () => { }); }); describe('concurrent directory manipulation', () => { + test('EncryptedFS.mkdir, recursive and rename', async () => { + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + const prom = Promise.all([ + scheduleCall(s, () => efs.mkdir('dir'), 'mkdir 1'), + scheduleCall(s, () => efs.mkdir('dir'), 'mkdir 2'), + scheduleCall( + s, + () => efs.mkdir('dir/dira/dirb', { recursive: true }), + 'mkdir recursive 1', + ), + scheduleCall( + s, + () => efs.mkdir('dir/dira/dirb', { recursive: true }), + 'mkdir recursive 2', + ), + scheduleCall(s, () => efs.rename('dir', 'one'), 'rename 1'), + scheduleCall(s, () => efs.rename('dir', 'two'), 'rename 2'), + ]); + await s.waitAll(); + // Expecting no transaction errors + try { + await prom; + } catch (e) { + if (!(e instanceof ErrorEncryptedFSError)) throw e; + } + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.mkdir', async () => { const results = await Promise.allSettled([ efs.mkdir('dir'), @@ -3288,6 +3896,35 @@ describe(`${EncryptedFS.name} Concurrency`, () => { ).toBe(true); expect(await efs.readdir('.')).toContain('one'); }); + test('EncryptedFS.readdir, EncryptedFS.rmdir, EncryptedFS.mkdir, EncryptedFS.writeFile and EncryptedFS.rename', async () => { + const path1 = utils.pathJoin('dir', 'file1'); + const path2 = utils.pathJoin('dir', 'file2'); + + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + const prom = Promise.all([ + scheduleCall(s, () => efs.readdir('dir'), 'readdir'), + scheduleCall( + s, + () => efs.rmdir('dir', { recursive: true }), + 'rmdir', + ), + scheduleCall(s, () => efs.mkdir(path1), 'mkdir'), + scheduleCall(s, () => efs.writeFile(path1, 'test'), 'writeFile'), + scheduleCall(s, () => efs.rename(path1, path2), 'rename'), + ]); + await s.waitAll(); + // Expecting no transaction errors + await expectError(prom, ErrorEncryptedFSError); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.readdir and EncryptedFS.rmdir', async () => { await efs.mkdir('dir'); // It is possible for only one to succeed or both can succeed @@ -3621,68 +4258,83 @@ describe(`${EncryptedFS.name} Concurrency`, () => { } }); test('EncryptedFS.rmdir and EncryptedFS.rename', async () => { - const PATH1 = utils.pathJoin('dir', 'p1'); - const PATH2 = utils.pathJoin('dir', 'p2'); - await efs.mkdir('dir'); - await efs.mkdir(PATH1); - - // Directories - let results = await Promise.allSettled([ - (async () => { - return await efs.rmdir(PATH1); - })(), - (async () => { - return await efs.rename(PATH1, PATH2); - })(), - ]); - if ( - results[0].status === 'fulfilled' && - results[1].status === 'rejected' - ) { - expect(results[0]).toStrictEqual({ - status: 'fulfilled', - value: undefined, - }); - expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); - } else { - expectReason(results[0], ErrorEncryptedFSError, errno.ENOENT); - expect(results[1]).toStrictEqual({ - status: 'fulfilled', - value: undefined, - }); - } - await efs.rmdir('dir', { recursive: true }); - await efs.mkdir('dir'); - await efs.mkdir(PATH1); + const path1 = utils.pathJoin('dir', 'file1'); + const path2 = utils.pathJoin('dir', 'file2'); - results = await Promise.allSettled([ - (async () => { - await sleep(100); - return await efs.rmdir(PATH1); - })(), - (async () => { - return await efs.rename(PATH1, PATH2); - })(), - ]); - if ( - results[0].status === 'fulfilled' && - results[1].status === 'rejected' - ) { - expect(results[0]).toStrictEqual({ - status: 'fulfilled', - value: undefined, - }); - expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); - } else { - expectReason(results[0], ErrorEncryptedFSError, errno.ENOENT); - expect(results[1]).toStrictEqual({ - status: 'fulfilled', - value: undefined, - }); - } + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + await efs.mkdir(path1); + + const prom = Promise.allSettled([ + scheduleCall(s, () => efs.rmdir(path1), 'rmdir'), + scheduleCall(s, () => efs.rename(path1, path2), 'rename'), + ]); + await s.waitAll(); + // Expecting no transaction errors + const results = await prom; + if ( + results[0].status === 'fulfilled' && + results[1].status === 'rejected' + ) { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.ENOENT); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { numRuns: 20, interruptAfterTimeLimit }, + ); }); }); describe('concurrent symlinking', () => { + test('EncryptedFS.symlink, EncryptedFS.symlink and EncryptedFS.mknod', async () => { + const path1 = utils.pathJoin('dir', 'file1'); + const path2 = utils.pathJoin('dir', 'file2'); + + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + const fd = await efs.open(path1, 'r+'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.symlink(path1, path2), 'symlink 1'), + scheduleCall(s, () => efs.symlink(path1, path2), 'symlink 2'), + scheduleCall( + s, + () => efs.mknod(path2, constants.S_IFREG, 0, 0), + 'mknod', + ), + scheduleCall(s, () => efs.mkdir(path2), 'mkdir'), + scheduleCall(s, () => efs.write(fd, 'test'), 'write'), + ]); + await s.waitAll(); + // Expecting no transaction errors + await expectError(prom, ErrorEncryptedFSError); + + await efs.close(fd); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.symlink and EncryptedFS.symlink', async () => { const path1 = utils.pathJoin('dir', 'file1'); const path2 = utils.pathJoin('dir', 'file2'); @@ -3894,6 +4546,32 @@ describe(`${EncryptedFS.name} Concurrency`, () => { }); }); describe('concurrent inode linking and unlinking', () => { + test('EncryptedFS.link, EncryptedFS.link and EncryptedFS.symlink', async () => { + const path1 = utils.pathJoin('dir', 'file1'); + const path2 = utils.pathJoin('dir', 'file2'); + + await fc.assert( + fc + .asyncProperty(fc.scheduler(), async (s) => { + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + const prom = Promise.all([ + scheduleCall(s, () => efs.link(path1, path2), 'link 1'), + scheduleCall(s, () => efs.link(path1, path2), 'link 2'), + scheduleCall(s, () => efs.symlink(path1, path2), 'symlink'), + ]); + await s.waitAll(); + // Expecting no transaction errors + await expectError(prom, ErrorEncryptedFSError); + }) + .afterEach(async () => { + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + }), + { numRuns: 20, interruptAfterTimeLimit }, + ); + }); test('EncryptedFS.link and EncryptedFS.link', async () => { const path1 = utils.pathJoin('dir', 'file1'); const path2 = utils.pathJoin('dir', 'file2');