Skip to content

Commit 2323ac0

Browse files
committed
sketch out a new test setup for the analysis revamp
1 parent cdbed51 commit 2323ac0

File tree

14 files changed

+332
-7
lines changed

14 files changed

+332
-7
lines changed

analysis/bin/main.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ let main () =
209209
| [_; "format"; path] ->
210210
Printf.printf "\"%s\"" (Json.escape (Commands.format ~path))
211211
| [_; "test"; path] -> Commands.test ~path
212+
| [_; "test_revamped"; path; config_file_path] ->
213+
Packages.overrideConfigFilePath := Some config_file_path;
214+
Commands.test ~path
212215
| args when List.mem "-h" args || List.mem "--help" args -> prerr_endline help
213216
| _ ->
214217
prerr_endline help;

analysis/src/Packages.ml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let makePathsForModule ~projectFilesAndPaths ~dependenciesFilesAndPaths =
1212
pathsForModule
1313

1414
let overrideRescriptVersion = ref None
15+
let overrideConfigFilePath = ref None
1516

1617
let getReScriptVersion () =
1718
match !overrideRescriptVersion with
@@ -162,15 +163,21 @@ let newBsPackage ~rootPath =
162163
| None -> None
163164
in
164165

165-
match Files.readFile rescriptJson with
166-
| Some raw -> parseRaw raw
166+
match !overrideConfigFilePath with
167+
| Some configFilePath -> (
168+
match Files.readFile configFilePath with
169+
| Some raw -> parseRaw raw
170+
| None -> failwith "Unable to read passed config file")
167171
| None -> (
168-
Log.log ("Unable to read " ^ rescriptJson);
169-
match Files.readFile bsconfigJson with
172+
match Files.readFile rescriptJson with
170173
| Some raw -> parseRaw raw
171-
| None ->
172-
Log.log ("Unable to read " ^ bsconfigJson);
173-
None)
174+
| None -> (
175+
Log.log ("Unable to read " ^ rescriptJson);
176+
match Files.readFile bsconfigJson with
177+
| Some raw -> parseRaw raw
178+
| None ->
179+
Log.log ("Unable to read " ^ bsconfigJson);
180+
None))
174181

175182
let findRoot ~uri packagesByRoot =
176183
let path = Uri.toPath uri in

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"packages/*",
9191
"tests/dependencies/**",
9292
"tests/analysis_tests/**",
93+
"tests/analysis_new_tests/**",
9394
"tests/gentype_tests/**",
9495
"tests/tools_tests"
9596
],

