mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(run): rerun with the configuration as edited, and drop deleted ones from Services (#1579)
Rerunning a run tab launched the configuration snapshot taken when the tab first ran, so edits from Edit Configurations were ignored until the tab was closed. Every rerun path (Run tool window, Services, the exit notification, Debug from a run tab, Rerun Failed Tests / selected tests, Update Application's restart) now uses the saved configuration with the same id, falling back to the snapshot when it was deleted or never saved. Services also drops a configuration deleted after it ran once its run has ended; ad-hoc runs (build tasks, external tools) still show. Fixes #1497 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d1acf9f9ac
commit
f665beb17e
@@ -50,6 +50,7 @@ vi.mock("@/platform/javaLaunch", async (original) => ({
|
||||
vi.mock("./buildUpdate", () => ({ runUpdateBuild: mocks.runBuild }));
|
||||
vi.mock("./tomcat", () => ({ buildTomcatArtifact: mocks.build, syncTomcatOutputs: mocks.sync }));
|
||||
vi.mock("./store", () => ({
|
||||
rerunConfig: (s: Pick<RunSession, "config">) => mocks.state.configs.find((c) => c.id === s.config.id) ?? s.config,
|
||||
startRun: mocks.start,
|
||||
stopSession: mocks.stop,
|
||||
waitForExit: mocks.wait,
|
||||
@@ -105,6 +106,16 @@ describe("Tomcat application updates", () => {
|
||||
expect(mocks.wait.mock.invocationCallOrder[0]).toBeLessThan(mocks.action.mock.invocationCallOrder[0]!);
|
||||
expect(mocks.action).toHaveBeenCalledWith("run.debugConfig", session.config);
|
||||
});
|
||||
it("restarts with the configuration as edited since the server started", async () => {
|
||||
const edited = { ...session.config, name: "Tomcat (edited)" };
|
||||
mocks.state.configs = [edited];
|
||||
try {
|
||||
await updateTomcat(session, "restart");
|
||||
expect(mocks.action).toHaveBeenCalledWith("run.debugConfig", edited);
|
||||
} finally {
|
||||
mocks.state.configs = [];
|
||||
}
|
||||
});
|
||||
it("redeploys when the debugger cannot perform HotSwap", async () => {
|
||||
mocks.action.mockResolvedValue(false);
|
||||
await updateTomcat(session, "classes");
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { showContextMenu } from "@/ui";
|
||||
import { buildTomcatArtifact, syncTomcatOutputs } from "./tomcat";
|
||||
import { runUpdateBuild } from "./buildUpdate";
|
||||
import { startRun, stopSession, useRun, waitForExit, type RunSession } from "./store";
|
||||
import { rerunConfig, startRun, stopSession, useRun, waitForExit, type RunSession } from "./store";
|
||||
import { isUpdatable, updateAllowed, type UpdateMode } from "./updatePolicies";
|
||||
|
||||
export type UpdateSession = Pick<RunSession, "id" | "config"> & { debug?: DebugServiceSession };
|
||||
@@ -72,14 +72,15 @@ export async function updateApplication(session: UpdateSession, mode: string): P
|
||||
const debugging =
|
||||
!!session.debug || useDebugRunSessions.getState().sessions.some((s) => s.configId === session.config.id);
|
||||
if (mode === "restart") {
|
||||
const config = rerunConfig(session);
|
||||
if (session.debug) {
|
||||
await session.debug.stop();
|
||||
await executeAction("run.debugConfig", session.config);
|
||||
await executeAction("run.debugConfig", config);
|
||||
} else if (debugging) {
|
||||
await stopSession(session.id);
|
||||
await waitForExit([session.id]);
|
||||
await executeAction("run.debugConfig", session.config);
|
||||
} else await startRun(session.config, session.id);
|
||||
await executeAction("run.debugConfig", config);
|
||||
} else await startRun(config, session.id);
|
||||
return;
|
||||
}
|
||||
await maybeEditorService()?.saveAll();
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "./configs";
|
||||
import { contextConfig, fileRole } from "./context";
|
||||
import { envFromRows } from "./EnvDialog";
|
||||
import { defaultSelection } from "./store";
|
||||
import { configRemoved, defaultSelection, rerunConfig, useRun, type RunSession } from "./store";
|
||||
|
||||
const cfg = (over: Partial<RunConfig> & { id: string }): RunConfig => ({
|
||||
name: over.id,
|
||||
@@ -344,3 +344,38 @@ describe("context configuration", () => {
|
||||
expect(contextConfig("tools/gen.py", [])).toMatchObject({ command: "python3", args: ["tools/gen.py"] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("rerunning a session after its configuration changed (#1497)", () => {
|
||||
const session = (config: RunConfig, baseConfig?: RunConfig): RunSession => ({
|
||||
id: "s",
|
||||
config,
|
||||
baseConfig,
|
||||
runId: null,
|
||||
status: "finished",
|
||||
exitCode: 0,
|
||||
startedAt: 1,
|
||||
endedAt: 2,
|
||||
stdinClosed: true,
|
||||
});
|
||||
|
||||
it("launches the configuration as saved now, or the one it ran with once deleted", () => {
|
||||
const ran = cfg({ id: "app", command: "python", args: ["init.py"] });
|
||||
const edited = { ...ran, args: ["main.py"] };
|
||||
useRun.setState({ configs: [edited] });
|
||||
expect(rerunConfig(session(ran))).toBe(edited);
|
||||
expect(configRemoved("app")).toBe(false);
|
||||
useRun.setState({ configs: [] });
|
||||
expect(rerunConfig(session(ran))).toBe(ran);
|
||||
expect(configRemoved("app")).toBe(true);
|
||||
// A run that was never a listed configuration isn't "removed".
|
||||
expect(configRemoved("build:task")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the tests of a derived run (Rerun Failed Tests)", () => {
|
||||
const base = cfg({ id: "tests", isTest: true });
|
||||
const derived = { ...base, args: ["-k", "failing"] };
|
||||
useRun.setState({ configs: [base] });
|
||||
expect(rerunConfig(session(derived, base))).toBe(derived);
|
||||
useRun.setState({ configs: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,6 +256,17 @@ describe("configurations that ran", () => {
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]!.children!.map((c) => [c.label, c.detail])).toEqual([["dev", "Running"]]);
|
||||
});
|
||||
|
||||
it("drops a deleted configuration once its run has ended (#1497)", () => {
|
||||
const removed = (id: string) => id === "dev";
|
||||
const ended = runServiceItems([], [session("s1", dev, "finished", 1)], [], ["dev"], {}, [], removed);
|
||||
expect(ended).toEqual([]);
|
||||
const live = runServiceItems([], [session("s1", dev, "running", 1)], [], ["dev"], {}, [], removed);
|
||||
expect(live[0]!.children!.map((c) => [c.label, c.detail])).toEqual([["dev", "Running"]]);
|
||||
// Never a saved configuration (a build task, an external tool): its finished run stays.
|
||||
const adHoc = runServiceItems([], [session("s2", scratch, "finished", 1)], [], [], {}, [], removed);
|
||||
expect(adHoc[0]!.children!.map((c) => c.key)).toEqual(["run:config:context:file"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedded web server port", () => {
|
||||
|
||||
@@ -29,7 +29,16 @@ import { commandLine, configIcon, KIND_OPTIONS, kindLabel } from "./configs";
|
||||
import { SessionPanel } from "./RunView";
|
||||
import { showApplicationUpdate } from "./applicationUpdate";
|
||||
import { isUpdatable } from "./updatePolicies";
|
||||
import { loadConfigs, persistTemporary, startRun, stopSession, useRun, type RunSession } from "./store";
|
||||
import {
|
||||
configRemoved,
|
||||
loadConfigs,
|
||||
persistTemporary,
|
||||
rerunConfig,
|
||||
startRun,
|
||||
stopSession,
|
||||
useRun,
|
||||
type RunSession,
|
||||
} from "./store";
|
||||
import {
|
||||
currentServiceSession,
|
||||
currentServiceSessions,
|
||||
@@ -230,6 +239,7 @@ function executionDetail(s: ServiceSession | undefined): string | undefined {
|
||||
/**
|
||||
* Groups by configuration type: added types list all their configurations, other
|
||||
* types only those that ran. A default type (Spring Boot) without configurations is hidden.
|
||||
* A configuration deleted since it ran (`removed`) stays only while its run is live.
|
||||
*/
|
||||
export function runServiceItems(
|
||||
configs: readonly RunConfig[],
|
||||
@@ -238,11 +248,13 @@ export function runServiceItems(
|
||||
ran: readonly string[] = [],
|
||||
order: ServiceOrder = {},
|
||||
debug: readonly DebugServiceSession[] = [],
|
||||
removed: (id: string) => boolean = () => false,
|
||||
): ServiceTreeItem[] {
|
||||
const executions = serviceSessions(sessions, debug);
|
||||
const latest = currentServiceSessions(executions);
|
||||
const kinds = [...added];
|
||||
for (const s of executions) if (!kinds.includes(s.config.kind)) kinds.push(s.config.kind);
|
||||
const shown = (s: ServiceSession) => liveServiceSession(s) || !removed(s.config.id);
|
||||
for (const s of executions) if (shown(s) && !kinds.includes(s.config.kind)) kinds.push(s.config.kind);
|
||||
for (const id of ran) {
|
||||
const kind = configs.find((c) => c.id === id)?.kind;
|
||||
if (kind && !kinds.includes(kind)) kinds.push(kind);
|
||||
@@ -252,7 +264,7 @@ export function runServiceItems(
|
||||
? configs.filter((c) => c.kind === kind)
|
||||
: ran.map((id) => configs.find((c) => c.id === id)).filter((c): c is RunConfig => !!c && c.kind === kind);
|
||||
for (const s of latest.values())
|
||||
if (s.config.kind === kind && !entries.some((c) => c.id === s.config.id)) entries.push(s.config);
|
||||
if (s.config.kind === kind && shown(s) && !entries.some((c) => c.id === s.config.id)) entries.push(s.config);
|
||||
if (!entries.length && DEFAULT_TYPES.includes(kind)) return [];
|
||||
const children = orderServices(entries, order[kind]).map((c): ServiceTreeItem => {
|
||||
const s = latest.get(c.id);
|
||||
@@ -298,7 +310,15 @@ function useRoots(): ServiceTreeItem[] {
|
||||
}, [root, typesRoot, ranRoot]);
|
||||
return useMemo(
|
||||
() =>
|
||||
runServiceItems(configs, sessions, typesRoot === root ? types : [], ranRoot === root ? ran : [], order, debug),
|
||||
runServiceItems(
|
||||
configs,
|
||||
sessions,
|
||||
typesRoot === root ? types : [],
|
||||
ranRoot === root ? ran : [],
|
||||
order,
|
||||
debug,
|
||||
configRemoved,
|
||||
),
|
||||
[configs, sessions, types, typesRoot, ran, ranRoot, root, order, debug],
|
||||
);
|
||||
}
|
||||
@@ -335,7 +355,7 @@ function run(r: Resolved) {
|
||||
if (debug) debug.rerun?.();
|
||||
else if (r.session?.run) {
|
||||
const s = r.session.run;
|
||||
void startRun(s.config, s.id, s.baseConfig, { show: false });
|
||||
void startRun(rerunConfig(s), s.id, s.baseConfig, { show: false });
|
||||
} else if (r.config) void startRun(r.config, undefined, undefined, { show: false });
|
||||
}
|
||||
|
||||
|
||||
@@ -1010,7 +1010,7 @@ function notifyExit(sessionId: string, code: number | null) {
|
||||
toolWindowService().show("run", true);
|
||||
},
|
||||
},
|
||||
{ label: "Rerun", run: () => void startRun(session.config, sessionId) },
|
||||
{ label: "Rerun", run: () => void startRun(rerunConfig(session), sessionId, session.baseConfig) },
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -1070,9 +1070,35 @@ export function waitForExit(sessionIds: readonly string[], timeoutMs = 10_000):
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* What rerunning a session launches: its configuration as saved now (edited since, as IDEA), or
|
||||
* the one it ran with when that was deleted or never saved. A derived run (Rerun Failed Tests) keeps its tests.
|
||||
*/
|
||||
export function rerunConfig(session: Pick<RunSession, "config" | "baseConfig">): RunConfig {
|
||||
if (session.baseConfig) return session.config;
|
||||
return useRun.getState().configs.find((c) => c.id === session.config.id) ?? session.config;
|
||||
}
|
||||
|
||||
/** The session's original configuration, as saved now (see {@link rerunConfig}). */
|
||||
function baseConfigOf(session: RunSession): RunConfig {
|
||||
const base = session.baseConfig ?? session.config;
|
||||
return useRun.getState().configs.find((c) => c.id === base.id) ?? base;
|
||||
}
|
||||
|
||||
/** Ids of every configuration listed in this window, so a run of one deleted since can be told apart. */
|
||||
const listedConfigIds = new Set<string>();
|
||||
useRun.subscribe((s, prev) => {
|
||||
if (s.configs !== prev.configs) for (const c of s.configs) listedConfigIds.add(c.id);
|
||||
});
|
||||
|
||||
/** The configuration was saved or temporary in this window and has been removed since. */
|
||||
export function configRemoved(id: string): boolean {
|
||||
return listedConfigIds.has(id) && !useRun.getState().configs.some((c) => c.id === id);
|
||||
}
|
||||
|
||||
export function rerunSession(sessionId: string) {
|
||||
const session = getSession(sessionId);
|
||||
if (session) void startRun(session.config, sessionId, session.baseConfig);
|
||||
if (session) void startRun(rerunConfig(session), sessionId, session.baseConfig && baseConfigOf(session));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1082,7 +1108,7 @@ export function rerunSession(sessionId: string) {
|
||||
*/
|
||||
export function debugSession(sessionId: string) {
|
||||
const session = getSession(sessionId);
|
||||
if (session) void executeAction("run.debugConfig", session.baseConfig ?? session.config);
|
||||
if (session) void executeAction("run.debugConfig", baseConfigOf(session));
|
||||
}
|
||||
|
||||
/** Paths of the failed tests of a session's last run. */
|
||||
@@ -1095,7 +1121,7 @@ export function failedTests(sessionId: string): string[][] {
|
||||
export function rerunFailed(sessionId: string): boolean {
|
||||
const session = getSession(sessionId);
|
||||
if (!session || session.status === "running" || session.status === "starting") return false;
|
||||
const base = session.baseConfig ?? session.config;
|
||||
const base = baseConfigOf(session);
|
||||
const derived = rerunFailedConfig(base, failedTests(sessionId));
|
||||
if (!derived) return false;
|
||||
void startRun(derived, sessionId, base);
|
||||
@@ -1116,7 +1142,7 @@ export async function runSelectedTests(
|
||||
if (!session || !tests) return false;
|
||||
const nodes = nodeIds.map((id) => tests.get(id)).filter((n) => !!n);
|
||||
const paths = leafTestPaths(nodes);
|
||||
const base = session.baseConfig ?? session.config;
|
||||
const base = baseConfigOf(session);
|
||||
const name = nodes.length === 1 ? nodes[0]!.name : `${paths.length} tests`;
|
||||
const derived = testsConfig(base, paths, name);
|
||||
if (!derived) return false;
|
||||
|
||||
Reference in New Issue
Block a user