Skip to content

Commit a992ca0

Browse files
feat: test all APIs
1 parent 45310b9 commit a992ca0

File tree

5 files changed

+288
-118
lines changed

5 files changed

+288
-118
lines changed

src/clippy.ts

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import * as core from "@actions/core";
2+
import * as exec from "@actions/exec";
3+
import { Cargo, Cross } from "@actions-rs-plus/core";
4+
5+
import type * as input from "./input";
6+
import { OutputParser } from "./outputParser";
7+
import { Reporter } from "./reporter";
8+
import type { AnnotationWithMessageAndLevel, Context, Stats } from "./schema";
9+
10+
type Program = Cargo | Cross;
11+
12+
interface ClippyResult {
13+
stats: Stats;
14+
annotations: AnnotationWithMessageAndLevel[];
15+
exitCode: number;
16+
}
17+
18+
async function buildContext(program: Program): Promise<Context> {
19+
const context: Context = {
20+
cargo: "",
21+
clippy: "",
22+
rustc: "",
23+
};
24+
25+
await Promise.all([
26+
await exec.exec("rustc", ["-V"], {
27+
silent: true,
28+
listeners: {
29+
stdout: (buffer: Buffer) => {
30+
return (context.rustc = buffer.toString().trim());
31+
},
32+
},
33+
}),
34+
await program.call(["-V"], {
35+
silent: true,
36+
listeners: {
37+
stdout: (buffer: Buffer) => {
38+
return (context.cargo = buffer.toString().trim());
39+
},
40+
},
41+
}),
42+
await program.call(["clippy", "-V"], {
43+
silent: true,
44+
listeners: {
45+
stdout: (buffer: Buffer) => {
46+
return (context.clippy = buffer.toString().trim());
47+
},
48+
},
49+
}),
50+
]);
51+
52+
return context;
53+
}
54+
55+
async function runClippy(actionInput: input.ParsedInput, program: Program): Promise<ClippyResult> {
56+
const args = buildArgs(actionInput);
57+
const outputParser = new OutputParser();
58+
59+
let exitCode = 0;
60+
61+
try {
62+
core.startGroup("Executing cargo clippy (JSON output)");
63+
exitCode = await program.call(args, {
64+
ignoreReturnCode: true,
65+
failOnStdErr: false,
66+
listeners: {
67+
stdline: (line: string) => {
68+
outputParser.tryParseClippyLine(line);
69+
},
70+
},
71+
});
72+
} finally {
73+
core.endGroup();
74+
}
75+
76+
return {
77+
stats: outputParser.stats,
78+
annotations: outputParser.annotations,
79+
exitCode,
80+
};
81+
}
82+
83+
function getProgram(useCross: boolean): Promise<Program> {
84+
if (useCross) {
85+
return Cross.getOrInstall();
86+
} else {
87+
return Cargo.get();
88+
}
89+
}
90+
91+
export async function run(actionInput: input.ParsedInput): Promise<void> {
92+
const program: Program = await getProgram(actionInput.useCross);
93+
94+
const context = await buildContext(program);
95+
96+
const { stats, annotations, exitCode } = await runClippy(actionInput, program);
97+
98+
await new Reporter().report(stats, annotations, context);
99+
100+
if (exitCode !== 0) {
101+
throw new Error(`Clippy had exited with the ${exitCode} exit code`);
102+
}
103+
}
104+
105+
function buildArgs(actionInput: input.ParsedInput): string[] {
106+
const args: string[] = [];
107+
108+
// Toolchain selection MUST go first in any condition
109+
if (actionInput.toolchain) {
110+
args.push(`+${actionInput.toolchain}`);
111+
}
112+
113+
args.push("clippy");
114+
115+
// `--message-format=json` should just right after the `cargo clippy`
116+
// because usually people are adding the `-- -D warnings` at the end
117+
// of arguments and it will mess up the output.
118+
args.push("--message-format=json");
119+
120+
return args.concat(actionInput.args);
121+
}

src/main.ts

+1-117
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,8 @@
11
import * as core from "@actions/core";
2-
import * as exec from "@actions/exec";
3-
import { Cargo, Cross } from "@actions-rs-plus/core";
42

