Файли
openidea/src/features/run/targets.test.ts
T
Pedro Pessoa de4c325a28 feat(run): run and debug Pest tests from the gutter (#1925)
Pest test() and it() calls and describe() blocks get gutter Run and Debug,
nested by describe and filtered by the names Pest prints (`outer` → it does
x, plus a data set suffix). Projects with vendor/bin/pest get a Pest test
configuration of the PHPUnit kind, pest commands are recognised, Pest's
pest_qn:// hints are read and test tree reruns filter Pest tests by name.
2026-11-07 14:30:17 +08:00

404 рядки
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { detectRunTargets, jsNamePattern, targetConfig } from "./targets";
const cfg = (over: Partial<RunConfig> & { id: string; kind: string }): RunConfig => ({
name: over.id,
command: over.kind,
args: [],
cwd: null,
env: {},
detected: true,
isTest: true,
debugAdapter: null,
...over,
});
const pick = (ts: ReturnType<typeof detectRunTargets>) => ts.map((t) => [t.line, t.kind, t.path.join("/")]);
describe("detectRunTargets", () => {
it("rust: main, #[test] fns with module path, test modules", () => {
const src = [
"fn main() {}", // 0
"#[cfg(test)]",
"mod tests {", // 2
" use super::*;",
" #[test]",
' fn adds() { let s = "}"; }', // 5
" #[tokio::test]",
" #[ignore]",
" async fn async_one() {}", // 8
" fn helper() {}",
"}",
"fn after() {}",
].join("\n");
expect(pick(detectRunTargets(src, "src/main.rs"))).toEqual([
[0, "main", ""],
[2, "suite", "tests"],
[5, "test", "tests/adds"],
[8, "test", "tests/async_one"],
]);
expect(pick(detectRunTargets("#[test]\nfn x() {}", "crates/a/src/foo/bar.rs"))).toEqual([[1, "test", "foo/bar/x"]]);
expect(detectRunTargets("fn main() {}", "src/lib.rs")).toEqual([]);
});
it("js/ts: describe chains and test names", () => {
const src = [
"describe('math', () => {",
" it('adds', () => {});",
' describe("nested", () => {',
" test.each([[1]])('n=%i', () => {});",
" });",
" test.skip(`subs`, () => {});",
"});",
"const item = items.find((x) => x);",
"test('top', () => {});",
].join("\n");
const ts = detectRunTargets(src, "src/math.test.ts");
expect(pick(ts)).toEqual([
[0, "suite", "math"],
[1, "test", "math/adds"],
[2, "suite", "math/nested"],
[3, "test", "math/nested/n=%i"],
[5, "test", "math/subs"],
[8, "test", "top"],
]);
expect(ts[3]!.parameterized).toBe(true);
expect(jsNamePattern(ts[1]!)).toBe("^math adds$");
expect(jsNamePattern(ts[3]!)).toBe("^math nested n=.*$");
expect(jsNamePattern(ts[0]!)).toBe("^math");
expect(detectRunTargets("test('x', () => {})", "src/math.ts")).toEqual([]);
});
it("python: test functions, Test classes, __main__", () => {
const src = [
"class TestCalc:",
" def test_add(self):",
" pass",
"def test_top():",
" pass",
"class Helper:",
" def test_no(self): pass",
'if __name__ == "__main__":',
].join("\n");
expect(pick(detectRunTargets(src, "tests/test_calc.py"))).toEqual([
[0, "suite", "TestCalc"],
[1, "test", "TestCalc/test_add"],
[3, "test", "test_top"],
[7, "main", ""],
]);
});
it("python: unittest TestCase subclasses in tests.py (Django's default test module)", () => {
const src = [
"from django.test import TestCase",
"class QuestionTests(TestCase):",
" def test_recent(self):",
" pass",
"class Helper(Mixin):",
" def test_no(self): pass",
].join("\n");
expect(pick(detectRunTargets(src, "polls/tests.py", true))).toEqual([
[1, "suite", "QuestionTests"],
[2, "test", "QuestionTests/test_recent"],
]);
expect(pick(detectRunTargets(src, "polls/models.py", true))).toEqual([]);
// Outside Django projects `tests.py` is a script: no test targets.
expect(pick(detectRunTargets(src, "polls/tests.py"))).toEqual([]);
});
it("go and jvm", () => {
expect(
pick(detectRunTargets("package x\n\nfunc TestAdd(t *testing.T) {}\nfunc helper() {}", "pkg/add_test.go")),
).toEqual([[2, "test", "TestAdd"]]);
expect(pick(detectRunTargets("package main\nfunc main() {}", "cmd/app/main.go"))).toEqual([[1, "main", ""]]);
// Subtests: literal names, map-keyed tables; a one-line test has none.
const go = [
"package x",
"func TestOne(t *testing.T) {}",
"func TestMap(t *testing.T) {",
"\tcases := map[string]struct{ in int }{",
'\t\t"small n": {in: 1},',
"\t}",
"\tfor name, tc := range cases {",
"\t\tt.Run(name, func(t *testing.T) { _ = tc })",
"\t}",
'\tt.Run("a\\"b", nil)',
"}",
].join("\n");
expect(pick(detectRunTargets(go, "x_test.go"))).toEqual([
[1, "test", "TestOne"],
[2, "test", "TestMap"],
[4, "test", "TestMap/small_n"],
[9, "test", 'TestMap/a"b'],
]);
const kt = ["class CalcTest {", " @Test", " fun `adds numbers`() {}", " @Test fun subs() {}", "}"].join("\n");
expect(pick(detectRunTargets(kt, "src/test/kotlin/CalcTest.kt"))).toEqual([
[0, "suite", "CalcTest"],
[2, "test", "CalcTest/adds numbers"],
[3, "test", "CalcTest/subs"],
]);
});
});
describe("targetConfig", () => {
const configs = [
cfg({ id: "cargo:test", kind: "cargo", args: ["test"] }),
cfg({ id: "npm:test", kind: "npm", command: "pnpm", args: ["run", "test"] }),
cfg({ id: "pytest", kind: "pytest", command: "pytest", args: [] }),
cfg({ id: "go:test", kind: "go", command: "go", args: ["test", "./..."] }),
cfg({ id: "gradle:test", kind: "gradle", command: "./gradlew", args: ["test"] }),
];
const first = (src: string, rel: string, i = 0) =>
targetConfig(detectRunTargets(src, rel).filter((t) => t.kind !== "suite")[i]!, rel, configs)!;
it("derives per-framework filters", () => {
expect(first("mod tests {\n#[test]\nfn adds() {}\n}", "src/lib.rs").args).toEqual([
"test",
"--",
"tests::adds",
"--exact",
]);
expect(first("#[test]\nfn it_works() {}", "tests/api.rs").args).toEqual([
"test",
"--test",
"api",
"--",
"it_works",
"--exact",
]);
expect(first("describe('m', () => {\n it('adds', () => {});\n});", "src/a.test.ts").args).toEqual([
"run",
"test",
"--",
"src/a.test.ts",
"-t",
"^m adds$",
]);
expect(first("class TestC:\n def test_a(self): pass", "tests/test_c.py").args).toEqual([
"tests/test_c.py::TestC::test_a",
]);
// pytest also collects TestCase subclasses.
expect(first("class QTests(TestCase):\n def test_a(self): pass", "app/test_q.py").args).toEqual([
"app/test_q.py::QTests::test_a",
]);
const django = [cfg({ id: "django:test", kind: "django-test", command: "python3", args: ["manage.py", "test"] })];
const test = detectRunTargets("class QTests(TestCase):\n def test_a(self): pass", "polls/tests.py", true)[1]!;
expect(targetConfig(test, "polls/tests.py", django)!.args).toEqual([
"manage.py",
"test",
"polls.tests.QTests.test_a",
]);
// A module-level function or a plain class: unittest's loader would call it, so no Django run.
const fn = detectRunTargets("def test_top():\n pass", "polls/tests.py", true)[0]!;
expect(targetConfig(fn, "polls/tests.py", django)).toBeNull();
// Relative to the working directory, where manage.py is.
const nested = [{ ...django[0]!, cwd: "backend" }];
expect(targetConfig(test, "backend/polls/tests/test_q.py", nested)!.args).toEqual([
"manage.py",
"test",
"polls.tests.test_q.QTests.test_a",
]);
expect(first("func TestAdd(t *testing.T) {}", "pkg/add_test.go").args).toEqual([
"test",
"./pkg",
"-run",
"^TestAdd$",
]);
expect(first("class FooTest {\n @Test\n void bar() {}\n}", "src/test/java/FooTest.java").args).toEqual([
"test",
"--tests",
"FooTest.bar",
]);
});
it("names configs after the target and keeps ids stable", () => {
const c = first("mod tests {\n#[test]\nfn adds() {}\n}", "src/lib.rs");
expect(c.name).toBe("adds");
expect(c.id).toBe("context:target:cargo:test:src/lib.rs:tests/adds");
});
});
describe("Go test targets", () => {
it("detects benchmark, fuzz and example functions and runs a benchmark without tests", () => {
const text =
"package main\nfunc main() {}\nfunc BenchmarkWork(b *testing.B) {}\nfunc FuzzInput(f *testing.F) {}\nfunc Example() {}\n// func TestComment(t *testing.T) {}";
const targets = detectRunTargets(text, "work_test.go");
expect(targets.map((t) => t.name)).toEqual(["main", "BenchmarkWork", "FuzzInput", "Example"]);
const base = cfg({ id: "go:test", kind: "go", args: ["test", "-tags", "demo", "./...", "-args", "-test.short"] });
const config = targetConfig(targets[1]!, "work_test.go", [base], text)!;
expect(config.args).toEqual([
"test",
"-tags",
"demo",
".",
"-run",
"^$",
"-bench",
"^BenchmarkWork$",
"-args",
"-test.short",
]);
});
it("uses the module and overrides a saved filter before binary arguments", () => {
const base = cfg({
id: "go:typed",
kind: "go",
cwd: "runtime",
args: ["test", "-tags", "demo", "-run", "Old", ".", "-args", "-test.short"],
params: {
goKind: "test",
module: "app",
scope: "package",
target: ".",
pattern: "Old",
programArgs: "-test.short",
},
});
const text = 'package calc\nfunc TestNew(t *testing.T) {\n t.Run("child case", func(t *testing.T) {})\n}';
const targets = detectRunTargets(text, "app/calc_test.go");
const config = targetConfig(targets[1]!, "app/calc_test.go", [base], text, base)!;
expect(config.args).toEqual([
"test",
"-tags",
"demo",
".",
"-run",
"^TestNew$/^child_case$",
"-args",
"-test.short",
]);
expect(config.params).toMatchObject({
module: "app",
scope: "directory",
target: ".",
pattern: "^TestNew$/^child_case$",
});
});
});
describe("php (PHPUnit)", () => {
const src = [
"<?php", // 0
"namespace App\\Tests;",
"use PHPUnit\\Framework\\TestCase;",
"final class CalcTest extends TestCase", // 3
"{",
" public function testAdd(): void {}", // 5
" #[Test]",
" public function subtracts(): void {}", // 7
" /** @test */",
" public function multiplies(): void {}", // 9
" public function helper(): void {}",
" private function testHidden(): void {}",
"}",
"class Helper {}",
" public function testNotInTestClass() {}",
].join("\n");
it("finds test classes and test methods", () => {
expect(pick(detectRunTargets(src, "tests/CalcTest.php"))).toEqual([
[3, "suite", "CalcTest"],
[5, "test", "CalcTest/testAdd"],
[7, "test", "CalcTest/subtracts"],
[9, "test", "CalcTest/multiplies"],
]);
});
it("ignores files that are not tests", () => {
expect(detectRunTargets(src, "src/Calc.php")).toEqual([]);
});
it("runs a method with --filter ahead of the file", () => {
const phpunit = cfg({ id: "phpunit", kind: "phpunit", command: "vendor/bin/phpunit" });
const target = detectRunTargets(src, "tests/CalcTest.php")[1]!;
const run = targetConfig(target, "tests/CalcTest.php", [phpunit], src);
expect(run?.args).toEqual(["--filter", "/(CalcTest::testAdd( with data set .*)?)$/", "tests/CalcTest.php"]);
expect(run?.command).toBe("vendor/bin/phpunit");
});
});
describe("php (Pest)", () => {
const src = [
"<?php", // 0
"",
"test('adds numbers', function () {", // 2
" expect(1 + 1)->toBe(2);",
"});",
"",
'it("handles \\"quoted\\" names", fn () => true);', // 6
"",
"describe('division', function () {", // 8
" it('divides', function () {", // 9
" expect(4 / 2)->toBe(2);",
" });",
"",
" describe('by zero', function () {", // 13
" test('throws', fn () => true);", // 14
" });",
"});",
"",
"it('doubles', function (int $n) {", // 18
" expect($n * 2)->toBe($n + $n);",
"})->with([1, 2, 3]);",
"// it('is commented out')",
].join("\n");
const pest = cfg({ id: "phpunit", kind: "phpunit", command: "vendor/bin/pest" });
const run = (line: number) => {
const target = detectRunTargets(src, "tests/Unit/MathTest.php").find((t) => t.line === line)!;
return targetConfig(target, "tests/Unit/MathTest.php", [pest], src);
};
it("finds tests and describe blocks with the names Pest gives them", () => {
expect(pick(detectRunTargets(src, "tests/Unit/MathTest.php"))).toEqual([
[2, "test", "adds numbers"],
[6, "test", 'it handles "quoted" names'],
[8, "suite", "division"],
[9, "test", "division/it divides"],
[13, "suite", "division/by zero"],
[14, "test", "division/by zero/throws"],
[18, "test", "it doubles"],
]);
expect(detectRunTargets(src, "tests/Unit/MathTest.php")[3]).toMatchObject({
name: "division → it divides",
pest: true,
});
});
it("ignores files that are not tests", () => {
expect(detectRunTargets(src, "src/Math.php")).toEqual([]);
});
it("runs one test by the name Pest matches, data sets included", () => {
expect(run(2)?.args).toEqual(["--filter", "/::adds numbers( with data set .*)?$/", "tests/Unit/MathTest.php"]);
expect(run(14)?.args).toEqual([
"--filter",
"/::`division` → `by zero` → throws( with data set .*)?$/",
"tests/Unit/MathTest.php",
]);
expect(run(18)?.args[1]).toBe("/::it doubles( with data set .*)?$/");
expect(run(18)?.command).toBe("vendor/bin/pest");
});
it("runs every test of a describe block", () => {
expect(run(8)?.args[1]).toBe("/::`division` → /");
expect(run(13)?.args[1]).toBe("/::`division` → `by zero` → /");
});
it("keeps PHPUnit classes in the same file", () => {
const mixed = [
"<?php",
"final class CalcTest extends TestCase",
"{",
" public function testAdd() {}",
"}",
"test('x', fn () => 1);",
];
expect(pick(detectRunTargets(mixed.join("\n"), "tests/CalcTest.php"))).toEqual([
[1, "suite", "CalcTest"],
[3, "test", "CalcTest/testAdd"],
[5, "test", "x"],
]);
});
});