@flint.fyi/rule-tester 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +57 -33
- package/package.json +6 -5
package/dist/index.d.mts
CHANGED
|
@@ -61,10 +61,10 @@ interface TestCases<Options extends object | undefined> {
|
|
|
61
61
|
}
|
|
62
62
|
type TesterSetupDescribe = (description: string, setup: () => void) => void;
|
|
63
63
|
type TesterSetupIt = (description: string, setup: () => Promise<void>) => void;
|
|
64
|
-
declare class RuleTester {
|
|
64
|
+
export declare class RuleTester {
|
|
65
65
|
#private;
|
|
66
66
|
constructor({ assertNoLanguageReports, defaults, describe, diskBackedFSRoot, it, only, scope, skip }?: RuleTesterOptions);
|
|
67
67
|
describe<OptionsSchema extends AnyOptionalSchema | undefined>(rule: AnyRule<RuleAbout, OptionsSchema>, { invalid, valid }: TestCases<InferredInputObject<OptionsSchema>>): void;
|
|
68
68
|
}
|
|
69
69
|
//#endregion
|
|
70
|
-
export {
|
|
70
|
+
export type { InvalidTestCase, TestCase, TestSuggestion, TestSuggestionFileCase, TestSuggestionForFile, TestSuggestionForFiles, ValidTestCase, ValidTestCaseObject };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { CachedFactory } from "cached-factory";
|
|
4
|
-
import {
|
|
4
|
+
import { resolve } from "pathe";
|
|
5
|
+
import { applyChangesToText, createDiskBackedLinterHost, createEphemeralLinterHost, createVFSLinterHost, formatReport, getPositionOfColumnAndLine, isSuggestionForFiles, parseOptions, processRuleReport, withRepositoryRoot } from "@flint.fyi/core";
|
|
5
6
|
import { isTruthy, normalizePath, nullThrows, pathKey } from "@flint.fyi/utils";
|
|
6
7
|
//#region src/createOutput.ts
|
|
7
8
|
function createOutput(reports, testCaseNormalized) {
|
|
@@ -98,38 +99,41 @@ function isTestSuggestionForFiles(suggestion) {
|
|
|
98
99
|
function resolveReportedSuggestions(reports, testCaseNormalized) {
|
|
99
100
|
const suggestionsReported = reports.flatMap((report) => report.suggestions).filter(isTruthy);
|
|
100
101
|
if (!suggestionsReported.length) return;
|
|
101
|
-
return suggestionsReported.map((suggestionReported) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
return suggestionsReported.map((suggestionReported, index) => {
|
|
103
|
+
const suggestionExpected = testCaseNormalized.suggestions?.[index];
|
|
104
|
+
if (isSuggestionForFiles(suggestionReported)) return {
|
|
105
|
+
files: resolveReportedSuggestionForFiles(suggestionReported, suggestionExpected),
|
|
106
|
+
id: suggestionReported.id
|
|
107
|
+
};
|
|
108
|
+
if (suggestionExpected && isTestSuggestionForFiles(suggestionExpected)) throw new Error("This test case describes a suggestion across files, but the rule is only reporting changes to its own file.");
|
|
109
|
+
return {
|
|
110
|
+
id: suggestionReported.id,
|
|
111
|
+
updated: applyChangesToText([suggestionReported], testCaseNormalized.code)
|
|
112
|
+
};
|
|
107
113
|
});
|
|
108
114
|
}
|
|
109
|
-
function resolveReportedSuggestionForFiles(suggestionReported,
|
|
110
|
-
if (!
|
|
111
|
-
if (!
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})];
|
|
121
|
-
});
|
|
115
|
+
function resolveReportedSuggestionForFiles(suggestionReported, suggestionExpected) {
|
|
116
|
+
if (!suggestionExpected) return {};
|
|
117
|
+
if (!isTestSuggestionForFiles(suggestionExpected)) throw new Error("This test case describes a suggestion to its own file, but the rule is reporting changes across files.");
|
|
118
|
+
assert.deepStrictEqual(Object.keys(suggestionReported.files).sort(), Object.keys(suggestionExpected.files).sort(), "Reported suggestion target paths must exactly match expected target paths.");
|
|
119
|
+
return Object.fromEntries(Object.entries(suggestionExpected.files).map(([filePath, suggestionCasesExpected]) => {
|
|
120
|
+
const changes = suggestionReported.files[filePath];
|
|
121
|
+
assert.ok(changes, `Expected reported suggestion "${suggestionReported.id}" to provide changes for target "${filePath}".`);
|
|
122
|
+
return [filePath, suggestionCasesExpected.map((suggestionCaseExpected) => ({
|
|
123
|
+
original: suggestionCaseExpected.original,
|
|
124
|
+
updated: applyChangesToText(changes, suggestionCaseExpected.original)
|
|
125
|
+
}))];
|
|
122
126
|
}));
|
|
123
127
|
}
|
|
124
128
|
//#endregion
|
|
125
129
|
//#region src/runTestCaseRule.ts
|
|
126
130
|
async function runTestCaseRule(fileFactories, linterHost, { options, rule }, { code, fileName, files }, { collectLanguageReports = false } = {}) {
|
|
127
|
-
const filePathAbsolute = normalizePath(
|
|
131
|
+
const filePathAbsolute = normalizePath(resolve(linterHost.getCurrentDirectory(), fileName));
|
|
128
132
|
const caseSensitive = linterHost.isCaseSensitiveFS();
|
|
129
133
|
const targetKey = pathKey(filePathAbsolute, caseSensitive);
|
|
130
134
|
for (const oldFile of linterHost.vfsListFiles().keys()) if (pathKey(oldFile, caseSensitive) !== targetKey) linterHost.vfsDeleteFile(oldFile);
|
|
131
135
|
for (const [name, content] of Object.entries(files ?? {})) {
|
|
132
|
-
const filePath = normalizePath(
|
|
136
|
+
const filePath = normalizePath(resolve(linterHost.getCurrentDirectory(), name));
|
|
133
137
|
assert.notEqual(filePath, filePathAbsolute, `Expected 'files' not to shadow '${fileName}'`);
|
|
134
138
|
linterHost.vfsUpsertFile(filePath, content);
|
|
135
139
|
}
|
|
@@ -149,7 +153,13 @@ async function runTestCaseRule(fileFactories, linterHost, { options, rule }, { c
|
|
|
149
153
|
}
|
|
150
154
|
});
|
|
151
155
|
if (ruleRuntime) {
|
|
152
|
-
rule.language.runFileVisitors(file,
|
|
156
|
+
if (ruleRuntime.visitors) rule.language.runFileVisitors(file, [{
|
|
157
|
+
services: {
|
|
158
|
+
options,
|
|
159
|
+
...file.services
|
|
160
|
+
},
|
|
161
|
+
visitors: ruleRuntime.visitors
|
|
162
|
+
}]);
|
|
153
163
|
await ruleRuntime.teardown?.();
|
|
154
164
|
}
|
|
155
165
|
return {
|
|
@@ -164,12 +174,13 @@ var RuleTester = class {
|
|
|
164
174
|
#linterHost;
|
|
165
175
|
#testerOptions;
|
|
166
176
|
constructor({ assertNoLanguageReports = true, defaults = {}, describe, diskBackedFSRoot, it, only, scope = globalThis, skip } = {}) {
|
|
167
|
-
|
|
177
|
+
const virtualRoot = diskBackedFSRoot == null ? void 0 : resolve(process.cwd(), diskBackedFSRoot, "_flint-rule-tester-virtual");
|
|
178
|
+
let baseHost = virtualRoot != null ? createEphemeralLinterHost(withRepositoryRoot(createDiskBackedLinterHost(virtualRoot), virtualRoot)) : void 0;
|
|
168
179
|
const { files: defaultFiles = {} } = defaults;
|
|
169
180
|
if (Object.keys(defaultFiles).length) {
|
|
170
181
|
const vfs = createVFSLinterHost(baseHost == null ? { cwd: process.cwd() } : { baseHost });
|
|
171
182
|
for (const [name, content] of Object.entries(defaultFiles)) {
|
|
172
|
-
const filePath =
|
|
183
|
+
const filePath = resolve(vfs.getCurrentDirectory(), name);
|
|
173
184
|
vfs.vfsUpsertFile(filePath, content);
|
|
174
185
|
}
|
|
175
186
|
baseHost = vfs;
|
|
@@ -194,16 +205,18 @@ var RuleTester = class {
|
|
|
194
205
|
describe(rule, { invalid, valid }) {
|
|
195
206
|
this.#testerOptions.describe(rule.about.id, () => {
|
|
196
207
|
this.#testerOptions.describe("invalid", () => {
|
|
197
|
-
|
|
208
|
+
const seenTestCases = [];
|
|
209
|
+
for (const testCase of invalid) this.#itInvalidCase(rule, testCase, seenTestCases);
|
|
198
210
|
});
|
|
199
211
|
this.#testerOptions.describe("valid", () => {
|
|
200
|
-
|
|
212
|
+
const seenTestCases = [];
|
|
213
|
+
for (const testCase of valid) this.#itValidCase(rule, testCase, seenTestCases);
|
|
201
214
|
});
|
|
202
215
|
});
|
|
203
216
|
}
|
|
204
|
-
#itInvalidCase(rule, testCase) {
|
|
217
|
+
#itInvalidCase(rule, testCase, seenTestCases) {
|
|
205
218
|
const testCaseNormalized = normalizeTestCase(testCase, this.#testerOptions.defaults.fileName);
|
|
206
|
-
this.#itTestCase(testCaseNormalized, async () => {
|
|
219
|
+
this.#itTestCase(testCaseNormalized, seenTestCases, async () => {
|
|
207
220
|
const { languageReports, reports } = await runTestCaseRule(this.#fileFactories, this.#linterHost, {
|
|
208
221
|
options: parseOptions(rule.options, testCase.options),
|
|
209
222
|
rule
|
|
@@ -217,7 +230,7 @@ var RuleTester = class {
|
|
|
217
230
|
assert.deepStrictEqual(actualSuggestions, testCase.suggestions);
|
|
218
231
|
});
|
|
219
232
|
}
|
|
220
|
-
#itTestCase(testCase, setup) {
|
|
233
|
+
#itTestCase(testCase, seenTestCases, setup) {
|
|
221
234
|
let test = testCase.only ? this.#testerOptions.only : this.#testerOptions.it;
|
|
222
235
|
if (testCase.skip) {
|
|
223
236
|
if ("skip" in test && typeof test.skip === "function") test = test.skip;
|
|
@@ -227,14 +240,15 @@ var RuleTester = class {
|
|
|
227
240
|
[testCase.fileName]: testCase.code,
|
|
228
241
|
...testCase.files
|
|
229
242
|
}, null, 2) : testCase.code), () => {
|
|
243
|
+
assertNoDuplicateTestCase(testCase, seenTestCases);
|
|
230
244
|
if (testCase.files != null) assert.notEqual(Object.keys(testCase.files).length, 0, `'files' must have at least one file`);
|
|
231
245
|
return setup();
|
|
232
246
|
});
|
|
233
247
|
}
|
|
234
|
-
#itValidCase(rule, testCaseRaw) {
|
|
248
|
+
#itValidCase(rule, testCaseRaw, seenTestCases) {
|
|
235
249
|
const testCase = typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
|
|
236
250
|
const testCaseNormalized = normalizeTestCase(testCase, this.#testerOptions.defaults.fileName);
|
|
237
|
-
this.#itTestCase(testCaseNormalized, async () => {
|
|
251
|
+
this.#itTestCase(testCaseNormalized, seenTestCases, async () => {
|
|
238
252
|
const { languageReports, reports } = await runTestCaseRule(this.#fileFactories, this.#linterHost, {
|
|
239
253
|
options: parseOptions(rule.options, testCase.options),
|
|
240
254
|
rule
|
|
@@ -244,6 +258,16 @@ var RuleTester = class {
|
|
|
244
258
|
});
|
|
245
259
|
}
|
|
246
260
|
};
|
|
261
|
+
function assertNoDuplicateTestCase(testCase, seenTestCases) {
|
|
262
|
+
const duplicateProperties = {
|
|
263
|
+
code: testCase.code,
|
|
264
|
+
fileName: testCase.fileName,
|
|
265
|
+
files: testCase.files,
|
|
266
|
+
options: testCase.options
|
|
267
|
+
};
|
|
268
|
+
if (seenTestCases.some((seenTestCase) => isDeepStrictEqual(seenTestCase, duplicateProperties))) assert.fail("Expected no duplicate test cases, but an earlier test case has the same code, fileName, files, and options.");
|
|
269
|
+
seenTestCases.push(duplicateProperties);
|
|
270
|
+
}
|
|
247
271
|
function assertNoLanguageReports(languageReports) {
|
|
248
272
|
if (languageReports.length) assert.fail([`Expected no language reports, but found ${languageReports.length}:`, ...languageReports.map((languageReport) => languageReport.text)].join("\n\n"));
|
|
249
273
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flint.fyi/rule-tester",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "[Experimental] Rule unit tests for Flint.",
|
|
5
5
|
"homepage": "https://flint.fyi",
|
|
6
6
|
"repository": {
|
|
@@ -22,14 +22,15 @@
|
|
|
22
22
|
"dist/"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@flint.fyi/core": "^0.
|
|
25
|
+
"@flint.fyi/core": "^0.27.0",
|
|
26
26
|
"@flint.fyi/utils": "^0.16.0",
|
|
27
|
-
"cached-factory": "^0.3.0"
|
|
27
|
+
"cached-factory": "^0.3.0",
|
|
28
|
+
"pathe": "^2.0.3"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@flint.fyi/build": "^0.1.0",
|
|
31
|
-
"tsdown": "0.
|
|
32
|
-
"vitest": "
|
|
32
|
+
"tsdown": "0.23.0",
|
|
33
|
+
"vitest": "5.0.1"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=26.1.0"
|