feat(run): recognize TestNG classes annotated with @Test (#1403)

TestNG's class-level @Test makes the class's public void methods tests; the scanner now recognizes them (excluding @Before*/@After*/@DataProvider/@Factory). Test results locate TestNG failures past org.testng frames and parse TestNG's comparison message. Refs #1368
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-05 08:59:49 +08:00
1 джерело 1f70b134de
коміт 01e09565b7
6 files changed
+65 -2

No files matched your search

+7
Переглянути файл
@@ -10,6 +10,13 @@ describe("parseComparison", () => {
expect(parseComparison(['expected: "a b"', ' but was: "a c"'])).toEqual({ expected: '"a b"', actual: '"a c"' });
});
it("TestNG", () => {
expect(parseComparison(["java.lang.AssertionError: total expected [3] but found [2]"])).toEqual({
expected: "3",
actual: "2",
});
});
it("kotlin.test", () => {
expect(parseComparison(["java.lang.AssertionError: Expected <1>, actual <2>."])).toEqual({
expected: "1",
+2
Переглянути файл
@@ -21,6 +21,8 @@ export function parseComparison(output: readonly string[]): Comparison | null {
// JUnit / opentest4j / AssertJ / Hamcrest-style: `expected: <x> but was: <y>`.
if ((m = /expected:\s*(<[\s\S]*?>|\S[^\n]*?)\s*but was:\s*(<[\s\S]*?>|[^\n]*)/.exec(text)))
return { expected: unwrap(m[1]!), actual: unwrap(m[2]!) };
// TestNG `Assert.assertEquals`: `expected [x] but found [y]`.
if ((m = /expected \[([\s\S]*?)\] but found \[([\s\S]*?)\]/.exec(text))) return { expected: m[1]!, actual: m[2]! };
// kotlin.test: `Expected <x>, actual <y>.`
if ((m = /Expected <([\s\S]*?)>, actual <([\s\S]*?)>/.exec(text))) return { expected: m[1]!, actual: m[2]! };
// Rust `assert_eq!`: ` left: x` / ` right: y` (the macro doesn't say which one is expected).
+1 -1
Переглянути файл
@@ -21,7 +21,7 @@ const SUREFIRE_CLASS_RE =
const SUREFIRE_TOTAL_RE = /^(?:\[\w+\]\s+)?Tests run: (\d+), Failures: (\d+), Errors: (\d+), Skipped: (\d+)\s*$/;
// `at org.junit…`, also behind a class loader or module (`at app//org.junit…`, `at java.base@21/java.lang…`).
const LIBRARY_FRAME_RE =
/^\s*at\s+(?:\S*\/)?(?:org\.junit|junit\.|org\.opentest4j|kotlin\.|jdk\.internal|java\.|javax\.|sun\.|org\.gradle)/;
/^\s*at\s+(?:\S*\/)?(?:org\.junit|junit\.|org\.opentest4j|org\.testng|kotlin\.|jdk\.internal|java\.|javax\.|sun\.|org\.gradle)/;
/** The source position of a stack frame in the project's code, null for library frames. */
function frameLocation(line: string): TestLocation | null {
+18
Переглянути файл
@@ -471,6 +471,24 @@ org.opentest4j.AssertionFailedError: expected: <3> but was: <2>
expect(r.get("com.acme.UserTest")?.own).toBe("passed");
expect(r.summary).toMatchObject({ passed: 2, failed: 1 });
});
it("locates a TestNG failure in the test, past TestNG's own frames", () => {
const r = results(
jvmParser(),
`[INFO] Running com.acme.PriceTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.2 s <<< FAILURE! -- in com.acme.PriceTest
[ERROR] com.acme.PriceTest.rounds -- Time elapsed: 0.01 s <<< FAILURE!
java.lang.AssertionError: expected [3] but found [2]
at org.testng.Assert.fail(Assert.java:111)
at org.testng.Assert.assertEquals(Assert.java:1080)
at com.acme.PriceTest.rounds(PriceTest.java:12)
`,
);
expect(leaf(r, "com.acme.PriceTest", "rounds")?.location).toMatchObject({
path: "com/acme/PriceTest.java",
line: 12,
});
});
});
describe("parser selection", () => {