Skip to content

Commit c34bd81

Browse files
XiaochenCuiKernelDeimos
authored andcommitted
test: fix apitest, add new unit tests and benchmarks, normalize output
1 parent c0c601a commit c34bd81

File tree

13 files changed

+338
-71
lines changed

13 files changed

+338
-71
lines changed

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,18 @@ dist/
2929
# Local Netlify folder
3030
.netlify
3131
src/emulator/release/
32+
33+
# ======================================================================
34+
# vscode
35+
# ======================================================================
36+
# vscode configuration
37+
.vscode/
38+
39+
# JS language server, ref: https://code.visualstudio.com/docs/languages/jsconfig
40+
jsconfig.json
41+
42+
# ======================================================================
43+
# node js
44+
# ======================================================================
45+
# the exact tree installed in the node_modules folder
46+
package-lock.json

tools/api-tester/README.md

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ A test framework for testing the backend API of puter.
44

55
## Table of Contents
66

7-
- [API Tester](#api-tester)
87
- [How to use](#how-to-use)
98
- [Workflow](#workflow)
109
- [Shorthands](#shorthands)
1110
- [Basic Concepts](#basic-concepts)
1211
- [Behaviors](#behaviors)
13-
- [Isolation of `t.cwd`](#isolation-of-t-cwd)
12+
- [Working directory (`t.cwd`)](#working-directory-t-cwd)
1413
- [Implementation](#implementation)
1514
- [TODO](#todo)
1615

@@ -34,30 +33,65 @@ All commands below should be run from the root directory of puter.
3433

3534
Fields:
3635
- url: The endpoint of the backend server. (default: http://api.puter.localhost:4100/)
37-
- username: The username of the admin user. (e.g. admin)
38-
- token: The token of the user. (can be obtained by typing `puter.authToken` in Developer Tools's console)
36+
- username: The username of the user to test. (e.g. `admin`)
37+
- token: The token of the user. (can be obtained by logging in on the webpage and typing `puter.authToken` in Developer Tools's console)
38+
- mountpoints: The mountpoints to test. (default config includes 2 mountpoints: `/` for "puter fs provider" and `/admin/tmp` for "memory fs provider")
3939
40-
3. Run the tests:
40+
3. Run all tests (unit tests and benchmarks):
4141
4242
```bash
4343
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml
4444
```
4545
4646
### Shorthands
4747
48-
- Run unit tests only:
48+
- Run all tests (unit tests and benchmarks):
49+
50+
```bash
51+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml
52+
```
53+
54+
- Run all unit tests:
4955
5056
```bash
5157
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --unit
5258
```
5359
60+
- Run all benchmarks:
61+
62+
```bash
63+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --bench
64+
```
65+
5466
- Filter tests by suite name:
5567
5668
```bash
5769
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --unit --suite=mkdir
5870
```
5971
60-
- Rerun failed tests in the last run:
72+
- Filter benchmarks by name:
73+
74+
```bash
75+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --bench --suite=stat_intensive_1
76+
```
77+
78+
- Stop on first failure:
79+
80+
```bash
81+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --unit --stop-on-failure
82+
```
83+
84+
- (unimplemented) Filter tests by test name:
85+
86+
```bash
87+
# (wildcard matching) Run tests containing "memoryfs" in the name
88+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --unit --test='*memoryfs*'
89+
90+
# (exact matching) Run the test "mkdir in memoryfs"
91+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --unit --test='mkdir in memoryfs'
92+
```
93+
94+
- (unimplemented) Rerun failed tests in the last run:
6195
6296
```bash
6397
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --rerun-failed
@@ -98,10 +132,13 @@ module.exports = {
98132
99133
## Behaviors
100134
101-
### Isolation of `t.cwd`
135+
### Working directory (`t.cwd`)
102136
103-
- `t.cwd` is reset at the beginning of each test suite, since a test suite usually doesn't want to be affected by other test suites.
104-
- `t.cwd` will be inherited from the cases in the same test suite, since a leaf case might want to share the context with its parent/sibling cases.
137+
- The working directory is stored in `t.cwd`.
138+
- All filesystem operations are performed relative to the working directory, if the given path is not absolute. (e.g., `t.mkdir('foo')`, `t.cd('foo')`, `t.stat('foo')`, etc.)
139+
- Tests will be run under all mountpoints. The default working directory for a mountpoint is `${mountpoint.path}/{username}/api_test`. (This is subject to change in the future, the reason we include `admin` in the path is to ensure the test user `admin` has write access, see [Permission Documentation](https://github.com/HeyPuter/puter/blob/3290440f4bf7a263f37bc5233565f8fec146f17b/src/backend/doc/A-and-A/permission.md#permission-options) for details.)
140+
- The working directory is reset at the beginning of each test suite, since a test suite usually doesn't want to be affected by other test suites.
141+
- The working directory will be inherited from the cases in the same test suite, since a leaf case might want to share the context with its parent/sibling cases.
105142

106143
```js
107144
module.exports = {
@@ -126,5 +163,5 @@ module.exports = {
126163
127164
## TODO
128165
129-
- [ ] Update usage of apitest.js. (Is it possible to generate the usage automatically?)
130-
- [ ] Integrate it into CI, optionally running it only in specific scenarios (e.g., when backend code changes).
166+
- [ ] Reset `t.cwd` if a test case fails. Currently, `t.cwd` is not reset if a test case fails.
167+
- [ ] Integrate apitest into CI, optionally running it only in specific scenarios (e.g., when backend code changes).

tools/api-tester/apitest.js

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ const { parseArgs } = require('node:util');
99

1010
const args = process.argv.slice(2);
1111

12-
let config, report, suiteName;
12+
let config, report, suiteName, onlycase, bench, unit, stopOnFailure, id;
1313

1414
try {
1515
const parsed = parseArgs({
1616
options: {
1717
config: {
1818
type: 'string',
19+
default: './tools/api-tester/config.yml',
1920
},
2021
report: {
2122
type: 'string',
@@ -24,6 +25,7 @@ try {
2425
bench: { type: 'boolean' },
2526
unit: { type: 'boolean' },
2627
suite: { type: 'string' },
28+
'stop-on-failure': { type: 'boolean' },
2729
},
2830
allowPositionals: true,
2931
});
@@ -35,6 +37,7 @@ try {
3537
bench,
3638
unit,
3739
suite: suiteName,
40+
'stop-on-failure': stopOnFailure,
3841
}, positionals: [id] } = parsed);
3942

4043
onlycase = onlycase !== undefined ? Number.parseInt(onlycase) : undefined;
@@ -49,6 +52,7 @@ try {
4952
' --config=<path> (required) Path to configuration file\n' +
5053
' --report=<path> (optional) Output file for full test results\n' +
5154
' --suite=<name> (optional) Run only tests with matching suite name\n' +
55+
' --stop-on-failure (optional) Stop execution on first test failure\n' +
5256
''
5357
);
5458
process.exit(1);
@@ -58,29 +62,83 @@ const conf = YAML.parse(fs.readFileSync(config).toString());
5862

5963

6064
const main = async () => {
61-
const context = {
62-
options: {
63-
onlycase,
64-
suite: suiteName,
65+
const unit_test_results = [];
66+
const benchmark_results = [];
67+
for (const mountpoint of conf.mountpoints) {
68+
const { unit_test_results: results, benchmark_results: benchs } = await test({ mountpoint });
69+
unit_test_results.push(...results);
70+
benchmark_results.push(...benchs);
71+
}
72+
73+
// hard-coded identifier for ci script
74+
console.log("==================== nightly build results begin ====================")
75+
76+
// print unit test results
77+
let tbl = {};
78+
for ( const result of unit_test_results ) {
79+
tbl[result.name + ' - ' + result.settings] = {
80+
passed: result.caseCount - result.failCount,
81+
failed: result.failCount,
82+
total: result.caseCount,
83+
'duration (s)': result.duration ? result.duration.toFixed(2) : 'N/A',
84+
}
85+
}
86+
console.table(tbl);
87+
88+
// print benchmark results
89+
if (benchmark_results.length > 0) {
90+
tbl = {};
91+
for ( const result of benchmark_results ) {
92+
const fs_provider = result.fs_provider || 'unknown';
93+
tbl[result.name + ' - ' + fs_provider] = {
94+
'duration (s)': result.duration ? (result.duration / 1000).toFixed(2) : 'N/A',
95+
}
96+
}
97+
console.table(tbl);
98+
99+
// print description of each benchmark since it's too long to fit in the table
100+
const seen = new Set();
101+
for ( const result of benchmark_results ) {
102+
if ( seen.has(result.name) ) continue;
103+
seen.add(result.name);
104+
105+
if ( result.description ) {
106+
console.log(result.name + ': ' + result.description);
107+
}
65108
}
66-
};
67-
const ts = new TestSDK(conf, context);
68-
try {
69-
await ts.delete('api_test', { recursive: true });
70-
} catch (e) {
71109
}
72-
await ts.mkdir('api_test', { overwrite: true });
73-
ts.cd('api_test');
110+
111+
// hard-coded identifier for ci script
112+
console.log("==================== nightly build results end ====================")
113+
}
114+
115+
/**
116+
* Run test using the given config, and return the test results
117+
*
118+
* @param {Object} options
119+
* @param {Object} options.mountpoint
120+
* @returns {Promise<Object>}
121+
*/
122+
async function test({ mountpoint }) {
123+
const context = {
124+
mountpoint
125+
};
126+
127+
const ts = new TestSDK(conf, context, { stopOnFailure });
128+
await ts.init_working_directory();
74129

75130
const registry = new TestRegistry(ts);
76131

77132
registry.add_test_sdk('puter-rest.v1', require('./test_sdks/puter-rest')({
78133
config: conf,
79134
}));
80135

81-
require('./tests/__entry__.js')(registry);
136+
// TODO: merge it into the entry point
82137
require('./benches/simple.js')(registry);
83138

139+
require('./tests/__entry__.js')(registry);
140+
require('./benches/__entry__.js')(registry);
141+
84142
if ( id ) {
85143
if ( unit ) {
86144
await registry.run_test(id);
@@ -95,25 +153,18 @@ const main = async () => {
95153
if ( unit ) {
96154
await registry.run_all_tests(suiteName);
97155
} else if ( bench ) {
98-
await registry.run_all_benches();
156+
await registry.run_all_benches(suiteName);
99157
} else {
100158
await registry.run_all();
101159
}
102160

161+
if ( unit ) ts.printTestResults();
162+
if ( bench ) ts.printBenchmarkResults();
103163

104-
// await ts.runTestPackage(require('./tests/write_cart'));
105-
// await ts.runTestPackage(require('./tests/move_cart'));
106-
// await ts.runTestPackage(require('./tests/copy_cart'));
107-
// await ts.runTestPackage(require('./tests/write_and_read'));
108-
// await ts.runTestPackage(require('./tests/move'));
109-
// await ts.runTestPackage(require('./tests/stat'));
110-
// await ts.runTestPackage(require('./tests/readdir'));
111-
// await ts.runTestPackage(require('./tests/mkdir'));
112-
// await ts.runTestPackage(require('./tests/batch'));
113-
// await ts.runTestPackage(require('./tests/delete'));
114-
const all = unit && bench;
115-
if ( all || unit ) ts.printTestResults();
116-
if ( all || bench ) ts.printBenchmarkResults();
164+
return {
165+
unit_test_results: ts.packageResults,
166+
benchmark_results: ts.benchmarkResults,
167+
};
117168
}
118169

119170
const main_e = async () => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = registry => {
2+
registry.add_bench('write_intensive_1', require('./write_intensive_1.js'));
3+
registry.add_bench('stat_intensive_1', require('./stat_intensive_1.js'));
4+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const chai = require('chai');
2+
chai.use(require('chai-as-promised'))
3+
const expect = chai.expect;
4+
5+
module.exports = {
6+
name: 'stat intensive 1',
7+
description: 'create 10 directories and 100 subdirectories in each, then stat them over and over',
8+
do: async t => {
9+
console.log('stat intensive 1');
10+
11+
const dir_count = 10;
12+
const subdir_count = 100;
13+
14+
// key: uuid
15+
// value: path
16+
const dirs = {};
17+
18+
for (let i = 0; i < dir_count; i++) {
19+
await t.mkdir(`dir_${i}`);
20+
for (let j = 0; j < subdir_count; j++) {
21+
const subdir = await t.mkdir(`dir_${i}/subdir_${j}`);
22+
dirs[subdir.uid] = subdir.path;
23+
}
24+
}
25+
26+
const start = Date.now();
27+
for (let i = 0; i < 10; i++) {
28+
for (const [uuid, path] of Object.entries(dirs)) {
29+
const stat = await t.stat_uuid(uuid);
30+
expect(stat.is_dir).equal(true);
31+
expect(stat.path).equal(path);
32+
}
33+
}
34+
const duration = Date.now() - start;
35+
return { duration };
36+
}
37+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const chai = require('chai');
2+
chai.use(require('chai-as-promised'))
3+
const expect = chai.expect;
4+
5+
module.exports = {
6+
name: 'write intensive 1',
7+
description: 'create 100 new directories and write 10 files in each, then check integrity by stat/readdir/read api',
8+
do: async t => {
9+
console.log('write intensive 1');
10+
11+
const dir_count = 100;
12+
const file_count = 10;
13+
14+
for ( let i=0 ; i < dir_count ; i++ ) {
15+
await t.mkdir(`dir_${i}`);
16+
for ( let j=0 ; j < file_count ; j++ ) {
17+
const content = `example ${i} ${j}`;
18+
await t.write(`dir_${i}/file_${j}.txt`, content, { overwrite: true });
19+
}
20+
}
21+
22+
for ( let i=0 ; i < dir_count ; i++ ) {
23+
const dir = await t.stat(`dir_${i}`);
24+
const files = await t.readdir(dir.path);
25+
expect(files.length).equal(file_count);
26+
for ( let j=0 ; j < file_count ; j++ ) {
27+
const content = await t.read(`dir_${i}/file_${j}.txt`);
28+
expect(content).equal(`example ${i} ${j}`);
29+
}
30+
}
31+
}
32+
};
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
url: http://puter.localhost:4001/
2-
username: lavender_hat_6100
1+
url: http://api.puter.localhost:4100/
2+
username: admin
33
token: ---
4+
mountpoints:
5+
- path: /
6+
provider: puterfs
7+
- path: /admin/tmp
8+
provider: memoryfs

tools/api-tester/lib/TestFactory.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = class TestFactory {
1212
for ( let i=0 ; i < states.length ; i++ ) {
1313
const state = states[i];
1414

15-
if ( t.context.options.onlycase !== undefined ) {
15+
if ( t.context.options?.onlycase !== undefined ) {
1616
if ( i !== t.context.options.onlycase ) {
1717
continue;
1818
}

0 commit comments

Comments
 (0)