53
import * as input from "./input";
6-
import { OutputParser } from "./outputParser";
7-
import { Reporter } from "./reporter";
8-
import type { AnnotationWithMessageAndLevel, Context, Stats } from "./schema";
94

10-
type Program = Cargo | Cross;
11-
12-
interface ClippyResult {
13-
stats: Stats;
14-
annotations: AnnotationWithMessageAndLevel[];
15-
exitCode: number;
16-
}
17-
18-
async function buildContext(program: Program): Promise<Context> {
19-
const context: Context = {
20-
cargo: "",
21-
clippy: "",
22-
rustc: "",
23-
};
24-
25-
await Promise.all([
26-
await exec.exec("rustc", ["-V"], {
27-
silent: true,
28-
listeners: {
29-
stdout: (buffer: Buffer) => {
30-
return (context.rustc = buffer.toString().trim());
31-
},
32-
},
33-
}),
34-
await program.call(["-V"], {
35-
silent: true,
36-
listeners: {
37-
stdout: (buffer: Buffer) => {
38-
return (context.cargo = buffer.toString().trim());
39-
},
40-
},
41-
}),
42-
await program.call(["clippy", "-V"], {
43-
silent: true,
44-
listeners: {
45-
stdout: (buffer: Buffer) => {
46-
return (context.clippy = buffer.toString().trim());
47-
},
48-
},
49-
}),
50-
]);
51-
52-
return context;
53-
}
54-
55-
async function runClippy(actionInput: input.ParsedInput, program: Program): Promise<ClippyResult> {
56-
const args = buildArgs(actionInput);
57-
const outputParser = new OutputParser();
58-
59-
let exitCode = 0;
60-
61-
try {
62-
core.startGroup("Executing cargo clippy (JSON output)");
63-
exitCode = await program.call(args, {
64-
ignoreReturnCode: true,
65-
failOnStdErr: false,
66-
listeners: {
67-
stdline: (line: string) => {
68-
outputParser.tryParseClippyLine(line);
69-
},
70-
},
71-
});
72-
} finally {
73-
core.endGroup();
74-
}
75-
76-
return {
77-
stats: outputParser.stats,
78-
annotations: outputParser.annotations,
79-
exitCode,
80-
};
81-
}
82-
83-
function getProgram(useCross: boolean): Promise<Program> {
84-
if (useCross) {
85-
return Cross.getOrInstall();
86-
} else {
87-
return Cargo.get();
88-
}
89-
}
90-
91-
export async function run(actionInput: input.ParsedInput): Promise<void> {
92-
const program: Program = await getProgram(actionInput.useCross);
93-
94-
const context = await buildContext(program);
95-
96-
const { stats, annotations, exitCode } = await runClippy(actionInput, program);
97-
98-
await new Reporter().report(stats, annotations, context);
99-
100-
if (exitCode !== 0) {
101-
throw new Error(`Clippy had exited with the ${exitCode} exit code`);
102-
}
103-
}
5+
import { run } from "clippy";
1046

