Skip to content

Commit 1ca8b90

Browse files
ckairenmikeharder
andauthored
[TypeSpec Validation] tsv folder path standardization (#26721)
* normalizing path * windows slash * Add unit tests * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * Update eng/tools/typespec-validation/test/folder-structure.test.ts Co-authored-by: Mike Harder <mharder@microsoft.com> * test * remove drive letter * regex format * Update eng/tools/typespec-validation/src/index.ts --------- Co-authored-by: Mike Harder <mharder@microsoft.com>
1 parent 61323f3 commit 1ca8b90

File tree

10 files changed

+220
-12
lines changed

10 files changed

+220
-12
lines changed

eng/tools/typespec-validation/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { FormatRule } from "./rules/format.js";
66
import { GitDiffRule } from "./rules/git-diff.js";
77
import { LinterRulesetRule } from "./rules/linter-ruleset.js";
88
import { NpmPrefixRule } from "./rules/npm-prefix.js";
9-
import path from "path";
109
import { TsvRunnerHost } from "./tsv-runner-host.js";
1110

1211
export async function main() {
12+
const host = new TsvRunnerHost();
1313
const args = process.argv.slice(2);
1414
const options = {
1515
folder: {
@@ -18,10 +18,10 @@ export async function main() {
1818
},
1919
};
2020
const parsedArgs = parseArgs({ args, options, allowPositionals: true } as ParseArgsConfig);
21-
const folder = parsedArgs.positionals[0].split(path.sep).join("/");
22-
console.log("Running TypeSpecValidation on folder:", folder);
21+
const folder = parsedArgs.positionals[0];
22+
const absolutePath = host.normalizePath(folder);
2323

24-
const host = new TsvRunnerHost();
24+
console.log("Running TypeSpecValidation on folder: ", absolutePath);
2525

2626
const rules = [
2727
new FolderStructureRule(),
@@ -36,7 +36,7 @@ export async function main() {
3636
for (let i = 0; i < rules.length; i++) {
3737
const rule = rules[i];
3838
console.log("\nExecuting rule: " + rule.name);
39-
const result = await rule.execute(host, folder);
39+
const result = await rule.execute(host, absolutePath);
4040
if (result.stdOutput) console.log(result.stdOutput);
4141
if (!result.success) {
4242
success = false;

eng/tools/typespec-validation/src/rules/folder-structure.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { globby } from "globby";
21
import path from "path";
32
import { Rule } from "../rule.js";
43
import { RuleResult } from "../rule-result.js";
@@ -11,6 +10,8 @@ export class FolderStructureRule implements Rule {
1110
let success = true;
1211
let stdOutput = "";
1312
let errorOutput = "";
13+
let gitRoot = host.normalizePath(await host.gitOperation(folder).revparse("--show-toplevel"));
14+
let relativePath = path.relative(gitRoot, folder).split(path.sep).join("/");
1415

1516
stdOutput += `folder: ${folder}\n`;
1617
if (!(await host.checkFileExists(folder))) {
@@ -21,7 +22,7 @@ export class FolderStructureRule implements Rule {
2122
};
2223
}
2324

24-
const tspConfigs = await globby([`${folder}/**tspconfig.*`]);
25+
const tspConfigs = await host.globby([`${folder}/**tspconfig.*`]);
2526
stdOutput += `config files: ${JSON.stringify(tspConfigs)}\n`;
2627
tspConfigs.forEach((file: string) => {
2728
if (!file.endsWith("tspconfig.yaml")) {
@@ -31,7 +32,7 @@ export class FolderStructureRule implements Rule {
3132
});
3233

3334
// Verify top level folder is lower case
34-
let folderStruct = folder.split("/");
35+
let folderStruct = relativePath.split("/");
3536
if (folderStruct[1].match(/[A-Z]/g)) {
3637
success = false;
3738
errorOutput += `Invalid folder name. Folders under specification/ must be lower case.\n`;

eng/tools/typespec-validation/src/rules/npm-prefix.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import path from "path";
21
import { Rule } from "../rule.js";
32
import { RuleResult } from "../rule-result.js";
43
import { TsvHost } from "../tsv-host.js";
@@ -13,7 +12,7 @@ export class NpmPrefixRule implements Rule {
1312
let expected_npm_prefix: string | undefined;
1413
try {
1514
// If spec folder is inside a git repo, returns repo root
16-
expected_npm_prefix = path.normalize(await git.revparse("--show-toplevel"));
15+
expected_npm_prefix = host.normalizePath(await git.revparse("--show-toplevel"));
1716
} catch (err) {
1817
// If spec folder is outside git repo, or if problem running git, throws error
1918
return {
@@ -22,7 +21,9 @@ export class NpmPrefixRule implements Rule {
2221
};
2322
}
2423

25-
const actual_npm_prefix = path.normalize((await host.runCmd(`npm prefix`, folder))[1].trim());
24+
const actual_npm_prefix = host.normalizePath(
25+
(await host.runCmd(`npm prefix`, folder))[1].trim(),
26+
);
2627

2728
let success = true;
2829
let stdOutput =

eng/tools/typespec-validation/src/tsv-host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export interface TsvHost {
33
gitOperation(folder: string): IGitOperation;
44
readTspConfig(folder: string): Promise<string>;
55
runCmd(cmd: string, cwd: string): Promise<[Error | null, string, string]>;
6+
normalizePath(folder: string): string;
7+
globby(patterns: string[]): Promise<string[]>;
68
}
79

810
export interface IGitOperation {

eng/tools/typespec-validation/src/tsv-runner-host.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { join } from "path";
22
import { readFile } from "fs/promises";
33
import { IGitOperation, TsvHost } from "./tsv-host.js";
4+
import { globby } from "globby";
45
import { simpleGit } from "simple-git";
5-
import { runCmd, checkFileExists } from "./utils.js";
6+
import { checkFileExists, normalizePath, runCmd } from "./utils.js";
67

78
export class TsvRunnerHost implements TsvHost {
89
checkFileExists(file: string): Promise<boolean> {
@@ -20,4 +21,12 @@ export class TsvRunnerHost implements TsvHost {
2021
runCmd(cmd: string, cwd: string): Promise<[Error | null, string, string]> {
2122
return runCmd(cmd, cwd);
2223
}
24+
25+
normalizePath(folder: string): string {
26+
return normalizePath(folder);
27+
}
28+
29+
globby(patterns: string[]): Promise<string[]> {
30+
return globby(patterns);
31+
}
2332
}

eng/tools/typespec-validation/src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { access } from "fs/promises";
22
import { exec } from "child_process";
3+
import path from "path";
34

45
export async function runCmd(cmd: string, cwd: string) {
56
console.log(`run command:${cmd}`);
@@ -20,3 +21,7 @@ export async function checkFileExists(file: string) {
2021
.then(() => true)
2122
.catch(() => false);
2223
}
24+
25+
export function normalizePath(folder: string) {
26+
return path.resolve(folder).split(path.sep).join("/");
27+
}

eng/tools/typespec-validation/test/emit-autorest.ts renamed to eng/tools/typespec-validation/test/emit-autorest.test.ts

File renamed without changes.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { FolderStructureRule } from "../src/rules/folder-structure.js";
2+
import { TsvTestHost } from "./tsv-test-host.js";
3+
import { strict as assert } from "node:assert";
4+
5+
describe("folder-structure", function () {
6+
it("should fail if tspconfig has incorrect extension", async function () {
7+
let host = new TsvTestHost();
8+
host.globby = async () => {
9+
return ["/foo/bar/tspconfig.yml"];
10+
};
11+
12+
const result = await new FolderStructureRule().execute(host, TsvTestHost.folder);
13+
assert(result.errorOutput);
14+
assert(result.errorOutput.includes("Invalid config file"));
15+
});
16+
17+
it("should fail if folder under specification/ is capitalized", async function () {
18+
let host = new TsvTestHost();
19+
host.globby = async () => {
20+
return ["/foo/bar/tspconfig.yaml"];
21+
};
22+
host.normalizePath = () => {
23+
return "/gitroot";
24+
};
25+
26+
const result = await new FolderStructureRule().execute(host, "/gitroot/specification/Foo/Foo");
27+
assert(result.errorOutput);
28+
assert(result.errorOutput.includes("must be lower case"));
29+
});
30+
31+
it("should fail if package folder is more than 3 levels deep", async function () {
32+
let host = new TsvTestHost();
33+
host.globby = async () => {
34+
return ["/foo/bar/tspconfig.yaml"];
35+
};
36+
host.normalizePath = () => {
37+
return "/gitroot";
38+
};
39+
40+
const result = await new FolderStructureRule().execute(
41+
host,
42+
"/gitroot/specification/foo/Foo/Foo/Foo",
43+
);
44+
assert(result.errorOutput);
45+
assert(result.errorOutput.includes("3 levels or less"));
46+
});
47+
48+
it("should fail if second level folder not capitalized at after each '.' ", async function () {
49+
let host = new TsvTestHost();
50+
host.globby = async () => {
51+
return ["/foo/bar/tspconfig.yaml"];
52+
};
53+
host.normalizePath = () => {
54+
return "/gitroot";
55+
};
56+
57+
const result = await new FolderStructureRule().execute(
58+
host,
59+
"/gitroot/specification/foo/Foo.foo",
60+
);
61+
assert(result.errorOutput);
62+
assert(result.errorOutput.includes("must be capitalized"));
63+
});
64+
65+
it("should fail if Shared does not follow Management ", async function () {
66+
let host = new TsvTestHost();
67+
host.globby = async () => {
68+
return ["/foo/bar/tspconfig.yaml"];
69+
};
70+
host.normalizePath = () => {
71+
return "/gitroot";
72+
};
73+
74+
const result = await new FolderStructureRule().execute(
75+
host,
76+
"/gitroot/specification/foo/Foo.Management.Foo.Shared",
77+
);
78+
assert(result.errorOutput);
79+
assert(result.errorOutput.includes("should follow"));
80+
});
81+
82+
it("should fail if folder doesn't contain main.tsp nor client.tsp", async function () {
83+
let host = new TsvTestHost();
84+
host.globby = async () => {
85+
return ["/foo/bar/tspconfig.yaml"];
86+
};
87+
host.normalizePath = () => {
88+
return "/gitroot";
89+
};
90+
host.checkFileExists = async (file: string) => {
91+
if (file.includes("main.tsp")) {
92+
return false;
93+
} else if (file.includes("client.tsp")) {
94+
return false;
95+
}
96+
return true;
97+
};
98+
99+
const result = await new FolderStructureRule().execute(
100+
host,
101+
"/gitroot/specification/foo/Foo.Management",
102+
);
103+
104+
assert(result.errorOutput);
105+
assert(result.errorOutput.includes("must contain"));
106+
});
107+
108+
it("should fail if folder doesn't contain examples when main.tsp exists", async function () {
109+
let host = new TsvTestHost();
110+
host.globby = async () => {
111+
return ["/foo/bar/tspconfig.yaml"];
112+
};
113+
host.normalizePath = () => {
114+
return "/gitroot";
115+
};
116+
host.checkFileExists = async (file: string) => {
117+
if (file.includes("main.tsp")) {
118+
return true;
119+
} else if (file.includes("examples")) {
120+
return false;
121+
}
122+
return true;
123+
};
124+
125+
const result = await new FolderStructureRule().execute(
126+
host,
127+
"/gitroot/specification/foo/Foo.Management",
128+
);
129+
130+
assert(result.errorOutput);
131+
assert(result.errorOutput.includes("must contain"));
132+
});
133+
134+
it("should fail if non-shared folder doesn't contain tspconfig", async function () {
135+
let host = new TsvTestHost();
136+
host.globby = async () => {
137+
return ["/foo/bar/tspconfig.yaml"];
138+
};
139+
host.normalizePath = () => {
140+
return "/gitroot";
141+
};
142+
host.checkFileExists = async (file: string) => {
143+
if (file.includes("tspconfig.yaml")) {
144+
return false;
145+
}
146+
return true;
147+
};
148+
149+
const result = await new FolderStructureRule().execute(
150+
host,
151+
"/gitroot/specification/foo/Foo.Management",
152+
);
153+
154+
assert(result.errorOutput);
155+
assert(result.errorOutput.includes("must contain"));
156+
});
157+
});

eng/tools/typespec-validation/test/tsv-test-host.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { IGitOperation, TsvHost } from "../src/tsv-host.js";
2+
import { normalizePath } from "../src/utils.js";
23

34
export class TsvTestHost implements TsvHost {
45
static get folder() {
@@ -34,6 +35,10 @@ export class TsvTestHost implements TsvHost {
3435
return true;
3536
}
3637

38+
normalizePath(folder: string): string {
39+
return normalizePath(folder);
40+
}
41+
3742
async readTspConfig(_folder: string): Promise<string> {
3843
// Sample config that should cause all rules to succeed
3944
return `
@@ -50,4 +55,8 @@ options:
5055
output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/openapi.json"
5156
`;
5257
}
58+
59+
async globby(patterns: string[]): Promise<string[]> {
60+
return Promise.resolve(patterns);
61+
}
5362
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { TsvTestHost } from "./tsv-test-host.js";
2+
import { strict as assert } from "node:assert";
3+
import process from "process";
4+
5+
describe("normalize", function () {
6+
it("should succeed if normalized . and normalized cwd matches", async function () {
7+
let host = new TsvTestHost();
8+
const dotResult = host.normalizePath(".");
9+
const cwdResult = host.normalizePath(process.cwd());
10+
assert(dotResult === cwdResult);
11+
});
12+
13+
it("should succeed if /foo/bar/ is normalized", async function () {
14+
let host = new TsvTestHost();
15+
const result = host.normalizePath("/foo/bar/").replace(/^[a-zA-Z]:/g, "");
16+
assert(result === "/foo/bar");
17+
});
18+
19+
it("should succeed if /foo/bar is normalized", async function () {
20+
let host = new TsvTestHost();
21+
const result = host.normalizePath("/foo/bar").replace(/^[a-zA-Z]:/g, "");
22+
assert(result === "/foo/bar");
23+
});
24+
});

0 commit comments

Comments
 (0)