Skip to content

Commit

Permalink
fix: fix CoW in Windows (#2)
Browse files Browse the repository at this point in the history
* fix: fix CoW in Windows

* style: remove unused package

* test: clone less times in parallel

* style: remove identation change

* Apply suggestions from code review

Co-authored-by: Khải <[email protected]>

* types: fix types issues

---------

Co-authored-by: Khải <[email protected]>
  • Loading branch information
nachoaldamav and KSXGitHub authored Nov 3, 2023
1 parent 8c8eb47 commit 808dbb5
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 85 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,5 @@ Cargo.lock
@reflink

sandbox
__reflink-tests-*
*.json.bak
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ version = "0.0.0"
crate-type = ["cdylib"]

[dependencies]
copy_on_write = "0.1.1"
futures = "0.3.28"
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
napi-derive = "2.12.2"
reflink-copy = { version = "0.1.10" }

[build-dependencies]
napi-build = "2.0.1"
Expand Down
158 changes: 98 additions & 60 deletions __test__/main.spec.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,91 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, describe, expect, it } from 'vitest';
import { join, resolve } from 'path';
import { reflinkFileSync, reflinkFile } from '../index.js';
import { mkdir, rm, writeFile } from 'fs/promises';
import { readFileSync } from 'fs';
import { randomUUID, createHash } from 'crypto';
import { rimraf } from 'rimraf';
import { reflinkFileSync, reflinkFile } from '../index.js';

const sandboxDir = join(process.cwd(), `__reflink-tests-${randomUUID()}`);
const sandboxDir = () => join(process.cwd(), `__reflink-tests-${randomUUID()}`);

const sandboxFiles = [
{
path: join(sandboxDir, 'file1.txt'),
path: 'file1.txt',
content: 'Hello World!',
sha: createHash('sha256').update('Hello World!').digest('hex'),
},
{
path: join(sandboxDir, 'file2.txt'),
path: 'file2.txt',
content: 'Hello World!',
sha: createHash('sha256').update('Hello World!').digest('hex'),
},
{
path: join(sandboxDir, 'file3.txt'),
path: 'file3.txt',
content: 'Hello World!',
sha: createHash('sha256').update('Hello World!').digest('hex'),
},
];