1057
async function main(): Promise<void> {
1068
try {
@@ -117,22 +19,4 @@ async function main(): Promise<void> {
11719
}
11820
}
11921

120-
function buildArgs(actionInput: input.ParsedInput): string[] {
121-
const args: string[] = [];
122-
123-
// Toolchain selection MUST go first in any condition
124-
if (actionInput.toolchain) {
125-
args.push(`+${actionInput.toolchain}`);
126-
}
127-
128-
args.push("clippy");
129-
130-
// `--message-format=json` should just right after the `cargo clippy`
131-
// because usually people are adding the `-- -D warnings` at the end
132-
// of arguments and it will mess up the output.
133-
args.push("--message-format=json");
134-
135-
return args.concat(actionInput.args);
136-
}
137-
13822
void main();

src/schema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface MaybeCargoMessage {
2424
}
2525

2626
export interface CargoMessage {
27-
reason: string;
27+
reason: "compiler-message";
2828
message: {
2929
code: string;
3030
level: string;

src/tests/clippy.test.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as exec from "@actions/exec";
2+
3+
import { run } from "clippy";
4+
import { type ParsedInput } from "input";
5+
import { Reporter } from "reporter";
6+
import { type CargoMessage } from "schema";
7+
8+
jest.mock("@actions/core");
9+
jest.mock("@actions/exec");
10+
jest.mock("reporter");
11+
12+
describe("clippy", () => {
13+
it("runs with cargo", async () => {
14+
jest.spyOn(exec, "exec").mockResolvedValue(0);
15+
16+
const actionInput: ParsedInput = {
17+
toolchain: "stable",
18+
args: [],
19+
useCross: false,
20+
};
21+
22+
await expect(run(actionInput)).resolves.toBeUndefined();
23+
});
24+
25+
it("runs with cross", async () => {
26+
jest.spyOn(exec, "exec").mockResolvedValue(0);
27+
28+
const actionInput: ParsedInput = {
29+
toolchain: "stable",
30+
args: [],
31+
useCross: true,
32+
};
33+
34+
await expect(run(actionInput)).resolves.toBeUndefined();
35+
});
36+
37+
it("reports when clippy fails", async () => {
38+
jest.spyOn(exec, "exec").mockImplementation((_commandline: string, args?: string[] | undefined) => {
39+
const expected = ["clippy", "--message-format=json"];
40+
41+
if (
42+
(args ?? []).length > 0 &&
43+
expected.every((c) => {
44+
return args?.includes(c);
45+
})
46+
) {
47+
return Promise.resolve(101);
48+
} else {
49+
return Promise.resolve(0);
50+
}
51+
});
52+
53+
const actionInput: ParsedInput = {
54+
toolchain: "stable",
55+
args: [],
56+
useCross: false,
57+
};
58+
59+
await expect(run(actionInput)).rejects.toThrow(/Clippy had exited with the (\d)+ exit code/);
60+
});
61+
62+
it("records versions", async () => {
63+
const reportSpy = jest.spyOn(Reporter.prototype, "report");
64+
jest.spyOn(exec, "exec").mockImplementation((commandline: string, args?: string[], options?: exec.ExecOptions) => {
65+
if (commandline.endsWith("cargo")) {
66+
if (args?.[0] === "-V") {
67+
options?.listeners?.stdout?.(Buffer.from("cargo version"));
68+
} else if (args?.[0] === "clippy" && args?.[1] === "-V") {
69+
options?.listeners?.stdout?.(Buffer.from("clippy version"));
70+
}
71+
} else if (commandline === "rustc" && args?.[0] === "-V") {
72+
options?.listeners?.stdout?.(Buffer.from("rustc version"));
73+
}
74+
return Promise.resolve(0);
75+
});
76+
77+
const actionInput: ParsedInput = {
78+
toolchain: "stable",
79+
args: [],
80+
useCross: false,
81+
};
82+
83+
await expect(run(actionInput)).resolves.toBeUndefined();
84+
85+
expect(reportSpy).toBeCalledWith({ error: 0, help: 0, ice: 0, note: 0, warning: 0 }, [], { cargo: "cargo version", clippy: "clippy version", rustc: "rustc version" });
86+
});
87+
88+
it("clippy captures stdout", async () => {
89+
jest.spyOn(exec, "exec").mockImplementation((_commandline: string, args?: string[] | undefined, options?: exec.ExecOptions) => {
90+
const expected = ["clippy", "--message-format=json"];
91+
92+
if (
93+
(args ?? []).length > 0 &&
94+
expected.every((c) => {
95+
return args?.includes(c);
96+
})
97+
) {
98+
const data: CargoMessage = {
99+
reason: "compiler-message",
100+
message: {
101+
code: "500",
102+
level: "warning",
103+
message: "message",
104+
rendered: "rendered",
105+
spans: [{ is_primary: true, file_name: "main.rs", line_start: 12, line_end: 12, column_start: 30, column_end: 45 }],
106+
},
107+
};
108+
options?.listeners?.stdline?.(JSON.stringify(data));
109+
}
110+
111+
return Promise.resolve(0);
112+
});
113+
114+
const actionInput: ParsedInput = {
115+
toolchain: "stable",
116+
args: [],
117+
useCross: false,
118+
};
119+
120+
await expect(run(actionInput)).resolves.toBeUndefined();
121+
});
122+
});

0 commit comments

Comments
 (0)