From 859b64176b79a437a874f2382d176ecc0da2b8e9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 12 Sep 2026 20:30:13 -0700 Subject: [PATCH] test(capture): decouple completion proof from frame cadence --- .../Commands/Core/CaptureCommand+Action.swift | 18 ++++++------- .../CaptureActionCommandEndToEndTests.swift | 27 ++++++++++++------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Apps/CLI/Sources/PeekabooCLI/Commands/Core/CaptureCommand+Action.swift b/Apps/CLI/Sources/PeekabooCLI/Commands/Core/CaptureCommand+Action.swift index 69856431c..54e093cb2 100644 --- a/Apps/CLI/Sources/PeekabooCLI/Commands/Core/CaptureCommand+Action.swift +++ b/Apps/CLI/Sources/PeekabooCLI/Commands/Core/CaptureCommand+Action.swift @@ -349,10 +349,10 @@ RuntimeOptionsConfigurable, InjectedRuntimeBackedCommand { actionCompletedNs: UInt64, captureDeadlineNs: UInt64 ) async throws -> CaptureActionCaptureCompletion { - let postRollDeadlineNs = try timing.postRollDeadline(startingAtNs: actionCompletedNs) - guard postRollDeadlineNs <= captureDeadlineNs else { - throw ValidationError("Action completion left insufficient time for the requested post-roll") - } + let postRollDeadlineNs = try timing.postRollDeadline( + startingAtNs: actionCompletedNs, + captureDeadlineNs: captureDeadlineNs + ) try await Self.sleep(untilMonotonicNanoseconds: postRollDeadlineNs) if timing.postRollMs > 0 { session.requestStop(afterSampleStartedAtOrAfter: actionCompletedNs) @@ -1037,17 +1037,15 @@ struct CaptureActionTiming { return captureDeadlineNs - postRollNs } - func postRollFits(startingAtNs: UInt64, captureDeadlineNs: UInt64) -> Bool { - guard let deadlineNs = try? self.postRollDeadline(startingAtNs: startingAtNs) else { return false } - return deadlineNs <= captureDeadlineNs - } - - func postRollDeadline(startingAtNs: UInt64) throws -> UInt64 { + func postRollDeadline(startingAtNs: UInt64, captureDeadlineNs: UInt64) throws -> UInt64 { let postRollNs = try Self.nanoseconds(milliseconds: self.postRollMs) let (deadlineNs, overflowed) = startingAtNs.addingReportingOverflow(postRollNs) guard !overflowed else { throw ValidationError("--post-roll overflowed the capture deadline") } + guard deadlineNs <= captureDeadlineNs else { + throw ValidationError("Action completion left insufficient time for the requested post-roll") + } return deadlineNs } diff --git a/Apps/CLI/Tests/CoreCLITests/CaptureActionCommandEndToEndTests.swift b/Apps/CLI/Tests/CoreCLITests/CaptureActionCommandEndToEndTests.swift index fc4d14ebf..88aa40ec0 100644 --- a/Apps/CLI/Tests/CoreCLITests/CaptureActionCommandEndToEndTests.swift +++ b/Apps/CLI/Tests/CoreCLITests/CaptureActionCommandEndToEndTests.swift @@ -943,6 +943,7 @@ extension CaptureActionCommandEndToEndTests { ) defer { try? FileManager.default.removeItem(at: outputDirectory) } let processStartIdentity = try #require(SystemIdentityResolver.processStartIdentity(getpid())) + var runnerReturnedAfterDeadline = false var command = CaptureActionCommand() command.mode = "frontmost" command.durationLimit = CLIDuration(argument: "2500ms") @@ -955,12 +956,14 @@ extension CaptureActionCommandEndToEndTests { deadlineProcessRunner: { childCommand, timeoutSeconds, completionDeadlineNs, onLaunch in let actionStartedNs = DispatchTime.now().uptimeNanoseconds onLaunch(actionStartedNs) - let completedAtNs = completionDeadlineNs - 50_000_000 + // Keep sampling headroom independent of the delayed delivery being tested. + let completedAtNs = actionStartedNs let delayedReturnNs = completionDeadlineNs + 20_000_000 let nowNs = DispatchTime.now().uptimeNanoseconds if delayedReturnNs > nowNs { try await Task.sleep(nanoseconds: delayedReturnNs - nowNs) } + runnerReturnedAfterDeadline = DispatchTime.now().uptimeNanoseconds > completionDeadlineNs return CaptureActionProcessResult( command: childCommand, processIdentifier: getpid(), @@ -988,7 +991,10 @@ extension CaptureActionCommandEndToEndTests { from: Data(contentsOf: URL(fileURLWithPath: receipt.path)) ) + #expect(runnerReturnedAfterDeadline) #expect(result.success) + #expect(manifest.timeline.actionCompletedMs == manifest.timeline.actionStartedMs) + #expect(manifest.provesPostActionSample) #expect(manifest.timeline.samplingCompletedMs >= manifest.timeline.actionCompletedMs + 100) } @@ -1138,14 +1144,17 @@ extension CaptureActionCommandEndToEndTests { captureDeadlineNs: captureDeadlineNs ) #expect(actionCompletionDeadlineNs == captureStartedNs + 2_300_000_000) - #expect(timing.postRollFits( - startingAtNs: actionCompletionDeadlineNs, - captureDeadlineNs: captureDeadlineNs - )) - #expect(!timing.postRollFits( - startingAtNs: actionCompletionDeadlineNs + 1, - captureDeadlineNs: captureDeadlineNs - )) + for start in [actionCompletionDeadlineNs - 50_000_000, actionCompletionDeadlineNs] { + #expect(try timing.postRollDeadline( + startingAtNs: start, + captureDeadlineNs: captureDeadlineNs + ) == start + 200_000_000) + } + for start in [actionCompletionDeadlineNs + 1, actionCompletionDeadlineNs + 20_000_000, UInt64.max] { + #expect(throws: (any Error).self) { + try timing.postRollDeadline(startingAtNs: start, captureDeadlineNs: captureDeadlineNs) + } + } #expect(throws: (any Error).self) { _ = try CaptureActionTiming.resolve( durationLimit: 1.7,