diff --git a/examples/c-api/buildsystem/main.c b/examples/c-api/buildsystem/main.c index 02d291d4b..02d0cfb18 100644 --- a/examples/c-api/buildsystem/main.c +++ b/examples/c-api/buildsystem/main.c @@ -96,6 +96,8 @@ fancy_tool_create_command(void *context, const llb_data_t* name) { delegate.provide_value = fancy_command_provide_value; delegate.execute_command = fancy_command_execute_command; delegate.execute_command_ex = NULL; + delegate.execute_command_detached = NULL; + delegate.cancel_detached_command = NULL; delegate.is_result_valid = NULL; return llb_buildsystem_external_command_create(name, delegate); } diff --git a/include/llbuild/BuildSystem/BuildSystem.h b/include/llbuild/BuildSystem/BuildSystem.h index 536b34b57..bb138cf3a 100644 --- a/include/llbuild/BuildSystem/BuildSystem.h +++ b/include/llbuild/BuildSystem/BuildSystem.h @@ -300,6 +300,14 @@ class BuildSystem { /// Cancel the current build. void cancel(); + /// Add cancellation delegate. If the same delegate object was added before + /// then the call is a noop. + void addCancellationDelegate(core::CancellationDelegate* del); + + /// Remove cancellation delegate. If the delegate was not added or was + /// previously removed the call is a noop. + void removeCancellationDelegate(core::CancellationDelegate* del); + static uint32_t getSchemaVersion(); /// @} diff --git a/include/llbuild/BuildSystem/Command.h b/include/llbuild/BuildSystem/Command.h index 5e6dd2204..5d118c975 100644 --- a/include/llbuild/BuildSystem/Command.h +++ b/include/llbuild/BuildSystem/Command.h @@ -154,6 +154,9 @@ class Command : public basic::JobDescriptor { virtual bool isExternalCommand() const { return false; } + /// The command should execute outside the execution lanes. + virtual bool isDetached() const { return false; } + virtual void addOutput(BuildNode* node) final { outputs.push_back(node); node->getProducers().push_back(this); diff --git a/include/llbuild/Core/BuildEngine.h b/include/llbuild/Core/BuildEngine.h index 2e4571f39..e0f35b359 100644 --- a/include/llbuild/Core/BuildEngine.h +++ b/include/llbuild/Core/BuildEngine.h @@ -423,6 +423,14 @@ class BuildEngineDelegate { }; +/// Delegate interface for build cancellation notifications. +class CancellationDelegate { +public: + virtual ~CancellationDelegate(); + + virtual void buildCancelled() = 0; +}; + /// A build engine supports fast, incremental, persistent, and parallel /// execution of computational graphs. /// @@ -500,7 +508,10 @@ class BuildEngine { void resetForBuild(); bool isCancelled(); - + + void addCancellationDelegate(CancellationDelegate* del); + void removeCancellationDelegate(CancellationDelegate* del); + /// Attach a database for persisting build state. /// /// A database should only be attached immediately after creating the engine, diff --git a/lib/BuildSystem/BuildSystem.cpp b/lib/BuildSystem/BuildSystem.cpp index a1c2e6245..c9e92b462 100644 --- a/lib/BuildSystem/BuildSystem.cpp +++ b/lib/BuildSystem/BuildSystem.cpp @@ -356,6 +356,14 @@ class BuildSystemImpl { return buildEngine.isCancelled(); } + void addCancellationDelegate(CancellationDelegate* del) { + buildEngine.addCancellationDelegate(del); + } + + void removeCancellationDelegate(CancellationDelegate* del) { + buildEngine.removeCancellationDelegate(del); + } + /// @} }; @@ -1563,7 +1571,15 @@ class CommandTask : public Task { ti.complete(result.toData()); }); }; - ti.spawn({ &command, std::move(fn) }); + if (command.isDetached()) { + struct DetachedContext: public QueueJobContext { + unsigned laneID() const override { return -1; } + }; + DetachedContext ctx; + fn(&ctx); + } else { + ti.spawn({ &command, std::move(fn) }); + } } public: @@ -4115,6 +4131,18 @@ void BuildSystem::cancel() { } } +void BuildSystem::addCancellationDelegate(CancellationDelegate* del) { + if (impl) { + static_cast(impl)->addCancellationDelegate(del); + } +} + +void BuildSystem::removeCancellationDelegate(CancellationDelegate* del) { + if (impl) { + static_cast(impl)->removeCancellationDelegate(del); + } +} + void BuildSystem::resetForBuild() { static_cast(impl)->resetForBuild(); } diff --git a/lib/Core/BuildEngine.cpp b/lib/Core/BuildEngine.cpp index fa765a9e1..cc9e3488e 100644 --- a/lib/Core/BuildEngine.cpp +++ b/lib/Core/BuildEngine.cpp @@ -18,6 +18,7 @@ #include "llbuild/Core/BuildDB.h" #include "llbuild/Core/KeyID.h" +#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringMap.h" @@ -55,6 +56,8 @@ bool BuildEngineDelegate::shouldResolveCycle(const std::vector& items, return false; } +CancellationDelegate::~CancellationDelegate() = default; + #pragma mark - BuildEngine implementation namespace { @@ -103,6 +106,8 @@ class BuildEngineImpl : public BuildDBDelegate { std::atomic buildRunning{ false }; std::mutex buildEngineMutex; + llvm::DenseSet cancellationDelegates; + /// The queue of input requests to process. struct TaskInputRequest { /// The task making the request. @@ -1628,6 +1633,12 @@ class BuildEngineImpl : public BuildDBDelegate { void cancelBuild() { std::lock_guard guard(executionQueueMutex); + if (!buildCancelled) { + for (const auto &del : cancellationDelegates) { + del->buildCancelled(); + } + } + // Set the build cancelled marker. // // We do not need to handle waking the engine up, if it is waiting, because @@ -1646,6 +1657,20 @@ class BuildEngineImpl : public BuildDBDelegate { return buildCancelled; } + void addCancellationDelegate(CancellationDelegate* del) { + std::lock_guard guard(executionQueueMutex); + if (buildCancelled) { + del->buildCancelled(); + return; + } + cancellationDelegates.insert(del); + } + + void removeCancellationDelegate(CancellationDelegate* del) { + std::lock_guard guard(executionQueueMutex); + cancellationDelegates.erase(del); + } + bool attachDB(std::unique_ptr database, std::string* error_out) { assert(!db && "invalid attachDB() call"); assert(currentEpoch == 0 && "invalid attachDB() call"); @@ -1921,6 +1946,14 @@ bool BuildEngine::isCancelled() { return static_cast(impl)->isCancelled(); } +void BuildEngine::addCancellationDelegate(CancellationDelegate* del) { + static_cast(impl)->addCancellationDelegate(std::move(del)); +} + +void BuildEngine::removeCancellationDelegate(CancellationDelegate* del) { + static_cast(impl)->removeCancellationDelegate(del); +} + void BuildEngine::dumpGraphToFile(const std::string& path) { static_cast(impl)->dumpGraphToFile(path); } diff --git a/products/libllbuild/BuildSystem-C-API.cpp b/products/libllbuild/BuildSystem-C-API.cpp index 29ed00c54..edee26ebd 100644 --- a/products/libllbuild/BuildSystem-C-API.cpp +++ b/products/libllbuild/BuildSystem-C-API.cpp @@ -958,6 +958,26 @@ class CAPIExternalCommand : public ExternalCommand { // executeExternalCommand(). CAPIBuildValue* currentBuildValue = nullptr; + std::atomic detachedCommandFinished{false}; + std::unique_ptr cancellationDelegate = nullptr; + + bool isDetached() const override { + return cAPIDelegate.execute_command_detached != nullptr; + } + + static ProcessStatus getProcessStatusFromLLBResult(llb_buildsystem_command_result_t result) { + switch (result) { + case llb_buildsystem_command_result_succeeded: + return ProcessStatus::Succeeded; + case llb_buildsystem_command_result_failed: + return ProcessStatus::Failed; + case llb_buildsystem_command_result_cancelled: + return ProcessStatus::Cancelled; + case llb_buildsystem_command_result_skipped: + return ProcessStatus::Skipped; + } + } + virtual void executeExternalCommand(BuildSystem& system, core::TaskInterface ti, QueueJobContext* job_context, @@ -1001,6 +1021,65 @@ class CAPIExternalCommand : public ExternalCommand { completionFn.getValue()(result); }; + if (cAPIDelegate.execute_command_detached) { + struct ResultCallbackContext { + CAPIExternalCommand *thisCommand; + BuildSystem *buildSystem; + std::function doneFn; + + static void callback(void* result_ctx, + llb_buildsystem_command_result_t result, + llb_build_value* rvalue) { + ResultCallbackContext *ctx = static_cast(result_ctx); + auto thisCommand = ctx->thisCommand; + BuildSystem &system = *ctx->buildSystem; + auto doneFn = std::move(ctx->doneFn); + delete ctx; + + thisCommand->detachedCommandFinished = true; + if (auto cancellationDelegate = thisCommand->cancellationDelegate.get()) { + system.removeCancellationDelegate(cancellationDelegate); + thisCommand->cancellationDelegate = nullptr; + } + + thisCommand->currentBuildValue = reinterpret_cast(rvalue); + llbuild_defer { + delete thisCommand->currentBuildValue; + thisCommand->currentBuildValue = nullptr; + }; + doneFn(getProcessStatusFromLLBResult(result)); + } + }; + auto *callbackCtx = new ResultCallbackContext{this, &system, std::move(doneFn)}; + cAPIDelegate.execute_command_detached( + cAPIDelegate.context, + (llb_buildsystem_command_t*)this, + (llb_buildsystem_interface_t*)&system, + *reinterpret_cast(&ti), + (llb_buildsystem_queue_job_context_t*)job_context, + callbackCtx, ResultCallbackContext::callback); + + if (cAPIDelegate.cancel_detached_command) { + class CAPICancellationDelegate: public core::CancellationDelegate { + CAPIExternalCommand *thisCommand; + + public: + CAPICancellationDelegate(CAPIExternalCommand *thisCommand) : thisCommand(thisCommand) {} + + void buildCancelled() override { + if (thisCommand->detachedCommandFinished) + return; + thisCommand->cAPIDelegate.cancel_detached_command( + thisCommand->cAPIDelegate.context, + (llb_buildsystem_command_t*)this); + } + }; + this->cancellationDelegate = std::make_unique(this); + system.addCancellationDelegate(this->cancellationDelegate.get()); + } + return; + } + if (cAPIDelegate.execute_command_ex) { llb_build_value* rvalue = cAPIDelegate.execute_command_ex( cAPIDelegate.context, @@ -1031,22 +1110,7 @@ class CAPIExternalCommand : public ExternalCommand { (llb_buildsystem_interface_t*)&system, *reinterpret_cast(&ti), (llb_buildsystem_queue_job_context_t*)job_context); - ProcessStatus status; - switch (result) { - case llb_buildsystem_command_result_succeeded: - status = ProcessStatus::Succeeded; - break; - case llb_buildsystem_command_result_failed: - status = ProcessStatus::Failed; - break; - case llb_buildsystem_command_result_cancelled: - status = ProcessStatus::Cancelled; - break; - case llb_buildsystem_command_result_skipped: - status = ProcessStatus::Skipped; - break; - } - doneFn(status); + doneFn(getProcessStatusFromLLBResult(result)); } BuildValue computeCommandResult(BuildSystem& system, core::TaskInterface ti) override { @@ -1202,7 +1266,7 @@ llb_buildsystem_external_command_create( // Check that all required methods are provided. assert(delegate.start); assert(delegate.provide_value); - assert(delegate.execute_command); + assert(delegate.execute_command || delegate.execute_command_detached); return (llb_buildsystem_command_t*) new CAPIExternalCommand( StringRef((const char*)name->data, name->length), delegate); diff --git a/products/libllbuild/include/llbuild/buildsystem.h b/products/libllbuild/include/llbuild/buildsystem.h index d75b9cc30..cbc7fb26d 100644 --- a/products/libllbuild/include/llbuild/buildsystem.h +++ b/products/libllbuild/include/llbuild/buildsystem.h @@ -727,9 +727,10 @@ typedef struct llb_buildsystem_external_command_delegate_t_ { /// execution queue, so long as it arranges to only notify the system of /// completion once all that work is complete. /// - /// If defined, the build value returning `execute_command_ex` variant is - /// called first. If an 'invalid' buile value is returned, the bindings will - /// then try calling the legacy `execute_command` variant if it is defined. + /// If defined, the `execute_command_detached` variant is called first. + /// The build value returning `execute_command_ex` variant has priority next. + /// If an 'invalid' buile value is returned, the bindings will then try + /// calling the legacy `execute_command` variant if it is defined. /// /// The C API takes ownership of the value returned by `execute_command_ex`. llb_buildsystem_command_result_t (*execute_command)(void* context, @@ -744,6 +745,21 @@ typedef struct llb_buildsystem_external_command_delegate_t_ { llb_task_interface_t ti, llb_buildsystem_queue_job_context_t* job_context); + /// Called for the external command to do its work without blocking an + /// execution lane. When done the external command should call `result_fn` + /// passing a result and optionally a `llb_build_value`. + void (*execute_command_detached)(void* context, + llb_buildsystem_command_t* command, + llb_buildsystem_interface_t* bi, + llb_task_interface_t ti, + llb_buildsystem_queue_job_context_t* job_context, + void* result_ctx, + void (*result_fn)(void* result_ctx, llb_buildsystem_command_result_t, llb_build_value*)); + + /// If non-NULL and command is 'detached', the build system will call it to + /// request the command to cancel when the build is cancelled. + void (*cancel_detached_command)(void* context, llb_buildsystem_command_t* command); + /// Called by the build system to determine if the current build result /// remains valid. /// diff --git a/products/llbuildSwift/BuildSystemBindings.swift b/products/llbuildSwift/BuildSystemBindings.swift index d493ac259..799f7113e 100644 --- a/products/llbuildSwift/BuildSystemBindings.swift +++ b/products/llbuildSwift/BuildSystemBindings.swift @@ -205,18 +205,28 @@ private final class ToolWrapper { _delegate.get_signature = { return BuildSystem.toCommandWrapper($0!).getSignature($1!, $2!) } _delegate.start = { return BuildSystem.toCommandWrapper($0!).start($1!, $2!, $3) } _delegate.provide_value = { return BuildSystem.toCommandWrapper($0!).provideValue($1!, $2!, $3, $4!, $5) } - _delegate.execute_command = { return BuildSystem.toCommandWrapper($0!).executeCommand($1!, $2!, $3, $4!) } - if let _ = command as? ProducesCustomBuildValue { - _delegate.execute_command_ex = { - var value: BuildValue = BuildSystem.toCommandWrapper($0!).executeCommand($1!, $2!, $3, $4!) - return BuildValue.move(&value) - } - _delegate.is_result_valid = { - return BuildSystem.toCommandWrapper($0!).isResultValid($1!, $2!) + let shouldExecuteDetached = (command as? ExternalDetachedCommand)?.shouldExecuteDetached == true + if shouldExecuteDetached { + _delegate.execute_command_detached = { + return BuildSystem.toCommandWrapper($0!).executeDetachedCommand($1!, $2!, $3, $4!, $5, $6!) } + _delegate.cancel_detached_command = { + return BuildSystem.toCommandWrapper($0!).cancelDetachedCommand($1!) + } } else { - _delegate.execute_command_ex = nil - _delegate.is_result_valid = nil + _delegate.execute_command = { return BuildSystem.toCommandWrapper($0!).executeCommand($1!, $2!, $3, $4!) } + if let _ = command as? ProducesCustomBuildValue { + _delegate.execute_command_ex = { + var value: BuildValue = BuildSystem.toCommandWrapper($0!).executeCommand($1!, $2!, $3, $4!) + return BuildValue.move(&value) + } + _delegate.is_result_valid = { + return BuildSystem.toCommandWrapper($0!).isResultValid($1!, $2!) + } + } else { + _delegate.execute_command_ex = nil + _delegate.is_result_valid = nil + } } // Create the low-level command. @@ -293,6 +303,37 @@ public protocol ExternalCommand: AnyObject { func execute(_ command: Command, _ commandInterface: BuildSystemCommandInterface, _ jobContext: JobContext) -> CommandResult } +public protocol ExternalDetachedCommand: AnyObject { + /// Whether the command should run outside the execution lanes. + /// If true the build system will call `executeDetached` and `cancelDetached`. + var shouldExecuteDetached: Bool { get } + + /// Called to execute the command, without blocking the execution lanes. + /// The implementation should do the work asynchronously while returning as + /// soon as possible. + /// + /// - command: A handle to the executing command. + /// - commandInterface: A handle to the build system's command interface. + /// - jobContext: A handle to opaque context of the executing job for spawning external processes. + /// - resultFn: Callback for passing a result and optionally a `BuildValue`. + func executeDetached( + _ command: Command, + _ commandInterface: BuildSystemCommandInterface, + _ jobContext: JobContext, + _ resultFn: @escaping (CommandResult, BuildValue?) -> () + ) + + /// Called to request the command to cancel. + /// The implementation should aim to return as soon as possible. + /// + /// - command: A handle to the executing command. + func cancelDetached(_ command: Command) +} + +public extension ExternalDetachedCommand { + func cancelDetached(_ command: Command) {} +} + public protocol ProducesCustomBuildValue: AnyObject { /// Called to execute the given command that produces a custom build value. /// @@ -443,6 +484,29 @@ private final class CommandWrapper { return command.execute(_command, commandInterface, JobContext(jobContext)) } + func executeDetachedCommand( + _: OpaquePointer, + _ buildsystemInterface: OpaquePointer, + _ taskInterface: llb_task_interface_t, + _ jobContext: OpaquePointer, + _ resultContext: UnsafeMutableRawPointer?, + _ resultFn: @escaping (_ resultContext: UnsafeMutableRawPointer?, CommandResult, OpaquePointer?) -> () + ) { + let commandInterface = BuildSystemCommandInterface(buildsystemInterface, taskInterface) + func resultReceiver(_ result: CommandResult, value: BuildValue?) { + if var value { + resultFn(resultContext, result, BuildValue.move(&value)) + } else { + resultFn(resultContext, result, nil) + } + } + return (command as! ExternalDetachedCommand).executeDetached(_command, commandInterface, JobContext(jobContext), resultReceiver) + } + + func cancelDetachedCommand(_: OpaquePointer) { + return (command as! ExternalDetachedCommand).cancelDetached(_command) + } + func executeCommand(_: OpaquePointer, _ buildsystemInterface: OpaquePointer, _ taskInterface: llb_task_interface_t, _ jobContext: OpaquePointer) -> BuildValue { let commandInterface = BuildSystemCommandInterface(buildsystemInterface, taskInterface) return (command as! ProducesCustomBuildValue).execute(_command, commandInterface, JobContext(jobContext)) diff --git a/unittests/CAPI/BuildSystem-C-API.cpp b/unittests/CAPI/BuildSystem-C-API.cpp index 6bb53873e..f0badab42 100644 --- a/unittests/CAPI/BuildSystem-C-API.cpp +++ b/unittests/CAPI/BuildSystem-C-API.cpp @@ -147,6 +147,8 @@ depinfo_tester_tool_create_command(void *context, const llb_data_t* name) { delegate.provide_value = depinfo_tester_command_provide_value; delegate.execute_command = depinfo_tester_command_execute_command; delegate.execute_command_ex = NULL; + delegate.execute_command_detached = NULL; + delegate.cancel_detached_command = NULL; delegate.is_result_valid = NULL; return llb_buildsystem_external_command_create(name, delegate); } diff --git a/unittests/Swift/BuildSystemEngineTests.swift b/unittests/Swift/BuildSystemEngineTests.swift index 622823365..804315a44 100644 --- a/unittests/Swift/BuildSystemEngineTests.swift +++ b/unittests/Swift/BuildSystemEngineTests.swift @@ -36,6 +36,7 @@ protocol ExpectationCommand: AnyObject { // Command that expects to be executed. class BasicCommand: ExternalCommand, ExpectationCommand { private var executed = false + var completedTime: DispatchTime? func getSignature(_ command: Command) -> [UInt8] { return [] @@ -47,6 +48,7 @@ class BasicCommand: ExternalCommand, ExpectationCommand { func execute(_ command: Command, _ commandInterface: BuildSystemCommandInterface) -> Bool { executed = true + completedTime = DispatchTime.now() return true } @@ -95,8 +97,61 @@ class DependentCommand: BasicCommand { } } +/// Command that is executed without blocking the execution lanes. +class DetachedCommand: BasicCommand, ExternalDetachedCommand { + var shouldExecuteDetached: Bool { true } + + var startedTime: DispatchTime? + var isCancelled = false + + private let sema = DispatchSemaphore(value: 0) + + func cancelDetached(_ command: Command) { + isCancelled = true + sema.signal() + } + + func executeDetached( + _ command: Command, + _ commandInterface: BuildSystemCommandInterface, + _ jobContext: JobContext, + _ resultFn: @escaping (CommandResult, BuildValue?) -> () + ) { + startedTime = DispatchTime.now() + DispatchQueue(label: "detached").async { + _ = self.sema.wait(timeout: .now() + 1) + let result = super.execute(command, commandInterface) ? CommandResult.succeeded : CommandResult.failed + resultFn(result, nil) + } + } +} + +/// Command that blocks execution for 1 second. +class DelayedCommand: BasicCommand { + override func execute(_ command: Command, _ commandInterface: BuildSystemCommandInterface) -> Bool { + sleep(1) + return super.execute(command, commandInterface) + } +} + +/// Invokes a block when executed. +class CustomBlockCommand: BasicCommand { + let block: () -> () + + init(_ block: @escaping () -> ()) { + self.block = block + } + + override func execute(_ command: Command, _ commandInterface: BuildSystemCommandInterface) -> Bool { + defer { + block() + } + return super.execute(command, commandInterface) + } +} + final class TestTool: Tool { - let expectedCommands: [String: ExternalCommand] + var expectedCommands: [String: ExternalCommand] init(expectedCommands: [String: ExternalCommand]) { self.expectedCommands = expectedCommands @@ -178,15 +233,37 @@ class TestBuildSystem { let delegate: BuildSystemDelegate let buildSystem: BuildSystem - init(buildFile: String, databaseFile: String, expectedCommands: [String: ExternalCommand]) { - let tool = TestTool(expectedCommands: expectedCommands) + convenience init( + buildFile: String, + databaseFile: String, + expectedCommands: [String: ExternalCommand], + schedulerLanes: UInt32 = 0 + ) { + self.init( + buildFile: buildFile, + databaseFile: databaseFile, + tool: TestTool(expectedCommands: expectedCommands), + schedulerLanes: schedulerLanes + ) + } + + init( + buildFile: String, + databaseFile: String, + tool: Tool, + schedulerLanes: UInt32 = 0 + ) { delegate = TestBuildSystemDelegate(tool: tool) - buildSystem = BuildSystem(buildFile: buildFile, databaseFile: databaseFile, delegate: delegate) + buildSystem = BuildSystem(buildFile: buildFile, databaseFile: databaseFile, delegate: delegate, schedulerLanes: schedulerLanes) } func run(target: String) { XCTAssertTrue(buildSystem.build(target: target)) } + + func runNotSuccessful(target: String) { + XCTAssertFalse(buildSystem.build(target: target)) + } } class BuildSystemEngineTests: XCTestCase { @@ -398,4 +475,81 @@ commands: let fileInfo = BuildValueFileInfo(device: 1, inode: 2, mode: 3, size: 4, modTime: BuildValueFileTimestamp()) XCTAssertEqual(maincommandResult.value, BuildValue.SuccessfulCommand(outputInfos: [fileInfo])) } + + func testDetachedCommand() { + let buildFile = makeTemporaryFile(basicBuildManifest) + let databaseFile = makeTemporaryFile() + + let delayedCmd = DelayedCommand() + let detachedCmd1 = DetachedCommand() + let detachedCmd2 = DetachedCommand() + let detachedCmd3 = DetachedCommand() + let basicCmd = BasicCommand() + // The commands will get scheduled in reverse command-name order. + let expectedCommands = [ + "maincommand": DependentCommand(dependencyNames: [ + "5-delayed", + "4-detached", "3-detached", "2-detached", + "1-basic", + ]), + "5-delayed": delayedCmd, + "4-detached": detachedCmd1, + "3-detached": detachedCmd2, + "2-detached": detachedCmd3, + "1-basic": basicCmd, + ] + + // Using only one execution lane. + let buildSystem = TestBuildSystem( + buildFile: buildFile, + databaseFile: databaseFile, + expectedCommands: expectedCommands, + schedulerLanes: 1 + ) + buildSystem.run(target: "all") + + for (name, command) in expectedCommands { + XCTAssert(command.isFulfilled(), "\(name) did not execute") + } + // Verify that the detached commands were not blocked waiting for the execution lane to open. + XCTAssert(detachedCmd1.startedTime! < delayedCmd.completedTime!) + XCTAssert(detachedCmd2.startedTime! < delayedCmd.completedTime!) + XCTAssert(detachedCmd3.startedTime! < delayedCmd.completedTime!) + // Verify that the detached commands did not block the execution lane. + XCTAssert(Double(basicCmd.completedTime!.uptimeNanoseconds - delayedCmd.completedTime!.uptimeNanoseconds) / Double(NSEC_PER_SEC) < 0.5) + } + + func testCancelDetachedCommand() throws { + let buildFile = makeTemporaryFile(basicBuildManifest) + let databaseFile = makeTemporaryFile() + + let detachedCmd1 = DetachedCommand() + let detachedCmd2 = DetachedCommand() + let tool = TestTool(expectedCommands: [ + "maincommand": DependentCommand(dependencyNames: ["1-detached", "2-detached", "3-block"]), + "1-detached": detachedCmd1, + "2-detached": detachedCmd2, + ]) + + // Using only one execution lane. + let buildSystem = TestBuildSystem( + buildFile: buildFile, + databaseFile: databaseFile, + tool: tool, + schedulerLanes: 1 + ) + + let blockCmd = CustomBlockCommand({ + buildSystem.buildSystem.cancel() + }) + tool.expectedCommands["3-block"] = blockCmd + + buildSystem.runNotSuccessful(target: "all") + + XCTAssert(detachedCmd1.isCancelled) + XCTAssert(detachedCmd2.isCancelled) + // Verify that the detached commands cancelled and finished early. + XCTAssert(Double(detachedCmd1.completedTime!.uptimeNanoseconds - blockCmd.completedTime!.uptimeNanoseconds) / Double(NSEC_PER_SEC) < 0.5) + XCTAssert(Double(detachedCmd2.completedTime!.uptimeNanoseconds - blockCmd.completedTime!.uptimeNanoseconds) / Double(NSEC_PER_SEC) < 0.5) + } }