дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat: Second pass aligning interactions with IntelliJ IDEA (#23)
* feat: Second pass aligning interactions with IntelliJ IDEA Result tool windows - Next/Previous Occurrence (⌥⌘↓/↑) across Find, Usages, Problems, TODO, Inspection and Hierarchy, with an "N of M" status hint - Find: reuse the unpinned tab, Pin Tab, source preview, group by file or directory, exclude moves to the next result, Rerun - Usages: tabs with pinning, exclude and preview - Problems, TODO, Inspection: no autoscroll by default, severity-first sorting, context menu with quick fixes and copy description - Trees: F4/⌘↓ jump to source; speed search Home/End and arrow handling Find in Files, bookmarks, navigation - Replace mode: Enter replaces the selection, ⇧⌥Enter replaces all, multi-select - Pin Window toggle, highlighted read-only preview, search/replace history (⌥↓), Alt mnemonics, search context filter (comments / string literals) - Mnemonic bookmarks (⌥F3, ⌃⇧digit, ⌃digit), ⌘F3 bookmarks popup, descriptions, ⌘-click gutter toggles a bookmark - Select In popup (⌥F1); Recent Locations double-click and Delete; Search Everywhere remembers the query per tab Code insight and refactoring - Extract/Introduce continue with in-place rename and name suggestions; expression and occurrence choosers with live highlighting - Safe Delete confirms first; numbered Refactor This - Quick Documentation history, in-popup links and a Documentation tool window; Quick Definition switches between targets and shows the whole declaration - Goto chooser lists symbol names and can pin to the Find window - Parameter Info Tab/⇧Tab, intention preview, Reformat dialog and summary, inlay hint ⌘-click and context menu - Live templates: Enter moves to the next field, two-step Esc, ⌘J as a lookup, language surrounders with mnemonics - File Structure popup as a filterable tree; Show Usages filter and preview; call hierarchy jumps to call sites Editor - Paste re-indents each line by default; Move Statement moves whole statements in and out of blocks and re-indents - Type over closing quotes of existing strings; ⌘D/⌘⌫ cover folded regions; ⌥⌫ deletes the preceding word - ⌃G / ⌃⌘G whole-word rules and wrap hint; double-modifier caret cloning - Visual-column block selection, nested Collapse All, recursive folding, fold selection toggle, Go to Matching Brace, gutter drag selects lines, sticky line and error stripe clicks, Insert toggles overwrite mode Run, VCS, workbench - Gutter icons show the last test result; Modify Run Configuration; test tree hides passed tests, context menu and assertion diff - Stack frames resolve by fully qualified class name; context run chooser - Inline Smart Step Into - Git Log multi-select filters, "me" and date range; favourite branches, Fetch and tags in the branch popup; stash Keep index and Unstash options - Terminal tab titles follow the shell; Distraction Free persists; gear menu; sticky balloons move to the notifications tool window after a while * fix(vcs): Label restored local changes as yours after Smart Checkout; name the branch checked out Conflicts from restoring stashed changes show the user's uncommitted changes on the left like other operations, and the checkout notification names the new branch instead of the previous one. * fix: Close the merge editor on Apply; confirm before terminating a running Run tab Like IDEA, applying a merge result closes its tab, and closing a Run tab whose process still runs asks before terminating it (with a Settings toggle and "Do not ask again"). * fix(terminal): Don't start a new session in the background after closing the last tab The hidden Terminal window stays mounted, so it reopened a session as soon as the last tab closed. It now opens the Local tab only once the window is shown again, as IDEA does. * fix(vcs): Start the merge result from a three-way merge; close the editor on Apply The result pane now begins with non-conflicting changes merged and each conflict showing the base text, resolved from the side panes, instead of raw conflict markers. Apply now reports success, so the tab closes. fix(search): Match File Structure filter on word starts * fix: Limit Smart Step Into to the paused line; drop usages hints on callbacks; show full intention titles Adapters that list every remaining call in the function (debugpy) made Smart Step Into fall back to a chooser that offered calls from later lines. Targets are now narrowed to the calls on the paused line, so they are picked in place. Anonymous functions and callbacks no longer get a 'no usages' hint, and intention titles are no longer cut short when there is room for them. * fix: Undo / Redo from the Edit menu and Find Action use the editor's history They ran the webview's native undo, which misses programmatic edits such as templates, completion and refactorings, so nothing happened in the editor.
Цей коміт міститься в:
1 джерело
81e0e8e2d0
коміт
82b6980a59
262 files changed
+13085
-1596
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseComparison } from "./comparison";
|
||||
|
||||
describe("parseComparison", () => {
|
||||
it("JUnit / opentest4j", () => {
|
||||
expect(parseComparison(["org.opentest4j.AssertionFailedError: expected: <3> but was: <4>"])).toEqual({
|
||||
expected: "3",
|
||||
actual: "4",
|
||||
});
|
||||
expect(parseComparison(['expected: "a b"', ' but was: "a c"'])).toEqual({ expected: '"a b"', actual: '"a c"' });
|
||||
});
|
||||
|
||||
it("kotlin.test", () => {
|
||||
expect(parseComparison(["java.lang.AssertionError: Expected <1>, actual <2>."])).toEqual({
|
||||
expected: "1",
|
||||
actual: "2",
|
||||
});
|
||||
});
|
||||
|
||||
it("jest / vitest", () => {
|
||||
expect(
|
||||
parseComparison([
|
||||
" expect(received).toBe(expected) // Object.is equality",
|
||||
"",
|
||||
" Expected: 3",
|
||||
" Received: 4",
|
||||
]),
|
||||
).toEqual({ expected: "3", actual: "4" });
|
||||
});
|
||||
|
||||
it("rust assert_eq! keeps left / right", () => {
|
||||
expect(parseComparison(["assertion `left == right` failed", " left: 3", " right: 4"])).toEqual({
|
||||
expected: "3",
|
||||
actual: "4",
|
||||
labels: { expected: "Left", actual: "Right" },
|
||||
});
|
||||
});
|
||||
|
||||
it("go testify and pytest", () => {
|
||||
expect(
|
||||
parseComparison(["\tError: \tNot equal: ", "\t \texpected: 3", "\t \tactual : 4"]),
|
||||
).toEqual({
|
||||
expected: "3",
|
||||
actual: "4",
|
||||
});
|
||||
expect(parseComparison(["> assert add(1, 2) == 4", "E assert 3 == 4"])).toEqual({
|
||||
expected: "4",
|
||||
actual: "3",
|
||||
});
|
||||
});
|
||||
|
||||
it("no comparison in other failures", () => {
|
||||
expect(parseComparison(["java.lang.IllegalStateException: boom", "\tat com.acme.A.b(A.java:3)"])).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Expected / actual values of an assertion failure, read from a test's captured
|
||||
* output, for the test tree's "Show Diff" (as in IDEA's comparison failure diff).
|
||||
*/
|
||||
|
||||
export interface Comparison {
|
||||
expected: string;
|
||||
actual: string;
|
||||
/** Side labels when the framework doesn't say which side is expected (Rust `left` / `right`). */
|
||||
labels?: { expected: string; actual: string };
|
||||
}
|
||||
|
||||
/** Strips the quotes / angle brackets frameworks wrap values in (`<1>`, `"a"` stays as written). */
|
||||
const unwrap = (s: string) => s.trim().replace(/^<([\s\S]*)>$/, "$1");
|
||||
|
||||
export function parseComparison(output: readonly string[]): Comparison | null {
|
||||
// Output lines reach the parsers with ANSI escapes already stripped.
|
||||
const lines = output;
|
||||
const text = lines.join("\n");
|
||||
let m: RegExpExecArray | 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]!) };
|
||||
// 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).
|
||||
const left = lines.findIndex((l) => /^\s*left:\s/.test(l));
|
||||
if (left >= 0 && /^\s*right:\s/.test(lines[left + 1] ?? ""))
|
||||
return {
|
||||
expected: lines[left]!.replace(/^\s*left:\s*/, ""),
|
||||
actual: lines[left + 1]!.replace(/^\s*right:\s*/, ""),
|
||||
labels: { expected: "Left", actual: "Right" },
|
||||
};
|
||||
// Jest / Vitest: `Expected: x` then `Received: y`.
|
||||
const exp = lines.findIndex((l) => /^\s*Expected:\s/.test(l));
|
||||
const rec = exp >= 0 ? lines.findIndex((l, i) => i > exp && /^\s*Received:\s/.test(l)) : -1;
|
||||
if (exp >= 0 && rec >= 0)
|
||||
return {
|
||||
expected: lines[exp]!.replace(/^\s*Expected:\s*/, ""),
|
||||
actual: lines[rec]!.replace(/^\s*Received:\s*/, ""),
|
||||
};
|
||||
// Go testify: `expected: x` then `actual : y`.
|
||||
const gexp = lines.findIndex((l) => /^\s*(?:Error:\s+Not equal:\s*)?expected:\s/.test(l));
|
||||
const gact = gexp >= 0 ? lines.findIndex((l, i) => i > gexp && /^\s*actual\s*:\s/.test(l)) : -1;
|
||||
if (gexp >= 0 && gact >= 0)
|
||||
return {
|
||||
expected: lines[gexp]!.replace(/^.*?expected:\s*/, ""),
|
||||
actual: lines[gact]!.replace(/^\s*actual\s*:\s*/, ""),
|
||||
};
|
||||
// pytest: `assert actual == expected`.
|
||||
if ((m = /^(?:E\s+)?(?:AssertionError:\s*)?assert\s+(.+?)\s+==\s+(.+)$/m.exec(text)))
|
||||
return { expected: m[2]!.trim(), actual: m[1]!.trim() };
|
||||
return null;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
/** Gradle test logging (`Class > method() PASSED`) and Maven Surefire reports. */
|
||||
import { jvmFrameTarget } from "../links";
|
||||
import { parseDuration, type TestEvent, type TestOutputParser } from "./types";
|
||||
|
||||
const GRADLE_RE = /^(\S[^>]*?(?:\s>\s[^>]+?)+)\s+(PASSED|FAILED|SKIPPED)(?:\s+\(([\d.]+\s*m?s)\))?$/;
|
||||
const GRADLE_SUMMARY_RE = /^(\d+) tests? completed(?:, (\d+) failed)?(?:, (\d+) skipped)?/;
|
||||
const FRAME_RE = /\(([\w$.-]+\.(?:java|kt|kts|scala|groovy)):(\d+)\)|\bat ([\w$.-]+\.(?:java|kt|kts|scala|groovy)):(\d+)/;
|
||||
const FRAME_RE =
|
||||
/(?:([\w$]+(?:\.[\w$]+)*)\.[\w$<>-]+)?\(([\w$.-]+\.(?:java|kt|kts|scala|groovy)):(\d+)\)|\bat ([\w$.-]+\.(?:java|kt|kts|scala|groovy)):(\d+)/;
|
||||
// [ERROR] com.acme.OrderTest.shouldX -- Time elapsed: 0.01 s <<< FAILURE! (Surefire 3)
|
||||
// shouldX(com.acme.OrderTest) Time elapsed: 0.01 sec <<< FAILURE! (Surefire 2)
|
||||
const SUREFIRE3_RE = /^(?:\[\w+\]\s+)?([\w$.]+)\.([\w$]+)(?:\(\))?\s+--\s+Time elapsed: ([\d.]+ s(?:ec)?)\s+<<<\s+(FAILURE|ERROR)!/;
|
||||
@@ -83,7 +85,9 @@ export function jvmParser(): TestOutputParser {
|
||||
events.push({
|
||||
type: "location",
|
||||
path: failure,
|
||||
location: { path: (f[1] ?? f[3])!, line: Number(f[2] ?? f[4]), resolve: "name" },
|
||||
location: f[2]
|
||||
? jvmFrameTarget(f[1], f[2], Number(f[3]))
|
||||
: { path: f[4]!, line: Number(f[5]), resolve: "name" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,10 +390,12 @@ org.opentest4j.AssertionFailedError: expected: <3> but was: <2>
|
||||
`,
|
||||
);
|
||||
expect(nodeStatus(r.get("com.acme.OrderTest")!)).toBe("failed");
|
||||
// Resolved by the frame's class (package directory + file), not by a bare file name.
|
||||
expect(leaf(r, "com.acme.OrderTest", "shouldTotal")?.location).toEqual({
|
||||
path: "OrderTest.java",
|
||||
path: "com/acme/OrderTest.java",
|
||||
line: 31,
|
||||
resolve: "name",
|
||||
className: "com.acme.OrderTest",
|
||||
});
|
||||
expect(r.get("com.acme.UserTest")?.own).toBe("passed");
|
||||
expect(r.summary).toMatchObject({ passed: 2, failed: 1 });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { TestResults } from "./results";
|
||||
import { failedTestPaths, rerunFailedConfig } from "./rerun";
|
||||
import { failedTestPaths, leafTestPaths, rerunFailedConfig, testsConfig } from "./rerun";
|
||||
|
||||
const cfg = (kind: string, command: string, args: string[]): RunConfig => ({
|
||||
id: `${kind}:test`,
|
||||
@@ -29,6 +29,38 @@ describe("failedTestPaths", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("tests selected in the test tree", () => {
|
||||
it("expands suites to their tests; a suite without reported tests stands for itself", () => {
|
||||
const r = new TestResults();
|
||||
r.apply([
|
||||
{ type: "result", path: ["com.acme.ATest", "one()"], status: "passed" },
|
||||
{ type: "result", path: ["com.acme.ATest", "two()"], status: "failed" },
|
||||
{ type: "result", path: ["com.acme.BTest"], status: "passed" },
|
||||
]);
|
||||
const nodes = [
|
||||
r.get("com.acme.ATest")!,
|
||||
r.get(["com.acme.ATest", "one()"].join("\u0000"))!,
|
||||
r.get("com.acme.BTest")!,
|
||||
];
|
||||
expect(leafTestPaths(nodes)).toEqual([
|
||||
["com.acme.ATest", "one()"],
|
||||
["com.acme.ATest", "two()"],
|
||||
["com.acme.BTest"],
|
||||
]);
|
||||
const c = testsConfig(cfg("gradle", "./gradlew", ["test"]), leafTestPaths(nodes), "3 tests")!;
|
||||
expect(c.name).toBe("3 tests");
|
||||
expect(c.args).toEqual([
|
||||
"test",
|
||||
"--tests",
|
||||
"com.acme.ATest.one",
|
||||
"--tests",
|
||||
"com.acme.ATest.two",
|
||||
"--tests",
|
||||
"com.acme.BTest",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rerunFailedConfig", () => {
|
||||
it("cargo: exact names after --", () => {
|
||||
const c = rerunFailedConfig(cfg("cargo", "cargo", ["test", "--workspace"]), [
|
||||
|
||||
@@ -113,8 +113,29 @@ function jvmArgs(config: RunConfig, paths: string[][]): string[] | null {
|
||||
return [...dropFlag(config.args, ["--tests"]), ...filters.flatMap((f) => ["--tests", f])];
|
||||
}
|
||||
|
||||
/** Leaf test paths under the given nodes (a node without reported children stands for itself). */
|
||||
export function leafTestPaths(nodes: readonly TestNode[]): string[][] {
|
||||
const out: string[][] = [];
|
||||
const seen = new Set<string>();
|
||||
const visit = (n: TestNode) => {
|
||||
if (n.children.length) return n.children.forEach(visit);
|
||||
const key = n.path.join("\u0000");
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
out.push(n.path);
|
||||
};
|
||||
nodes.forEach(visit);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Config running only `paths` (failed test paths from the results tree), or null when unsupported. */
|
||||
export function rerunFailedConfig(config: RunConfig, paths: string[][]): RunConfig | null {
|
||||
const baseName = config.name.replace(/^Failed: /, "");
|
||||
return testsConfig(config, paths, `Failed: ${baseName}`);
|
||||
}
|
||||
|
||||
/** Config running only the tests at `paths` (test tree paths), named `name`; null when unsupported. */
|
||||
export function testsConfig(config: RunConfig, paths: string[][], name: string): RunConfig | null {
|
||||
if (!paths.length) return null;
|
||||
let args: string[] | null;
|
||||
switch (frameworkFor(config)) {
|
||||
@@ -137,6 +158,5 @@ export function rerunFailedConfig(config: RunConfig, paths: string[][]): RunConf
|
||||
args = null;
|
||||
}
|
||||
if (!args) return null;
|
||||
const baseName = config.name.replace(/^Failed: /, "");
|
||||
return { ...config, args, name: `Failed: ${baseName}`, detected: true, env: { ...config.env } };
|
||||
return { ...config, args, name, detected: true, env: { ...config.env } };
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { TestResults } from "./results";
|
||||
import { clearTestStates, onTestStatesChanged, recordTestStates, testState } from "./states";
|
||||
|
||||
function run(events: Parameters<TestResults["apply"]>[0]) {
|
||||
const r = new TestResults();
|
||||
r.apply(events);
|
||||
r.finish(0);
|
||||
recordTestStates(r);
|
||||
}
|
||||
|
||||
describe("test states for the run gutter", () => {
|
||||
afterEach(() => clearTestStates());
|
||||
|
||||
it("matches Rust tests by module path under any crate suite", () => {
|
||||
run([
|
||||
{ type: "result", path: ["src/lib.rs", "tests", "adds"], status: "passed" },
|
||||
{ type: "result", path: ["src/lib.rs", "tests", "subs"], status: "failed" },
|
||||
]);
|
||||
expect(testState({ path: ["tests", "adds"] })).toBe("passed");
|
||||
expect(testState({ path: ["tests", "subs"] })).toBe("failed");
|
||||
// The suite (`mod tests`) failed because one of its tests did.
|
||||
expect(testState({ path: ["tests"] })).toBe("failed");
|
||||
expect(testState({ path: ["tests", "other"] })).toBeNull();
|
||||
});
|
||||
|
||||
it("matches JVM results named by qualified class and `method()`", () => {
|
||||
run([
|
||||
{ type: "result", path: ["com.acme.OrderTest", "lists()"], status: "passed" },
|
||||
{ type: "result", path: ["com.acme.OrderTest", "creates()"], status: "failed" },
|
||||
]);
|
||||
expect(testState({ path: ["OrderTest", "lists"] })).toBe("passed");
|
||||
expect(testState({ path: ["OrderTest", "creates"] })).toBe("failed");
|
||||
expect(testState({ path: ["OrderTest"] })).toBe("failed");
|
||||
});
|
||||
|
||||
it("checks the file for results rooted at it, and any parameter failing fails the test", () => {
|
||||
run([
|
||||
{ type: "result", path: ["tests/test_math.py", "test_add[1]"], status: "passed" },
|
||||
{ type: "result", path: ["tests/test_math.py", "test_add[2]"], status: "failed" },
|
||||
{ type: "result", path: ["src/math.test.ts", "math", "adds"], status: "passed" },
|
||||
]);
|
||||
expect(testState({ path: ["test_add"], file: "/work/app/tests/test_math.py" })).toBe("failed");
|
||||
expect(testState({ path: ["test_add"], file: "/work/app/other/test_math2.py" })).toBeNull();
|
||||
expect(testState({ path: ["math", "adds"], file: "/work/app/src/math.test.ts" })).toBe("passed");
|
||||
});
|
||||
|
||||
it("newer runs win and listeners hear about changes", () => {
|
||||
const listener = vi.fn();
|
||||
const off = onTestStatesChanged(listener);
|
||||
run([{ type: "result", path: ["pkg", "TestA"], status: "failed" }]);
|
||||
run([{ type: "result", path: ["pkg", "TestA"], status: "passed" }]);
|
||||
expect(testState({ path: ["TestA"] })).toBe("passed");
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
// Unchanged results don't redraw the gutters.
|
||||
run([{ type: "result", path: ["pkg", "TestA"], status: "passed" }]);
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
off();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Last known result per test, across runs, for the gutter ▶ icons: like IDEA, a
|
||||
* test (or suite) that failed last time shows a red run icon, a passed one green.
|
||||
* Results are matched to gutter targets by their test path (the trailing name
|
||||
* parts), plus the file for frameworks whose results are rooted at the file.
|
||||
*/
|
||||
import { nodeStatus, type TestNode, type TestResults } from "./results";
|
||||
import type { TestStatus } from "./types";
|
||||
|
||||
export type TestState = "passed" | "failed";
|
||||
|
||||
interface Entry {
|
||||
path: string[];
|
||||
state: TestState;
|
||||
}
|
||||
|
||||
const SEP = "\u0000";
|
||||
const entries = new Map<string, Entry>();
|
||||
const listeners = new Set<() => void>();
|
||||
let version = 0;
|
||||
|
||||
export function testStatesVersion(): number {
|
||||
return version;
|
||||
}
|
||||
|
||||
export function onTestStatesChanged(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
function toState(s: TestStatus | null): TestState | null {
|
||||
return s === "passed" || s === "failed" ? s : null;
|
||||
}
|
||||
|
||||
/** Records the outcome of every finished test and suite of a run (newer runs win). */
|
||||
export function recordTestStates(results: Pick<TestResults, "roots">) {
|
||||
let changed = false;
|
||||
const visit = (n: TestNode) => {
|
||||
const state = toState(nodeStatus(n));
|
||||
if (state) {
|
||||
const key = n.path.join(SEP);
|
||||
if (entries.get(key)?.state !== state) changed = true;
|
||||
entries.set(key, { path: n.path, state });
|
||||
}
|
||||
n.children.forEach(visit);
|
||||
};
|
||||
results.roots.forEach(visit);
|
||||
if (!changed) return;
|
||||
version++;
|
||||
for (const l of listeners) l();
|
||||
}
|
||||
|
||||
export function clearTestStates() {
|
||||
if (!entries.size) return;
|
||||
entries.clear();
|
||||
version++;
|
||||
for (const l of listeners) l();
|
||||
}
|
||||
|
||||
/** `name()`, `name(int)`, `test_x[param]` → `name` / `test_x`. */
|
||||
const norm = (s: string) => s.replace(/\s*(\(.*\)|\[.*\])$/, "").trim();
|
||||
|
||||
function segmentMatches(result: string, target: string): boolean {
|
||||
const a = norm(result);
|
||||
const b = norm(target);
|
||||
// JVM results name the class by its fully qualified name, gutter targets by the simple one.
|
||||
return a === b || a.endsWith(`.${b}`) || b.endsWith(`.${a}`);
|
||||
}
|
||||
|
||||
const cleanPath = (p: string) => p.replace(/\\/g, "/").replace(/^\.\//, "");
|
||||
|
||||
function sameFile(resultRoot: string, relPath: string): boolean {
|
||||
const a = cleanPath(resultRoot);
|
||||
const b = cleanPath(relPath);
|
||||
return a === b || b.endsWith(`/${a}`) || a.endsWith(`/${b}`);
|
||||
}
|
||||
|
||||
export interface StateQuery {
|
||||
/** Test path of the gutter target (describe chain, module path, `[Class, method]`…). */
|
||||
path: string[];
|
||||
/** Project-relative path of the file, checked when results are rooted at their file. */
|
||||
file?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last state of a gutter target: failed when any matching result failed (a
|
||||
* parameterised test failed for one of its parameters), else passed.
|
||||
*/
|
||||
export function testState({ path, file }: StateQuery): TestState | null {
|
||||
if (!path.length) return null;
|
||||
let passed = false;
|
||||
for (const e of entries.values()) {
|
||||
if (e.path.length < path.length) continue;
|
||||
const offset = e.path.length - path.length;
|
||||
if (!path.every((p, i) => segmentMatches(e.path[offset + i]!, p))) continue;
|
||||
if (file !== undefined && (offset === 0 || !sameFile(e.path[0]!, file))) continue;
|
||||
if (e.state === "failed") return "failed";
|
||||
passed = true;
|
||||
}
|
||||
return passed ? "passed" : null;
|
||||
}
|
||||
@@ -10,8 +10,10 @@ export interface TestLocation {
|
||||
/** 1-based, as printed. */
|
||||
line?: number;
|
||||
col?: number;
|
||||
/** `name`: a bare file name to look up in the project (JVM / Go frames). */
|
||||
/** `name`: a file name to look up in the project (JVM / Go frames). */
|
||||
resolve: "path" | "name";
|
||||
/** JVM frames: the class's binary name; `path` is then the file under its package directory. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type TestEvent =
|
||||
|
||||
Reference in new issue
Заблокувати користувача