describe('reflink', () => {
beforeAll(async () => {
await mkdir(sandboxDir, { recursive: true });
});
const sandboxDirectories: string[] = [];

afterAll(async () => {
await rm(sandboxDir, { recursive: true, force: true });
});
async function prepare(dir: string) {
await mkdir(dir, { recursive: true });

sandboxDirectories.push(dir);

beforeEach(async () => {
// remove the sandbox directory and recreate it
await rm(sandboxDir, { recursive: true, force: true });
await mkdir(sandboxDir, { recursive: true });
return Promise.all(
sandboxFiles.map(async (file) => {
await writeFile(join(dir, file.path), file.content);
return {
...file,
path: join(dir, file.path),
};
})
);
}

// create the files again
describe('reflink', () => {
afterAll(async () => {
await Promise.all(
sandboxFiles.map(async (file) => {
await writeFile(file.path, file.content);
sandboxDirectories.map(async (dir) => {
await rimraf(dir).catch(() => {});
})
);
});

it('should correctly clone a file (sync)', () => {
const file = sandboxFiles[0];
it('should correctly clone a file (sync)', async () => {
const dir = sandboxDir();
const files = await prepare(dir);
const file = files[0];

reflinkFileSync(file.path, join(sandboxDir, 'file1-copy.txt'));
reflinkFileSync(file.path, join(dir, 'file1-copy.txt'));

const content = readFileSync(join(sandboxDir, 'file1-copy.txt'), 'utf-8');
const content = readFileSync(join(dir, 'file1-copy.txt'), 'utf-8');

expect(content).toBe(file.content);
});

it('should correctly clone a file (async)', async () => {
const file = sandboxFiles[0];
const dir = sandboxDir();
const files = await prepare(dir);
const file = files[0];

await reflinkFile(file.path, join(sandboxDir, 'file1-copy.txt'));
await reflinkFile(file.path, join(dir, 'file1-copy.txt'));

const content = readFileSync(join(sandboxDir, 'file1-copy.txt'), 'utf-8');
const content = readFileSync(join(dir, 'file1-copy.txt'), 'utf-8');

expect(content).toBe(file.content);
});

it('should keep the same content in source file after editing the cloned file', async () => {
const file = sandboxFiles[0];
const dir = sandboxDir();
const files = await prepare(dir);
const file = files[0];

await reflinkFile(file.path, join(sandboxDir, 'file1-copy.txt'));
await reflinkFile(file.path, join(dir, 'file1-copy.txt'));

await writeFile(
join(sandboxDir, 'file1-copy.txt'),
join(dir, 'file1-copy.txt'),
file.content + '\nAdded content!'
);

Expand All @@ -82,65 +94,84 @@ describe('reflink', () => {
expect(originalContent).toBe(file.content);
});

it('should fail if the source file does not exist (sync)', () => {
it('should fail if the source file does not exist (sync)', async () => {
const dir = sandboxDir();
await prepare(dir);

expect(() => {
reflinkFileSync(
join(sandboxDir, 'file-does-not-exist.txt'),
join(sandboxDir, 'file1-copy.txt')
join(dir, 'file-does-not-exist.txt'),
join(dir, 'file1-copy.txt')
);
}).toThrow();
});

it('should fail if the source file does not exist (async)', async () => {
const dir = sandboxDir();
await prepare(dir);

await expect(
reflinkFile(
join(sandboxDir, 'file-does-not-exist.txt'),
join(sandboxDir, 'file1-copy.txt')
join(dir, 'file-does-not-exist.txt'),
join(dir, 'file1-copy.txt')
)
).rejects.toThrow();
});

it('should fail if the destination file already exists (sync)', () => {
it('should fail if the destination file already exists (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);

expect(() => {
reflinkFileSync(sandboxFiles[0].path, sandboxFiles[1].path);
}).toThrow();
});

it('should fail if the destination file already exists (async)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
await expect(
reflinkFile(sandboxFiles[0].path, sandboxFiles[1].path)
).rejects.toThrow();
});

it('should fail if the source file is a directory (sync)', () => {
it('should fail if the source file is a directory (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
expect(() => {
reflinkFileSync(sandboxDir, sandboxFiles[1].path);
reflinkFileSync(dir, sandboxFiles[1].path);
}).toThrow();
});

it('should fail if the source file is a directory (async)', async () => {
await expect(
reflinkFile(sandboxDir, sandboxFiles[1].path)
).rejects.toThrow();
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
await expect(reflinkFile(dir, sandboxFiles[1].path)).rejects.toThrow();
});

it('should fail if the source and destination files are the same (sync)', () => {
it('should fail if the source and destination files are the same (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
expect(() => {
reflinkFileSync(sandboxFiles[0].path, sandboxFiles[0].path);
}).toThrow();
});

it('should fail if the source and destination files are the same (async)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
await expect(
reflinkFile(sandboxFiles[0].path, sandboxFiles[0].path)
).rejects.toThrow();
});

it('should fail if the destination parent directory does not exist (sync)', () => {
it('should fail if the destination parent directory does not exist (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
expect(() => {
reflinkFileSync(
sandboxFiles[0].path,
join(sandboxDir, 'does-not-exist', 'file1-copy.txt')
join(dir, 'does-not-exist', 'file1-copy.txt')
);
}).toThrow();
});
Expand Down Expand Up @@ -190,8 +221,11 @@ describe('reflink', () => {
});

it('should correctly clone 1000 files (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);

const files = Array.from({ length: 1000 }, (_, i) => ({
path: join(sandboxDir, `file${i}.txt`),
path: join(dir, `file${i}.txt`),
content: 'Hello World!',
}));

Expand All @@ -201,22 +235,21 @@ describe('reflink', () => {

await Promise.all(
files.map(async (file, i) =>
reflinkFileSync(file.path, join(sandboxDir, `file${i}-copy.txt`))
reflinkFileSync(file.path, join(dir, `file${i}-copy.txt`))
)
);

files.forEach((file, i) => {
const content = readFileSync(
join(sandboxDir, `file${i}-copy.txt`),
'utf-8'
);
const content = readFileSync(join(dir, `file${i}-copy.txt`), 'utf-8');
expect(content).toBe(file.content);
});
});

it('should correctly clone 1000 files (async)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
const files = Array.from({ length: 1000 }, (_, i) => ({
path: join(sandboxDir, `file${i}.txt`),
path: join(dir, `file${i}.txt`),
content: 'Hello World!',
hash: createHash('sha256').update('Hello World!').digest('hex'),
}));
Expand All @@ -227,29 +260,28 @@ describe('reflink', () => {

await Promise.all(
files.map(async (file, i) =>
reflinkFile(file.path, join(sandboxDir, `file${i}-copy.txt`))
reflinkFile(file.path, join(dir, `file${i}-copy.txt`))
)
);

files.forEach((file, i) => {
const content = readFileSync(
join(sandboxDir, `file${i}-copy.txt`),
'utf-8'
);
const content = readFileSync(join(dir, `file${i}-copy.txt`), 'utf-8');
const hash = createHash('sha256').update(content).digest('hex');
expect(content).toBe(file.content);
expect(hash).toBe(file.hash);
});
});

it('should keep the same hash when cloning a file more than 3,000 times', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
const srcFile = {
path: resolve('./package.json'),
content: readFileSync(join('./package.json'), 'utf-8'),
};

const destFiles = Array.from({ length: 3_000 }, (_, i) => ({
path: join(sandboxDir, `file1-copy-${i}.txt`),
path: join(dir, `file1-copy-${i}.txt`),
hash: createHash('sha256').update(srcFile.content).digest('hex'),
}));

Expand All @@ -276,13 +308,15 @@ describe('reflink', () => {
});

it('should clone "sample.pyc" file correctly (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
const srcFile = {
path: resolve(join('fixtures', 'sample.pyc')),
content: readFileSync(join('fixtures', 'sample.pyc')),
};

const destFile = {
path: join(sandboxDir, 'sample.pyc'),
path: join(dir, 'sample.pyc'),
hash: createHash('sha256').update(srcFile.content).digest('hex'),
};

Expand All @@ -299,13 +333,15 @@ describe('reflink', () => {
* The issue with empty cloned files doesnt seem related to ASCII characters
*/
it.skip('should clone "ascii-file.js" file correctly (sync)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
const srcFile = {
path: resolve(join('fixtures', 'ascii-file.js')),
content: readFileSync(join('fixtures', 'ascii-file.js')),
};

const destFile = {
path: join(sandboxDir, 'ascii-file.js'),
path: join(dir, 'ascii-file.js'),
hash: createHash('sha256').update(srcFile.content).digest('hex'),
};

Expand All @@ -325,13 +361,15 @@ describe('reflink', () => {
});

it('should clone "sample.pyc" file correctly (async)', async () => {
const dir = sandboxDir();
const sandboxFiles = await prepare(dir);
const srcFile = {
path: resolve(join('fixtures', 'sample.pyc')),
content: readFileSync(join('fixtures', 'sample.pyc')),
};

const destFile = {
path: join(sandboxDir, 'sample.pyc'),
path: join(dir, 'sample.pyc'),
hash: createHash('sha256').update(srcFile.content).digest('hex'),
};

Expand Down
2 changes: 1 addition & 1 deletion __test__/threads.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('reflink worker', () => {
}
});

it('clone the same file to different location simultaneously (sync)', async () => {
it('clone the same file to different location simultaneously (async)', async () => {
const src = {
path: join(process.cwd(), 'fixtures', 'ascii-file.js'),
content: readFileSync(join(process.cwd(), 'fixtures', 'ascii-file.js')),
Expand Down
1 change: 0 additions & 1 deletion infinite_clone_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ async function main() {
for (let i = 0; i < 1000; i++) {
const destPath = path.join('./sandbox', `file1-copy-${i}.txt`);

// Assume reflinkFile is your function that performs the file cloning operation
await reflinkFile(srcFile.path, destPath);

const destContent = await fs.readFile(destPath, 'utf-8');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"build": "napi build --platform --release",
"build:debug": "napi build --platform",
"prepublishOnly": "napi prepublish -t npm",
"pretest": "yarn build",
"pretest": "pnpm build",
"test": "cargo t && vitest",
"bench": "node benchmark.mjs",
"universal": "napi universal",
Expand Down
Loading

0 comments on commit 808dbb5

Please sign in to comment.