Skip to content

Commit 2ae32b1

Browse files
committed
File operations
1 parent b428d85 commit 2ae32b1

File tree

2 files changed

+240
-106
lines changed

2 files changed

+240
-106
lines changed
Lines changed: 231 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createNodeFsMountHandler, loadNodeRuntime } from '..';
2-
import { PHP } from '@php-wasm/universal';
2+
import { ErrnoError, PHP } from '@php-wasm/universal';
33
import { RecommendedPHPVersion } from '@wp-playground/common';
44
import path, { dirname } from 'path';
55
import fs from 'fs';
@@ -18,132 +18,257 @@ describe('Mounting', () => {
1818
php.exit();
1919
});
2020

21-
it('Should mount a file with exact content match', async () => {
22-
const testFilePath = path.join(
23-
__dirname,
24-
'test-data',
25-
'long-post-body.txt'
26-
);
21+
describe('File operations', () => {
22+
it('Should mount a file with exact content match', async () => {
23+
const testFilePath = path.join(
24+
__dirname,
25+
'test-data',
26+
'long-post-body.txt'
27+
);
2728

28-
await php.mount(
29-
'/single-file.txt',
30-
createNodeFsMountHandler(testFilePath)
31-
);
29+
await php.mount(
30+
'/single-file.txt',
31+
createNodeFsMountHandler(testFilePath)
32+
);
3233

33-
const vfsContent = await php.readFileAsText('/single-file.txt');
34-
const localContent = fs.readFileSync(testFilePath, 'utf8');
35-
expect(vfsContent).toEqual(localContent);
36-
});
34+
const vfsContent = await php.readFileAsText('/single-file.txt');
35+
const localContent = fs.readFileSync(testFilePath, 'utf8');
36+
expect(vfsContent).toEqual(localContent);
37+
});
3738

38-
it('Should mount nested directories with recursive structure matching', async () => {
39-
const testDataPath = path.join(__dirname, 'test-data');
40-
await php.mount('/nested-test', createNodeFsMountHandler(testDataPath));
41-
42-
// Recursively compare directory structure
43-
const compareDirectories = (vfsPath: string, localPath: string) => {
44-
if (!fs.existsSync(localPath)) return;
45-
46-
const localFiles = fs.readdirSync(localPath);
47-
const vfsFiles = php.listFiles(vfsPath);
48-
expect(vfsFiles.sort()).toEqual(localFiles.sort());
49-
50-
localFiles.forEach((file) => {
51-
const localFilePath = path.join(localPath, file);
52-
const vfsFilePath = `${vfsPath}/${file}`;
53-
const localStats = fs.statSync(localFilePath);
54-
55-
expect(php.isFile(vfsFilePath)).toBe(localStats.isFile());
56-
expect(php.isDir(vfsFilePath)).toBe(localStats.isDirectory());
57-
58-
if (localStats.isDirectory()) {
59-
compareDirectories(vfsFilePath, localFilePath);
60-
}
61-
});
62-
};
63-
64-
compareDirectories('/nested-test', testDataPath);
65-
66-
// Test specific nested file content
67-
const nestedFilePath =
68-
'/nested-test/nested-symlinked-folder/nested-document.txt';
69-
const localNestedPath = path.join(
70-
testDataPath,
71-
'nested-symlinked-folder',
72-
'nested-document.txt'
73-
);
39+
it('Should throw an error when mounting to an existing file', async () => {
40+
const testFilePath = path.join(
41+
__dirname,
42+
'test-data',
43+
'long-post-body.txt'
44+
);
7445

75-
if (fs.existsSync(localNestedPath)) {
76-
const vfsContent = await php.readFileAsText(nestedFilePath);
77-
const localContent = fs.readFileSync(localNestedPath, 'utf8');
78-
expect(vfsContent).toEqual(localContent);
79-
}
80-
});
46+
await php.mount(
47+
'/single-file.txt',
48+
createNodeFsMountHandler(testFilePath)
49+
);
8150

82-
it('Should unmount a file and remove created node from VFS', async () => {
83-
const testFilePath = path.join(
84-
__dirname,
85-
'test-data',
86-
'long-post-body.txt'
87-
);
51+
try {
52+
await php.mount(
53+
'/single-file.txt',
54+
createNodeFsMountHandler(testFilePath)
55+
);
56+
} catch (e: any) {
57+
e = e as ErrnoError;
58+
expect(e.name).toBe('ErrnoError');
59+
expect(e.errno).toBe(10);
60+
}
61+
});
8862

89-
const unmount = await php.mount(
90-
'/single-file.txt',
91-
createNodeFsMountHandler(testFilePath)
92-
);
63+
it('Should be editable', async () => {
64+
const testFilePath = path.join(
65+
__dirname,
66+
'test-data',
67+
'long-post-body.txt'
68+
);
69+
await php.mount(
70+
'/single-file.txt',
71+
createNodeFsMountHandler(testFilePath)
72+
);
9373

94-
expect(php.isFile('/single-file.txt')).toBe(true);
74+
const originalContent = await php.readFileAsText(
75+
'/single-file.txt'
76+
);
77+
await php.writeFile('/single-file.txt', 'new content');
9578

96-
await unmount();
97-
expect(php.isFile('/single-file.txt')).toBe(false);
98-
});
79+
expect(await php.readFileAsText('/single-file.txt')).toBe(
80+
'new content'
81+
);
9982

100-
it('Should unmount a directory and remove created node from VFS', async () => {
101-
const testDataPath = path.join(__dirname, 'test-data');
102-
const unmount = await php.mount(
103-
'/nested-test',
104-
createNodeFsMountHandler(testDataPath)
105-
);
83+
await php.writeFile('/single-file.txt', originalContent);
84+
expect(await php.readFileAsText('/single-file.txt')).toBe(
85+
originalContent
86+
);
87+
});
10688

107-
expect(php.isDir('/nested-test')).toBe(true);
89+
it('Should not be deletable', async () => {
90+
const testFilePath = path.join(
91+
__dirname,
92+
'test-data',
93+
'long-post-body.txt'
94+
);
95+
await php.mount(
96+
'/single-file.txt',
97+
createNodeFsMountHandler(testFilePath)
98+
);
10899

109-
await unmount();
110-
expect(php.isDir('/nested-test')).toBe(false);
111-
});
100+
try {
101+
await php.unlink('/single-file.txt');
102+
} catch (e: any) {
103+
e = e as Error;
104+
expect(e.message).toContain(
105+
'Could not unlink "/single-file.txt": Device or resource busy.'
106+
);
107+
}
108+
});
112109

113-
it('Should unmount a file, but not remove the parent directory from VFS if it was created manually', async () => {
114-
const testFilePath = path.join(
115-
__dirname,
116-
'test-data',
117-
'long-post-body.txt'
118-
);
110+
it('Should not be movable', async () => {
111+
const testFilePath = path.join(
112+
__dirname,
113+
'test-data',
114+
'long-post-body.txt'
115+
);
116+
await php.mount(
117+
'/single-file.txt',
118+
createNodeFsMountHandler(testFilePath)
119+
);
119120

120-
const mountPoint = '/sub-dir/single-file.txt';
121+
try {
122+
await php.mv('/single-file.txt', '/single-file-moved.txt');
123+
} catch (e: any) {
124+
e = e as Error;
125+
expect(e.message).toContain(
126+
'Could not move /single-file.txt to /single-file-moved.txt: Device or resource busy.'
127+
);
128+
}
129+
});
121130

122-
await php.mkdir(dirname(mountPoint));
131+
it('Should unmount a file and remove created node from VFS', async () => {
132+
const testFilePath = path.join(
133+
__dirname,
134+
'test-data',
135+
'long-post-body.txt'
136+
);
123137

124-
const unmount = await php.mount(
125-
mountPoint,
126-
createNodeFsMountHandler(testFilePath)
127-
);
138+
const unmount = await php.mount(
139+
'/single-file.txt',
140+
createNodeFsMountHandler(testFilePath)
141+
);
142+
143+
expect(php.isFile('/single-file.txt')).toBe(true);
144+
145+
await unmount();
146+
expect(php.isFile('/single-file.txt')).toBe(false);
147+
});
148+
149+
it('Should remount after unmounting', async () => {
150+
const testFilePath = path.join(
151+
__dirname,
152+
'test-data',
153+
'long-post-body.txt'
154+
);
155+
156+
const unmount = await php.mount(
157+
'/single-file.txt',
158+
createNodeFsMountHandler(testFilePath)
159+
);
160+
161+
await unmount();
162+
await php.mount(
163+
'/single-file.txt',
164+
createNodeFsMountHandler(testFilePath)
165+
);
128166

129-
expect(php.isFile(mountPoint)).toBe(true);
167+
expect(php.isFile('/single-file.txt')).toBe(true);
168+
expect(await php.readFileAsText('/single-file.txt')).toBe(
169+
fs.readFileSync(testFilePath, 'utf8')
170+
);
171+
});
130172

131-
await unmount();
132-
expect(php.isDir(dirname(mountPoint))).toBe(true);
173+
it('Should unmount a file, but not remove the parent directory from VFS if it was created manually', async () => {
174+
const testFilePath = path.join(
175+
__dirname,
176+
'test-data',
177+
'long-post-body.txt'
178+
);
179+
180+
const mountPoint = '/sub-dir/single-file.txt';
181+
182+
await php.mkdir(dirname(mountPoint));
183+
184+
const unmount = await php.mount(
185+
mountPoint,
186+
createNodeFsMountHandler(testFilePath)
187+
);
188+
189+
expect(php.isFile(mountPoint)).toBe(true);
190+
191+
await unmount();
192+
expect(php.isDir(dirname(mountPoint))).toBe(true);
193+
});
133194
});
134195

135-
it('Should unmount a directory, but not remove the parent directory from VFS if it was created manually', async () => {
136-
const testDataPath = path.join(__dirname, 'test-data');
196+
describe('Directory operations', () => {
197+
it('Should mount nested directories with recursive structure matching', async () => {
198+
const testDataPath = path.join(__dirname, 'test-data');
199+
await php.mount(
200+
'/nested-test',
201+
createNodeFsMountHandler(testDataPath)
202+
);
137203

138-
await php.mkdir('/nested-test');
139-
const unmount = await php.mount(
140-
'/nested-test',
141-
createNodeFsMountHandler(testDataPath)
142-
);
204+
// Recursively compare directory structure
205+
const compareDirectories = (vfsPath: string, localPath: string) => {
206+
if (!fs.existsSync(localPath)) return;
207+
208+
const localFiles = fs.readdirSync(localPath);
209+
const vfsFiles = php.listFiles(vfsPath);
210+
expect(vfsFiles.sort()).toEqual(localFiles.sort());
211+
212+
localFiles.forEach((file) => {
213+
const localFilePath = path.join(localPath, file);
214+
const vfsFilePath = `${vfsPath}/${file}`;
215+
const localStats = fs.statSync(localFilePath);
216+
217+
expect(php.isFile(vfsFilePath)).toBe(localStats.isFile());
218+
expect(php.isDir(vfsFilePath)).toBe(
219+
localStats.isDirectory()
220+
);
221+
222+
if (localStats.isDirectory()) {
223+
compareDirectories(vfsFilePath, localFilePath);
224+
}
225+
});
226+
};
227+
228+
compareDirectories('/nested-test', testDataPath);
229+
230+
// Test specific nested file content
231+
const nestedFilePath =
232+
'/nested-test/nested-symlinked-folder/nested-document.txt';
233+
const localNestedPath = path.join(
234+
testDataPath,
235+
'nested-symlinked-folder',
236+
'nested-document.txt'
237+
);
238+
239+
if (fs.existsSync(localNestedPath)) {
240+
const vfsContent = await php.readFileAsText(nestedFilePath);
241+
const localContent = fs.readFileSync(localNestedPath, 'utf8');
242+
expect(vfsContent).toEqual(localContent);
243+
}
244+
});
245+
246+
it('Should unmount a directory and remove created node from VFS', async () => {
247+
const testDataPath = path.join(__dirname, 'test-data');
248+
const unmount = await php.mount(
249+
'/nested-test',
250+
createNodeFsMountHandler(testDataPath)
251+
);
252+
253+
expect(php.isDir('/nested-test')).toBe(true);
254+
255+
await unmount();
256+
expect(php.isDir('/nested-test')).toBe(false);
257+
});
258+
259+
it('Should unmount a directory, but not remove the parent directory from VFS if it was created manually', async () => {
260+
const testDataPath = path.join(__dirname, 'test-data');
261+
262+
await php.mkdir('/nested-test');
263+
const unmount = await php.mount(
264+
'/nested-test',
265+
createNodeFsMountHandler(testDataPath)
266+
);
143267

144-
expect(php.isDir('/nested-test')).toBe(true);
268+
expect(php.isDir('/nested-test')).toBe(true);
145269

146-
await unmount();
147-
expect(php.isDir('/nested-test')).toBe(true);
270+
await unmount();
271+
expect(php.isDir('/nested-test')).toBe(true);
272+
});
148273
});
149274
});

packages/php-wasm/node/src/test/symlinks.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,15 @@ testSymlinks.forEach(({ name, sourcePath, symlinkPath }) => {
404404
php.readlink(vfsMountPoint)
405405
);
406406
});
407+
408+
it.skip('Should move a symlink', async () => {
409+
// Symlinks can't be moved because of a Resource busy error.
410+
// TODO: Compare POSIX and Emscripten behavior.
411+
const newVfsMountPoint = '/symlink-moved.txt';
412+
php.mv(vfsMountPoint, newVfsMountPoint);
413+
expect(php.fileExists(vfsMountPoint)).toBe(false);
414+
expect(php.fileExists(newVfsMountPoint)).toBe(true);
415+
});
407416
});
408417
});
409418
});

0 commit comments

Comments
 (0)