tests/analysis_new_tests/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Analysis tests
2+
3+
These tests test the analysis functionality for the editor tooling.
4+
5+
## Test Structure
6+
7+
Test files are located in the `test_files` directory and follow a specific format:
8+
9+
1. Each test file can contain multiple test blocks, separated by `// == TEST:` markers
10+
2. Each test block consists of:
11+
- A description line immediately following the marker
12+
- The actual test code
13+
- Any `^xxx` marker indicating what test to run, and where. Several `^xxx` tests can run per test block
14+
15+
Example:
16+
17+
```rescript
18+
// == TEST: Record field completion in nested record
19+
let x = TestTypeDefs.nestedTestRecord.
20+
// ^com
21+
```
22+
23+
## Directory Structure
24+
25+
### `support_files`
26+
27+
The `support_files` directory contains reusable code that can be shared across multiple tests. These files:
28+
29+
- Are compiled as part of the normal ReScript build process
30+
- Should be valid ReScript code that compiles without errors
31+
- Can contain type definitions, modules, and other code needed by multiple tests
32+
33+
### `test_files`
34+
35+
The `test_files` directory contains all the test files. These files:
36+
37+
- Are compiled incrementally for testing purposes only
38+
- Are not part of the main ReScript build
39+
- Can (should!) contain invalid/incomplete code since they're only used as tests
40+
- Each file can contain multiple test blocks as described above
41+
42+
## Test Execution
43+
44+
The test runner:
45+
46+
1. Extracts all test blocks from all test files
47+
2. Compiles each test block and sets each block up for incremental type checking
48+
3. Runs the editor analysis tool on each test block
49+
4. Compares the output against snapshots
50+
51+
This setup allows testing all editor tooling analysis features, in a flexible manner.
52+
53+
## Configuration Files
54+
55+
The test runner uses a special `rescript.test.json` configuration file when running the analysis tests. This is separate from the normal `rescript.json` configuration and is only used when calling the analysis binary for testing purposes. This separation ensures that test configurations don't interfere with the main project configuration, since the test files will not actually compile.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.res.js
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "@tests/analysis_new",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "rescript",
7+
"clean": "rescript clean -with-deps",
8+
"test": "yarn build && node test.js",
9+
"test:update": "node --test-update-snapshots test.js"
10+
},
11+
"dependencies": {
12+
"@rescript/react": "link:../../dependencies/rescript-react",
13+
"rescript": "workspace:^"
14+
}
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "test",
3+
"sources": [
4+
{
5+
"dir": "support_files"
6+
}
7+
],
8+
"bsc-flags": ["-w -33-44-8"],
9+
"bs-dependencies": ["@rescript/react"],
10+
"suffix": ".res.js",
11+
"package-specs": [
12+
{
13+
"module": "esmodule",
14+
"in-source": true
15+
}
16+
]
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "test",
3+
"sources": [
4+
{
5+
"dir": "support_files"
6+
},
7+
{
8+
"dir": "test_files/.build"
9+
}
10+
],
11+
"bsc-flags": ["-w -33-44-8"],
12+
"bs-dependencies": ["@rescript/react"],
13+
"suffix": ".res.js",
14+
"package-specs": [
15+
{
16+
"module": "esmodule",
17+
"in-source": true
18+
}
19+
]
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type nestedTestRecord = {
2+
test: bool,
3+
nested: {
4+
name: string,
5+
oneMoreLevel: {here: bool},
6+
},
7+
}
8+
9+
let nestedTestRecord = {
10+
test: true,
11+
nested: {
12+
name: "test",
13+
oneMoreLevel: {
14+
here: true,
15+
},
16+
},
17+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { test } from "node:test";
2+
import fs from "node:fs/promises";
3+
import path from "node:path";
4+
import { glob } from "glob";
5+
import { spawn, spawnSync } from "node:child_process";
6+
7+
// Get the current ReScript version
8+
const rescriptVersion = spawnSync("./node_modules/.bin/bsc", ["-v"])
9+
.stdout.toString()
10+
.trim()
11+
.replace("ReScript ", "");
12+
13+
const testFilesDir = path.join(import.meta.dirname, "./test_files");
14+
const buildDir = path.join(import.meta.dirname, "./test_files/.build");
15+
const incrementalDir = path.join(
16+
import.meta.dirname,
17+
"./lib/bs/___incremental"
18+
);
19+
20+
// Recreate directories needed
21+
try {
22+
await fs.access(incrementalDir);
23+
await fs.rm(incrementalDir, { recursive: true });
24+
} catch (_) {}
25+
await fs.mkdir(incrementalDir, { recursive: true });
26+
27+
try {
28+
await fs.access(buildDir);
29+
await fs.rm(buildDir, { recursive: true });
30+
} catch (_) {}
31+
await fs.mkdir(buildDir, { recursive: true });
32+
33+
const resFiles = await glob("**/*.res", {
34+
cwd: testFilesDir,
35+
absolute: true,
36+
}).then((files) =>
37+
files.map((file) => ({
38+
absolutePath: file,
39+
relativePath: path.relative(testFilesDir, file),
40+
}))
41+
);
42+
43+
// Function to split test file contents into blocks
44+
const splitTestBlocks = (contents) => {
45+
const testBlocks = contents.split(/\/\/ == TEST:/);
46+
// Skip the first empty block if it exists
47+
return testBlocks.slice(1).map((block) => {
48+
const [description, ...rest] = block.split("\n");
49+
return {
50+
description: description.trim(),
51+
content: rest.join("\n").trim(),
52+
};
53+
});
54+
};
55+
56+
const testBlocksPerFile = new Map();
57+
58+
const baseCommand = `./node_modules/.bin/bsc -I lib/bs/support_files -I node_modules/@rescript/react/lib/ocaml -editor-mode -ignore-parse-errors -color never -bs-package-name test -bs-package-output esmodule:test.res`;
59+
60+
// Compile all files and move incremental cmt's
61+
await Promise.all(
62+
resFiles.map(async (file) => {
63+
const contents = await fs.readFile(file.absolutePath, "utf-8");
64+
const testBlocks = splitTestBlocks(contents);
65+
66+
let blockIndex = 1;
67+
const blockData = [];
68+
69+
for (const block of testBlocks) {
70+
const { description, content } = block;
71+
const filePath = path.join(
72+
buildDir,
73+
`${file.relativePath.slice(0, -4)}_${blockIndex}.res`
74+
);
75+
76+
const fileContent = `// ${description}\n${content}`;
77+
78+
await fs.writeFile(filePath, fileContent);
79+
80+
const command = `${baseCommand} ${filePath}`;
81+
const [cmd, ...args] = command.split(" ");
82+
83+
const _debugData = await new Promise((resolve) => {
84+
const child = spawn(cmd, args);
85+
86+
let stdout = "";
87+
let stderr = "";
88+
89+
child.stdout.on("data", (chunk) => {
90+
stdout += chunk;
91+
});
92+
93+
child.stderr.on("data", (chunk) => {
94+
stderr += chunk;
95+
});
96+
97+
child.on("close", () => {
98+
resolve({ stdout, stderr });
99+
});
100+
});
101+
102+
// Move .cmt file to incremental directory
103+
const cmtPath = filePath.replace(".res", ".cmt");
104+
const cmtFileName = path.basename(cmtPath);
105+
const targetPath = path.join(incrementalDir, cmtFileName);
106+
await fs.rename(cmtPath, targetPath);
107+
108+
blockData.push({ filePath, description, fileContent });
109+
blockIndex++;
110+
}
111+
112+
testBlocksPerFile.set(file.relativePath, blockData);
113+
})
114+
);
115+
116+
resFiles.forEach((file) => {
117+
const blockData = testBlocksPerFile.get(file.relativePath);
118+
for (const block of blockData) {
119+
test(`${file.relativePath} - ${block.description}`, async (t) => {
120+
// Run rescript-editor-analysis and capture output
121+
const analysisOutput = await new Promise((resolve, reject) => {
122+
const analysisCmd = spawn(
123+
"../../../_build/install/default/bin/rescript-editor-analysis",
124+
["test_revamped", block.filePath, "rescript.test.json"],
125+
{
126+
stdio: "pipe",
127+
env: {
128+
RESCRIPT_INCREMENTAL_TYPECHECKING: "true",
129+
RESCRIPT_PROJECT_CONFIG_CACHE: "true",
130+
RESCRIPT_VERSION: rescriptVersion,
131+
},
132+
}
133+
);
134+
135+
let stdout = "";
136+
let stderr = "";
137+
138+
analysisCmd.stdout.on("data", (data) => {
139+
stdout += data.toString();
140+
});
141+
142+
analysisCmd.stderr.on("data", (data) => {
143+
stderr += data.toString();
144+
});
145+
146+
analysisCmd.on("close", (code) => {
147+
if (code === 0) {
148+
resolve({ stdout, stderr });
149+
} else {
150+
reject(new Error(`Analysis command failed with code ${code}`));
151+
console.error(stderr);
152+
}
153+
});
154+
});
155+
156+
t.assert.snapshot(analysisOutput.stdout);
157+
});
158+
}
159+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
exports[`RecordFieldCompletions.res - Record field completion in nested record 1`] = `
2+
"Complete /Users/zth/OSS/rescript-compiler/tests/analysis_new_tests/tests/test_files/.build/RecordFieldCompletions_1.res 1:38\\nposCursor:[1:38] posNoWhite:[1:37] Found expr:[1:8->1:38]\\nPackage opens Stdlib.place holder Pervasives.JsxModules.place holder\\nResolved opens 1 Stdlib\\n[{\\n \\"label\\": \\"test\\",\\n \\"kind\\": 5,\\n \\"tags\\": [],\\n \\"detail\\": \\"bool\\",\\n \\"documentation\\": {\\"kind\\": \\"markdown\\", \\"value\\": \\"\`\`\`rescript\\\\ntest: bool\\\\n\`\`\`\\\\n\\\\n\`\`\`rescript\\\\ntype nestedTestRecord = {\\\\n test: bool,\\\\n nested: {name: string, oneMoreLevel: {here: bool}},\\\\n}\\\\n\`\`\`\\"}\\n }, {\\n \\"label\\": \\"nested\\",\\n \\"kind\\": 5,\\n \\"tags\\": [],\\n \\"detail\\": \\"\\\\\\\\\\\\\\"nestedTestRecord.nested\\\\\\"\\",\\n \\"documentation\\": {\\"kind\\": \\"markdown\\", \\"value\\": \\"\`\`\`rescript\\\\nnested: \\\\\\\\\\\\\\"nestedTestRecord.nested\\\\\\"\\\\n\`\`\`\\\\n\\\\n\`\`\`rescript\\\\ntype nestedTestRecord = {\\\\n test: bool,\\\\n nested: {name: string, oneMoreLevel: {here: bool}},\\\\n}\\\\n\`\`\`\\"}\\n }]\\n\\n"
3+
`;
4+
5+
exports[`RecordFieldCompletions.res - Record field completion in nested record, another level 1`] = `
6+
"Complete /Users/zth/OSS/rescript-compiler/tests/analysis_new_tests/tests/test_files/.build/RecordFieldCompletions_2.res 1:45\\nposCursor:[1:45] posNoWhite:[1:44] Found expr:[1:8->1:45]\\nPackage opens Stdlib.place holder Pervasives.JsxModules.place holder\\nResolved opens 1 Stdlib\\n[{\\n \\"label\\": \\"name\\",\\n \\"kind\\": 5,\\n \\"tags\\": [],\\n \\"detail\\": \\"string\\",\\n \\"documentation\\": {\\"kind\\": \\"markdown\\", \\"value\\": \\"\`\`\`rescript\\\\nname: string\\\\n\`\`\`\\\\n\\\\n\`\`\`rescript\\\\ntype \\\\\\\\\\\\\\"nestedTestRecord.nested\\\\\\" = {\\\\n name: string,\\\\n oneMoreLevel: {here: bool},\\\\n}\\\\n\`\`\`\\"}\\n }, {\\n \\"label\\": \\"oneMoreLevel\\",\\n \\"kind\\": 5,\\n \\"tags\\": [],\\n \\"detail\\": \\"\\\\\\\\\\\\\\"nestedTestRecord.nested.oneMoreLevel\\\\\\"\\",\\n \\"documentation\\": {\\"kind\\": \\"markdown\\", \\"value\\": \\"\`\`\`rescript\\\\noneMoreLevel: \\\\\\\\\\\\\\"nestedTestRecord.nested.oneMoreLevel\\\\\\"\\\\n\`\`\`\\\\n\\\\n\`\`\`rescript\\\\ntype \\\\\\\\\\\\\\"nestedTestRecord.nested\\\\\\" = {\\\\n name: string,\\\\n oneMoreLevel: {here: bool},\\\\n}\\\\n\`\`\`\\"}\\n }]\\n\\n"
7+
`;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.build
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// == TEST: Record field completion in nested record
2+
let x = TestTypeDefs.nestedTestRecord.
3+
// ^com
4+
5+
// == TEST: Record field completion in nested record, another level
6+
let x = TestTypeDefs.nestedTestRecord.nested.
7+
// ^com

yarn.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ __metadata:
400400
languageName: node
401401
linkType: soft
402402

403+
"@rescript/react@link:../../dependencies/rescript-react::locator=%40tests%2Fanalysis_new%40workspace%3Atests%2Fanalysis_new_tests%2Ftests":
404+
version: 0.0.0-use.local
405+
resolution: "@rescript/react@link:../../dependencies/rescript-react::locator=%40tests%2Fanalysis_new%40workspace%3Atests%2Fanalysis_new_tests%2Ftests"
406+
languageName: node
407+
linkType: soft
408+
403409
"@rescript/react@link:../dependencies/rescript-react::locator=%40tests%2Ftools%40workspace%3Atests%2Ftools_tests":
404410
version: 0.0.0-use.local
405411
resolution: "@rescript/react@link:../dependencies/rescript-react::locator=%40tests%2Ftools%40workspace%3Atests%2Ftools_tests"
@@ -611,6 +617,15 @@ __metadata:
611617
languageName: unknown
612618
linkType: soft
613619

620+
"@tests/analysis_new@workspace:tests/analysis_new_tests/tests":
621+
version: 0.0.0-use.local
622+
resolution: "@tests/analysis_new@workspace:tests/analysis_new_tests/tests"
623+
dependencies:
624+
"@rescript/react": "link:../../dependencies/rescript-react"
625+
rescript: "workspace:^"
626+
languageName: unknown
627+
linkType: soft
628+
614629
"@tests/generic-jsx-transform@workspace:tests/analysis_tests/tests-generic-jsx-transform":
615630
version: 0.0.0-use.local
616631
resolution: "@tests/generic-jsx-transform@workspace:tests/analysis_tests/tests-generic-jsx-transform"

0 commit comments

Comments
 (0)