mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): report absent launch process before cleanup classification
This commit is contained in:
@@ -661,18 +661,15 @@ def trace(debugger, config):
|
||||
open(observation_path, "wb")
|
||||
)
|
||||
process = target.Launch(_launch_info(target, config, listener), error)
|
||||
if not error.Success() or not process.IsValid():
|
||||
process_created = process.IsValid()
|
||||
if not error.Success() or not process_created:
|
||||
exit_observed = _retire_failed_launch(
|
||||
process, config, listener, stdout, stderr
|
||||
)
|
||||
return {
|
||||
"status": "launch-error",
|
||||
"error": error.GetCString(),
|
||||
**(
|
||||
{"target_process_created": True}
|
||||
if process.IsValid()
|
||||
else {}
|
||||
),
|
||||
"target_process_created": process_created,
|
||||
**(
|
||||
{"target_process_exit_observed": True}
|
||||
if exit_observed
|
||||
|
||||
@@ -25,6 +25,73 @@ spec.loader.exec_module(tracer)
|
||||
|
||||
|
||||
class NativeTraceBudgetTests(unittest.TestCase):
|
||||
def test_invalid_launch_process_reports_explicit_no_creation(self):
|
||||
class InvalidProcess:
|
||||
def IsValid(self):
|
||||
return False
|
||||
|
||||
def Kill(self):
|
||||
raise AssertionError("an invalid process cannot own target cleanup")
|
||||
|
||||
class LaunchError:
|
||||
def Success(self):
|
||||
return False
|
||||
|
||||
def GetCString(self):
|
||||
return "No such file or directory: fixture/absent"
|
||||
|
||||
class Target:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def Launch(self, _launch_info, _error):
|
||||
return InvalidProcess()
|
||||
|
||||
class Debugger:
|
||||
def SetAsync(self, _enabled):
|
||||
pass
|
||||
|
||||
def GetInstanceName(self):
|
||||
return "fixture"
|
||||
|
||||
def CreateTarget(self, *_arguments):
|
||||
return Target()
|
||||
|
||||
class Pipe:
|
||||
def __init__(self, *_arguments):
|
||||
self.closed = False
|
||||
pipes.append(self)
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def close(self, expected_exit=False):
|
||||
self.closed = True
|
||||
|
||||
pipes = []
|
||||
debugger_api = types.SimpleNamespace(SetInternalVariable=lambda *_arguments: None)
|
||||
with patch.object(tracer.lldb, "SBDebugger", debugger_api, create=True), patch.object(
|
||||
tracer.lldb, "SBError", LaunchError, create=True
|
||||
), patch.object(tracer.lldb, "SBListener", lambda _name: object(), create=True), patch.object(
|
||||
tracer, "_hash_file", return_value="expected"
|
||||
), patch.object(tracer, "_file_identity", return_value=(1, 2)), patch.object(
|
||||
tracer, "_module_identity", return_value=("fixture", (1, 2), "fixture-uuid")
|
||||
), patch.object(tracer, "_launch_info", return_value=object()), patch.object(
|
||||
tracer, "_OutputPipe", Pipe
|
||||
):
|
||||
result = tracer.trace(Debugger(), {
|
||||
"executable": "fixture", "architecture": "arm64", "expected_sha256": "expected",
|
||||
"breakpoints": [], "working_directory": "fixture/absent",
|
||||
"stdout_path": "stdout", "stdout_capture_path": "stdout-capture",
|
||||
"stderr_path": "stderr", "stderr_capture_path": "stderr-capture",
|
||||
})
|
||||
self.assertEqual(result["status"], "launch-error")
|
||||
self.assertEqual(result["error"], "No such file or directory: fixture/absent")
|
||||
self.assertIs(result["target_process_created"], False)
|
||||
self.assertNotIn("target_process_exit_observed", result)
|
||||
self.assertEqual(len(pipes), 2)
|
||||
self.assertTrue(all(pipe.closed for pipe in pipes))
|
||||
|
||||
def test_failed_launch_does_not_infer_exit_from_a_failed_kill(self):
|
||||
class Process:
|
||||
def IsValid(self):
|
||||
|
||||
@@ -769,6 +769,9 @@ verified, the result or error preserves that uncertainty. The result reports:
|
||||
Launch errors keep the fact that LLDB created a process separate from whether
|
||||
it remains alive. An exited `SBProcess` is reported as exited only after LLDB
|
||||
confirms that state; unresolved ownership remains retained for cleanup retry.
|
||||
An invalid process returned by `Launch`, including a rejected working directory,
|
||||
reports explicit no-process creation. With no target PID or other retained
|
||||
resource, REA releases that runtime and admits the next observation.
|
||||
Cancellation observed while output and Evidence are finalized returns partial
|
||||
cancellation Evidence, even if the target has already exited.
|
||||
|
||||
|
||||
@@ -842,6 +842,70 @@ describe("LLDB late timeout publication", () => {
|
||||
});
|
||||
|
||||
describe("LLDB pre-target phase proof", () => {
|
||||
it("releases an invalid-process launch failure and permits the next complete observation", async () => {
|
||||
let launches = 0;
|
||||
const runtimes: string[] = [];
|
||||
const tracer = new LldbCallTracer(
|
||||
{},
|
||||
async (_executable, arguments_) => {
|
||||
const { config } = await configFromArguments(arguments_);
|
||||
launches += 1;
|
||||
runtimes.push(dirname(config.result_path));
|
||||
await writeFile(config.observation_path, "");
|
||||
await writeFile(config.stdout_capture_path, "");
|
||||
await writeFile(config.stderr_capture_path, "");
|
||||
await writeFile(
|
||||
config.result_path,
|
||||
JSON.stringify(
|
||||
launches === 1
|
||||
? {
|
||||
status: "launch-error",
|
||||
error: "No such file or directory: fixture/absent",
|
||||
target_process_created: false,
|
||||
}
|
||||
: successfulTracerOutput,
|
||||
),
|
||||
);
|
||||
return {
|
||||
kind: "exited",
|
||||
exitCode: launches === 1 ? 1 : 0,
|
||||
output: "",
|
||||
targetLaunched: launches === 1,
|
||||
targetIdentity: undefined,
|
||||
cleanupFailure: undefined,
|
||||
};
|
||||
},
|
||||
async () => ok(fakeTool),
|
||||
);
|
||||
try {
|
||||
const rejected = await tracer.trace(request);
|
||||
expect(rejected.ok).toBe(false);
|
||||
if (rejected.ok) throw new Error("Expected launch refusal");
|
||||
expect(projectAnalysisError(rejected.error)).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
details: {
|
||||
reason: "launch-failed: No such file or directory: fixture/absent",
|
||||
},
|
||||
});
|
||||
expect(rejected.error.cleanupIncomplete).toBe(false);
|
||||
expect(rejected.error.cleanupResources).toEqual([]);
|
||||
await expect(access(runtimes[0]!)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
const completed = await tracer.trace(request);
|
||||
expect(completed.ok).toBe(true);
|
||||
if (!completed.ok) throw completed.error;
|
||||
expect(completed.value.run).toMatchObject(successfulTracerOutput);
|
||||
expect(launches).toBe(2);
|
||||
await expect(access(runtimes[1]!)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
expect((await tracer.close()).ok).toBe(true);
|
||||
} finally {
|
||||
await tracer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("releases a proven pre-target integrity rejection and allows tracer reuse", async () => {
|
||||
let launches = 0;
|
||||
let firstRuntime: string | undefined;
|
||||
|
||||
Reference in New Issue
Block a user