diff --git a/Benchmarks/CoverageBenchmarks/CoverageBenchmarks.swift b/Benchmarks/CoverageBenchmarks/CoverageBenchmarks.swift index b24ec478..c3b5a920 100644 --- a/Benchmarks/CoverageBenchmarks/CoverageBenchmarks.swift +++ b/Benchmarks/CoverageBenchmarks/CoverageBenchmarks.swift @@ -93,7 +93,7 @@ let benchmarks: @Sendable () -> Void = { let result = try await fuzz( duration: .seconds(0.1), persistence: .ephemeral, - coverageStrategy: .newEdge + scheduler: MutationScheduler.weightedPool(coverageStrategy: .newEdge) ) { (input: Int) in blackHole(input) } @@ -140,7 +140,7 @@ let benchmarks: @Sendable () -> Void = { let result = try await fuzz( duration: .seconds(0.1), persistence: .ephemeral, - coverageStrategy: CoverageStrategy(onEdge: { edge, _ in blackHole(edge) }) { _ in false } + scheduler: MutationScheduler.weightedPool(coverageStrategy: CoverageStrategy(onEdge: { edge, _ in blackHole(edge) }) { _ in false }) ) { (input: Int) in blackHole(input) } @@ -182,7 +182,7 @@ let benchmarks: @Sendable () -> Void = { let result = try await fuzz( duration: .seconds(0.1), persistence: .ephemeral, - coverageStrategy: .pathTrie + scheduler: MutationScheduler.weightedPool(coverageStrategy: .pathTrie) ) { (input: Int) in blackHole(input) } diff --git a/Benchmarks/ProfiledBenchmark/ProfiledBenchmark.swift b/Benchmarks/ProfiledBenchmark/ProfiledBenchmark.swift index 0e01714b..460091c6 100644 --- a/Benchmarks/ProfiledBenchmark/ProfiledBenchmark.swift +++ b/Benchmarks/ProfiledBenchmark/ProfiledBenchmark.swift @@ -49,7 +49,8 @@ let benchmarks: @Sendable () -> Void = { let startWall = DispatchTime.now().uptimeNanoseconds let result = try await fuzz( - duration: .seconds(0.1), persistence: .replace, coverageStrategy: .pathTrie + duration: .seconds(0.1), persistence: .replace, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .pathTrie) ) { (input: Int) in blackHole(input) } diff --git a/LLVMPasses/EmitCmpTrace.cpp b/LLVMPasses/EmitCmpTrace.cpp new file mode 100644 index 00000000..6c587811 --- /dev/null +++ b/LLVMPasses/EmitCmpTrace.cpp @@ -0,0 +1,125 @@ +// Out-of-tree LLVM pass plugin: emit __sanitizer_cov_trace_cmp* callbacks +// ourselves for the comparisons we care about, replacing SanitizerCoverage's +// trace-cmp emission. Build the SUT with `-sanitize-coverage=edge,pc-table` +// (NO trace-cmp) and load this plugin; it emits cmp callbacks for every integer +// comparison EXCEPT trap guards (bounds / overflow / precondition checks, whose +// branch reaches `unreachable`). Faithful to InjectTraceForCmp otherwise. + +#include "llvm/ADT/SmallVector.h" +#include "llvm/IR/DataLayout.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/IRBuilder.h" +#include "llvm/IR/Instructions.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/PassManager.h" +#include "llvm/Passes/PassBuilder.h" +#include "llvm/Passes/PassPlugin.h" +#include "llvm/Support/Compiler.h" + +using namespace llvm; + +namespace { + +// True iff following single-successor edges from BB reaches an `unreachable` +// terminator within MaxDepth hops. Swift lowers cond_fail (bounds/overflow/ +// precondition checks) to a branch whose failure edge runs — directly or via an +// empty split critical edge + shared trap merge block — into a +// _fatalErrorMessage/llvm.trap block ending in `unreachable`. +static bool reachesUnreachable(const BasicBlock *BB, unsigned MaxDepth) { + for (unsigned I = 0; BB && I <= MaxDepth; ++I) { + if (isa(BB->getTerminator())) + return true; + BB = BB->getSingleSuccessor(); + } + return false; +} + +static bool isTrapGuard(ICmpInst *CMP) { + if (!CMP->hasOneUse()) + return false; + auto *BR = dyn_cast(CMP->user_back()); + if (!BR || !BR->isConditional()) + return false; + for (BasicBlock *Succ : BR->successors()) + if (reachesUnreachable(Succ, /*MaxDepth=*/3)) + return true; + return false; +} + +struct EmitCmpTrace : PassInfoMixin { + PreservedAnalyses run(Module &M, ModuleAnalysisManager &) { + LLVMContext &Ctx = M.getContext(); + const DataLayout &DL = M.getDataLayout(); + Type *VoidTy = Type::getVoidTy(Ctx); + IntegerType *IntTys[4] = {Type::getInt8Ty(Ctx), Type::getInt16Ty(Ctx), + Type::getInt32Ty(Ctx), Type::getInt64Ty(Ctx)}; + const char *CmpNames[4] = { + "__sanitizer_cov_trace_cmp1", "__sanitizer_cov_trace_cmp2", + "__sanitizer_cov_trace_cmp4", "__sanitizer_cov_trace_cmp8"}; + const char *ConstNames[4] = {"__sanitizer_cov_trace_const_cmp1", + "__sanitizer_cov_trace_const_cmp2", + "__sanitizer_cov_trace_const_cmp4", + "__sanitizer_cov_trace_const_cmp8"}; + FunctionCallee CmpFn[4], ConstFn[4]; + for (int i = 0; i < 4; ++i) { + FunctionType *FT = FunctionType::get(VoidTy, {IntTys[i], IntTys[i]}, false); + CmpFn[i] = M.getOrInsertFunction(CmpNames[i], FT); + ConstFn[i] = M.getOrInsertFunction(ConstNames[i], FT); + } + + bool Changed = false; + for (Function &F : M) { + if (F.isDeclaration()) + continue; + if (F.getName().starts_with("__sanitizer_")) + continue; + if (F.hasFnAttribute(Attribute::NoSanitizeCoverage)) + continue; + + SmallVector Targets; + for (BasicBlock &BB : F) + for (Instruction &I : BB) + if (auto *CMP = dyn_cast(&I)) + if (!isTrapGuard(CMP)) + Targets.push_back(CMP); + + for (ICmpInst *CMP : Targets) { + Value *A0 = CMP->getOperand(0); + Value *A1 = CMP->getOperand(1); + if (!A0->getType()->isIntegerTy()) + continue; + uint64_t TS = DL.getTypeStoreSizeInBits(A0->getType()); + int Idx = TS == 8 ? 0 : TS == 16 ? 1 : TS == 32 ? 2 : TS == 64 ? 3 : -1; + if (Idx < 0) + continue; + bool C0 = isa(A0), C1 = isa(A1); + if (C0 && C1) + continue; // both const: nothing to learn + FunctionCallee Fn = CmpFn[Idx]; + if (C0 || C1) { + Fn = ConstFn[Idx]; + if (C1) + std::swap(A0, A1); // const goes first, matching SanCov + } + IRBuilder<> IRB(CMP); + Type *Ty = IntTys[Idx]; + IRB.CreateCall(Fn, {IRB.CreateIntCast(A0, Ty, /*isSigned=*/true), + IRB.CreateIntCast(A1, Ty, /*isSigned=*/true)}); + Changed = true; + } + } + return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); + } +}; + +} // namespace + +extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo +llvmGetPassPluginInfo() { + return {LLVM_PLUGIN_API_VERSION, "EmitCmpTrace", "0.1", + [](PassBuilder &PB) { + PB.registerOptimizerLastEPCallback( + [](ModulePassManager &MPM, OptimizationLevel, + ThinOrFullLTOPhase) { MPM.addPass(EmitCmpTrace()); }); + }}; +} diff --git a/LLVMPasses/TagCompilerGenerated.cpp b/LLVMPasses/TagCompilerGenerated.cpp new file mode 100644 index 00000000..46c6e32b --- /dev/null +++ b/LLVMPasses/TagCompilerGenerated.cpp @@ -0,0 +1,94 @@ +// Out-of-tree LLVM pass plugin: tag compiler-generated Swift functions with +// `nosanitize_coverage` BEFORE SanitizerCoverage runs, so SanCov emits no edge +// guards / pc-table entries (and no cmp callbacks) for them. This replaces the +// RUNTIME edge filter (SanCovHooks.c: g_edge_state / sancov_apply_edge_filter / +// sancov_is_compiler_generated) with a compile-time decision. +// +// The name patterns are ported verbatim from sancov_is_compiler_generated. +// MUST run at OptimizerLast (after coroutine splitting, so async funclet names +// like ...TQ3_ exist) and before SanitizerCoverage (plugin EP callbacks are +// registered ahead of Swift's, so this pass runs first at OptimizerLast). + +#include "llvm/ADT/StringRef.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/PassManager.h" +#include "llvm/Passes/PassBuilder.h" +#include "llvm/Passes/PassPlugin.h" +#include "llvm/Support/Compiler.h" + +using namespace llvm; + +namespace { + +// Verbatim port of SanCovHooks.c `sancov_is_compiler_generated`, operating on +// the function's mangled name. Async continuation edges (T[QY]_) are filtered +// for pathTrie determinism, not just noise — keep parity exact. +static bool isCompilerGenerated(StringRef N) { + if (N.starts_with("__swift_")) + return true; + if (N.starts_with("_swift_")) + return true; + size_t len = N.size(); + if (len < 3) + return false; + + if (N.ends_with("Wl") || N.ends_with("WL") || N.ends_with("Ma")) + return true; + // WO + specifier (all outlined operations: WOh/c/d/r/b/e/...) + if (N[len - 3] == 'W' && N[len - 2] == 'O') + return true; + if (N.ends_with("TA") || N.ends_with("TR") || N.ends_with("TK") || + N.ends_with("Mr")) + return true; + if (N.contains("TATQ") || N.contains("TATY") || N.contains("TRTQ") || + N.contains("TRTY")) + return true; + // global/static variable addressor + if (N.ends_with("vau")) + return true; + + // bare async resume/yield: ...T[QY]_ + if (len >= 4 && N[len - 1] == '_') { + size_t p = len - 2; + while (p > 0 && N[p] >= '0' && N[p] <= '9') + --p; + if (p >= 1 && (N[p] == 'Q' || N[p] == 'Y') && N[p - 1] == 'T') + return true; + } + + // default argument generator: ...fA_ or ...fA_ + if (N[len - 3] == 'f' && N[len - 2] == 'A' && N[len - 1] == '_') + return true; + if (len >= 4 && N[len - 4] == 'f' && N[len - 3] == 'A' && N[len - 1] == '_') + return true; + + return false; +} + +struct TagCompilerGenerated : PassInfoMixin { + PreservedAnalyses run(Module &M, ModuleAnalysisManager &) { + for (Function &F : M) { + if (F.isDeclaration()) + continue; + if (F.hasFnAttribute(Attribute::NoSanitizeCoverage)) + continue; + if (isCompilerGenerated(F.getName())) + F.addFnAttr(Attribute::NoSanitizeCoverage); + } + // Only function attributes change; no IR/CFG mutation. + return PreservedAnalyses::all(); + } +}; + +} // namespace + +extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo +llvmGetPassPluginInfo() { + return {LLVM_PLUGIN_API_VERSION, "TagCompilerGenerated", "0.1", + [](PassBuilder &PB) { + PB.registerOptimizerLastEPCallback( + [](ModulePassManager &MPM, OptimizationLevel, + ThinOrFullLTOPhase) { MPM.addPass(TagCompilerGenerated()); }); + }}; +} diff --git a/Package.swift b/Package.swift index b10c0a06..40cd659d 100644 --- a/Package.swift +++ b/Package.swift @@ -2,6 +2,35 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +import Foundation + +// Compile-time coverage instrumentation is provided by two out-of-tree LLVM +// pass plugins (sources in LLVMPasses/, built by scripts/build-llvm-plugins.sh +// into .build/llvm-plugins). They replace the former runtime filters that used +// to live in SanCovHooks.c: +// TagCompilerGenerated — tags compiler-generated functions NoSanitizeCoverage +// so SanCov emits no edge/cmp guards for them (compile- +// time edge filter; async resume/yield edges stay out, +// preserving pathTrie determinism). MUST load first so +// EmitCmpTrace also skips those functions. +// EmitCmpTrace — emits __sanitizer_cov_trace_cmp* ourselves for the +// comparisons we want, dropping trap-guard cmps +// (bounds/overflow/precondition). Used INSTEAD of +// `-sanitize-coverage=…,trace-cmp`. +// build-local-toolchain.sh builds the plugins before compiling; for a raw +// `swift build` run scripts/build-llvm-plugins.sh first. +let pluginDir = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .appendingPathComponent(".build/llvm-plugins") +func loadPass(_ name: String) -> [String] { + ["-Xfrontend", "-load-pass-plugin=\(pluginDir.appendingPathComponent(name + ".dylib").path)"] +} + +// Edge coverage with the compile-time compiler-generated filter. +let edgeCoverage: [String] = + ["-sanitize=undefined", "-sanitize-coverage=edge,pc-table"] + loadPass("TagCompilerGenerated") +// Edge + comparison coverage (the cmp channel via EmitCmpTrace, not stock trace-cmp). +let edgeCmpCoverage: [String] = edgeCoverage + loadPass("EmitCmpTrace") let package = Package( name: "PropertyTestingKit", @@ -132,10 +161,10 @@ let package = Package( ], exclude: ["Corpus", "Fuzzing/Corpus"], swiftSettings: [ - .unsafeFlags([ - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + // edge + comparison coverage; the cmp channel (via EmitCmpTrace) + // lets the input-to-state integration tests exercise the real cmp + // hooks (FuzzInputToStateTests fuzzes a magic-value SUT in-target). + .unsafeFlags(edgeCmpCoverage) ] ), .testTarget( @@ -148,10 +177,7 @@ let package = Package( .product(name: "Clocks", package: "swift-clocks"), ], swiftSettings: [ - .unsafeFlags([ - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + .unsafeFlags(edgeCoverage) ] ), .testTarget( @@ -163,10 +189,7 @@ let package = Package( ], exclude: ["Corpus"], swiftSettings: [ - .unsafeFlags([ - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + .unsafeFlags(edgeCoverage) ] ), .testTarget( @@ -177,10 +200,7 @@ let package = Package( ], swiftSettings: [ // Enable sanitizer coverage for thread-local coverage testing - .unsafeFlags([ - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + .unsafeFlags(edgeCoverage) ] ), // TSanTests: Race condition tests that exercise concurrent code paths. @@ -205,10 +225,7 @@ let package = Package( ], swiftSettings: [ .swiftLanguageMode(.v5), - .unsafeFlags([ - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + .unsafeFlags(edgeCoverage) ] ), .testTarget( @@ -238,11 +255,7 @@ package.targets += [ swiftSettings: [ // Enable sanitizer coverage so we have realistic counter counts // Note: sanitize-coverage requires a sanitizer to be enabled - .unsafeFlags([ - "-O", - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + .unsafeFlags(["-O"] + edgeCoverage) ], linkerSettings: [ // Add rpath for Testing.framework from Xcode (needed for local toolchain) @@ -263,11 +276,11 @@ package.targets += [ ], path: "Benchmarks/ProfiledBenchmark", swiftSettings: [ - .unsafeFlags([ - "-O", - "-sanitize=undefined", - "-sanitize-coverage=edge,pc-table" - ]) + // edge + comparison coverage (cmp channel via EmitCmpTrace) so the + // benchmark closure's integer comparisons dispatch through + // sancov_dispatch_cmp → the boundary observer, exercising the + // per-comparison hot path under profiling. + .unsafeFlags(["-O"] + edgeCmpCoverage) ], linkerSettings: [ // Add rpath for Testing.framework from Xcode (needed for local toolchain) diff --git a/PropertyTestingKit.xcodeproj/project.pbxproj b/PropertyTestingKit.xcodeproj/project.pbxproj index e02a9938..9bca2b0b 100644 --- a/PropertyTestingKit.xcodeproj/project.pbxproj +++ b/PropertyTestingKit.xcodeproj/project.pbxproj @@ -27,8 +27,10 @@ 095C1FC99432825350AF90D0 /* CorpusPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B54D66375BF29A19B143AE3 /* CorpusPersistence.swift */; }; 0A0812CF494F9244089B781C /* PathGrams.swift in Sources */ = {isa = PBXBuildFile; fileRef = B030A82E402C8D5202BEFE53 /* PathGrams.swift */; }; 0A0B966D3B103DDD9ADC457C /* CoverageCountersClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7DA3E55A8132FE210D8EAF /* CoverageCountersClient.swift */; }; + 0A8EA9C99291BC201A9856F9 /* BoundaryDistanceStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6BB002C2461C0A4D7BFBC66 /* BoundaryDistanceStrategyTests.swift */; }; 0AEA251D3FF9432F04D9FC04 /* CoverageGapDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A7DD272E538B8E8CFE5C75 /* CoverageGapDetectorTests.swift */; }; 0AF273C35C44F4B57C9AE477 /* CoverageEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9791710C8985E9069A0AAEA9 /* CoverageEngineTests.swift */; }; + 0B4D6F87B283CB18C2DF623C /* CmpOnlySchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049F353FB2914B702681DEEC /* CmpOnlySchedulerTests.swift */; }; 0BA9AD179FB702D07F12F65E /* NewEdgeStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6744BAE7780BE09993D850 /* NewEdgeStrategy.swift */; }; 0BBBCECACEFDD2F0255FDB97 /* ParallelEarlyCancelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C99FD379289FA24BBE7A5B /* ParallelEarlyCancelTest.swift */; }; 0F0864904370837E3166E2A7 /* UnicodeMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A2C049941E715713FD44DD /* UnicodeMutator.swift */; }; @@ -46,26 +48,35 @@ 16A96F144F2C00EB30D5AB81 /* ScheduleControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA1A34B8FC6F4EFE3022741B /* ScheduleControl.framework */; }; 171C6F6047C7576F09F61374 /* ABAInheritanceHandleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD3FFE8C65FFC7DFDAABC32 /* ABAInheritanceHandleTests.swift */; }; 17D551F0DC9A49573320CBC2 /* ck_ht.c in Sources */ = {isa = PBXBuildFile; fileRef = DF69D0BBA6357FA0A5F78ABC /* ck_ht.c */; }; + 17D5DCD59887D9103E1C9C4A /* AdaptiveDepthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123C6DAB5ECCBBEC26AB3C89 /* AdaptiveDepthPolicyTests.swift */; }; 186AD859AC88863837BB5318 /* Character+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9F69D3B0B4EB52DD10C3C5 /* Character+MutatorProviding.swift */; }; 18AD5DD480F2B7FF17911BD8 /* SanCovResetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5793C170004170EB1BC50580 /* SanCovResetTests.swift */; }; 190CC6D79C904001E2EC76BF /* PathTrieStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E409E9172BADE44207E55E /* PathTrieStrategyTests.swift */; }; + 19207BE9E1FF2282D2A2AE1B /* OwnershipLedger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA529AFE126535DFFFB53A /* OwnershipLedger.swift */; }; 195A0ADBE2AE0752E292BE3B /* URLMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BBCB1776E53F47AD0D68618 /* URLMutator.swift */; }; 19E5E7F83FA7FB0675B65818 /* MockDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64D06718A05E1272E84861D /* MockDatabase.swift */; }; + 1B9F28F98D6A4AE8216C770B /* SanCovSuppressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF17F797111C786B45F76BC5 /* SanCovSuppressionTests.swift */; }; 1BC58299C9881E94394571A2 /* PhoneNumberMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBB3E73A77D729B720FC8ADA /* PhoneNumberMutator.swift */; }; 1BF75DF93AC5857F7B8DABD3 /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; 1C9770C71F0A01C606B38EF7 /* MutationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E05741C671DFC85D8A63A2 /* MutationScheduler.swift */; }; + 1CCBBFC23E17E7C597669ED0 /* ComparisonCoverageStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2704E8BD88F40CF9BF414641 /* ComparisonCoverageStrategy.swift */; }; 1D87FB9564146A8E2D57EB61 /* Int+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951E33D0078C4A59FF897AD4 /* Int+MutatorProviding.swift */; }; 1DCA977CDD3021E0E016C3FC /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = B99C4D96737480ABC5B2E668 /* Dependencies */; }; 1FC08EF5231A3765481B884F /* CorpusCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B488F4DEDD008E96E7F6C3 /* CorpusCoordinator.swift */; }; 20F73DB698F40134660DEB4E /* PropertyTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 21C930230313DB0CC2C877D3 /* ScheduleFlatten.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C679683B4D3CDAE4E9BD50C /* ScheduleFlatten.swift */; }; + 21D5E40774A2BA976365E0C3 /* ComparisonObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AECCD66326E8AEA9BEF03D3 /* ComparisonObserverTests.swift */; }; 244F543DDFAA24140A76485F /* FuzzEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC4138150CDC1ABC2DE7C65 /* FuzzEngineTests.swift */; }; 246F4DE4D1646F70F78D238C /* EdgeHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A467224B8821AF297298A6 /* EdgeHooks.swift */; }; + 24FE24E8E3703FE7A68D9F28 /* SchedulerProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AD67782E1C097D160C9DFD /* SchedulerProbe.swift */; }; 26AFB7A88CB3B4D8386EF7A9 /* SignatureHashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7508FECD8A4E1A1528B9E5 /* SignatureHashTests.swift */; }; 2751CD1820839130FC3FEACC /* String+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204BDF70E7D5F50251C7A775 /* String+MutatorProviding.swift */; }; 278C0BBB25AD6646850AECA4 /* ScheduleControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA1A34B8FC6F4EFE3022741B /* ScheduleControl.framework */; }; 286622DA029C3AC20D7DA262 /* PropertyTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; }; + 295F375C0DEBF15A2495AD12 /* EdgeUnionBitmapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8AA636EFB6AED289971643 /* EdgeUnionBitmapTests.swift */; }; + 29653509BB27301F722388D4 /* AdaptiveDepthInsertedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B76959C25CA4FE7BE3B00D0 /* AdaptiveDepthInsertedTests.swift */; }; 2A49D41C9A1A8C66E533B43B /* ArrayPositionAwareMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28BE722825C9C30C1B1DE68D /* ArrayPositionAwareMutator.swift */; }; + 2AABED73782D56B97CB8D409 /* ComparisonCoverageStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DD8EB93B39B3A786B2B45 /* ComparisonCoverageStrategyTests.swift */; }; 2BE81B2317F7F136F5AF3E82 /* NegativeIntMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813527F798EBF4073FCCB7C9 /* NegativeIntMutator.swift */; }; 2CA446146BF11AFA8C0DDD7A /* MutationLineageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B52072822CAE79551FCAB6 /* MutationLineageTests.swift */; }; 2D75AE633E0540CD4E43BF48 /* EdgeHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD0587CE21A2AB1B87113BEE /* EdgeHooks.framework */; }; @@ -77,28 +88,37 @@ 30ABA134D5956FD446CCE3C9 /* IntMutators.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FA092D1F7A3D60D1BF95CB /* IntMutators.swift */; }; 31B706837C40F40D05B1352B /* InstrumentationSeamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC67D944E495D340262E27CC /* InstrumentationSeamTests.swift */; }; 31C68AA799FA3B1BDFD06296 /* ContinuousClockClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E5DD3B8575BC75880E15FF /* ContinuousClockClient.swift */; }; + 322D41378EE00FAF31E761A7 /* CoverageStrategyCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C7626BD5E1269597F45D31 /* CoverageStrategyCompositionTests.swift */; }; + 33BBAD81800871542AD54BB0 /* AtomicRep.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A060AE28AC76C1D9CE6F8D /* AtomicRep.swift */; }; 35063D9CAF31E1E2874B7E02 /* DrainConcurrencyTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 601C8A74E0D1285E70084C34 /* DrainConcurrencyTest.swift */; }; 3509EA0470EF5B7431586D5C /* ShrinkConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B54F79AF815D96ED3F562D /* ShrinkConfig.swift */; }; 35E17C6B02665DCFD28C61CF /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = C6C96F00E7EA2EE69BCEDC3A /* Atomics */; }; 39A37D202D51C735DD44C2F8 /* SanCovHooks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3AE90F2D2F5E78080AAB081C /* FuzzAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99DF2D2D7A9C78BEFDA1C9FF /* FuzzAPITests.swift */; }; + 3AFFE52B1972946459F74ECC /* HitCountAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65874BE183B686F124793FB4 /* HitCountAccumulator.swift */; }; 3BD3F103CBAC2DB810328150 /* Shrinkable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447218A4047DEE5BBB9EFF09 /* Shrinkable.swift */; }; + 3C347A9952CC4C8E4AC5B11A /* GlobalEverCoveredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF098748DE9F44058DB7BB45 /* GlobalEverCoveredTests.swift */; }; 3CD3F99321403136AB93C484 /* SchedulerSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96335D04A27579950460A29 /* SchedulerSupport.swift */; }; 3DE4055B12544F96FB88B490 /* FuzzEngine+Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF7DD32AFECCA9E69821998D /* FuzzEngine+Config.swift */; }; + 3E657BB5EE826DEDF6B354D6 /* AtomicFeatureSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FAEEDF5D30CDE9997EDCEAE /* AtomicFeatureSet.swift */; }; 408691F1E373D68F229EA2D0 /* FuzzCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 69A1455BBDC81EED5F1D2C67 /* FuzzCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 413720205EA64C2558BD9F04 /* FuzzAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A18400D950AE2D1D13443E9A /* FuzzAPI.swift */; }; 41ACAFC0BD7C2CB8EFFC25B3 /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; 4457DF779519E4489834FCE4 /* ScratchPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080B558DCF0B7BC8051813FB /* ScratchPad.swift */; }; + 4536E5471E56302535CE66F3 /* CmpRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED5EFB8B81DD1F34E12B2635 /* CmpRecorderTests.swift */; }; 4663207B9583FDBBC72B6069 /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; 4663EC7618D3511158524207 /* RaceConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797B369A783CF8DA4F7C9190 /* RaceConditionTests.swift */; }; 4B20768005EE54597E64312B /* CoverageDeterminismTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3593C7A78C4DB15293ED6F47 /* CoverageDeterminismTest.swift */; }; + 4B2D7D666F6C29F7DDD234C7 /* BoundarySiteAccumulatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C342768E738E2FE06AEF0624 /* BoundarySiteAccumulatorTests.swift */; }; 4B46C972C1518B04075D7EED /* DWARFSymbolizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F1917814603DE56511E5F24 /* DWARFSymbolizer.swift */; }; 4CD221E7828FFFA5D503E515 /* GenericTimerPollerReproductionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779153C9B2EE2604BB0510F4 /* GenericTimerPollerReproductionTest.swift */; }; 4D084EAD4D986C3036915DA8 /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = ACF4244C122A62263A93B0DE /* Dependencies */; }; + 4D3E5F1B9F6C98DBC6821F3A /* BoundarySiteAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F7019EDAF76A64238D3D748 /* BoundarySiteAccumulator.swift */; }; 4E80F115414D70956138C81C /* UInt8+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE585F0A050A5BF56B67442E /* UInt8+MutatorProviding.swift */; }; 4EB9436B27158A5C6839F9BA /* DWARFSymbolizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E97A029218EB361C14F01D /* DWARFSymbolizerTests.swift */; }; 5006BB1A539FAA39A92FC158 /* ScheduleHooks.c in Sources */ = {isa = PBXBuildFile; fileRef = 06ED1D87CAF04357C6E3DFE9 /* ScheduleHooks.c */; }; 5127D413390D3FE20602F726 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = ED638EEB32787F0136CC6158 /* DequeModule */; }; + 52D2F4420D90A1093759EA6A /* FeatureHashSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42FD9D07290973345A01F20 /* FeatureHashSet.swift */; }; 53170A331CFF41FC3C904294 /* ScheduleABITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E08CBC9D53E1482257F8512B /* ScheduleABITests.swift */; }; 534C8AE7D3768059DDAEA18D /* Benchmark in Frameworks */ = {isa = PBXBuildFile; productRef = FD911ACF527827DA331405BD /* Benchmark */; }; 53983E0590B008B0818F58EF /* CorpusPersistenceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3A65FF014BAD23D72C0772 /* CorpusPersistenceClient.swift */; }; @@ -120,9 +140,9 @@ 61031F0CE3C1B19C1D04DF89 /* PropertyTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 61385ABD9D088DA7B1BCEB13 /* BoolMutators.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82662CE734CB5CCF8C0C782 /* BoolMutators.swift */; }; 61437804FBED0EADE0671F2B /* CoverageEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD1EC805CD71A270C692864 /* CoverageEngine.swift */; }; - 619E8CA36EC2421D248ADCD8 /* FeatureOwnershipLedger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2E59331674D16FC32BD5A7 /* FeatureOwnershipLedger.swift */; }; 61CE51368B8A8DB9F85766E9 /* StopWhenQueueEmptyPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E37B0F71C6AF3FAD60F074F7 /* StopWhenQueueEmptyPluginTests.swift */; }; 6278A355CE18D7FB1ED46FA9 /* AlwaysInterestingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB988F36432EEA023A812BEA /* AlwaysInterestingStrategy.swift */; }; + 632D2571661008F7786F766C /* FeatureHashSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB34AC6D91EE9581E4786649 /* FeatureHashSetTests.swift */; }; 66DCF6A598215F7B30A9ADA8 /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = DDEB96549CC1CCF35FD3E1FA /* Atomics */; }; 673C3E6E506B60678B4A7A01 /* WeightedPoolCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F000A4108F2BF3EC22200A76 /* WeightedPoolCore.swift */; }; 699B38A3B7C7104055FCE349 /* ArraySequenceInsertionMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0C816E3E7C04A2F173CCAF /* ArraySequenceInsertionMutator.swift */; }; @@ -142,6 +162,7 @@ 743180BC62940DDFB7C85342 /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = F68FBBC165DE4EA066FAFF01 /* Dependencies */; }; 743BBA7F6BDECF794DE6C5F2 /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; 7566C0F6C73FE1CF8765DDD3 /* ExecutorAffinityTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB79C5014695DD269F6E198 /* ExecutorAffinityTest.swift */; }; + 76399ADB3031E94C7A251B5E /* OwnershipEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43563689823267191ED952F8 /* OwnershipEvaluatorTests.swift */; }; 77318276C93EAB250B715AAD /* SaturationPlateauDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37ED10677A92DC01DD0B289 /* SaturationPlateauDetectorTests.swift */; }; 7860427F4252E7CF955018E1 /* SaturationPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543E53F7A2745CDD7F2C03DE /* SaturationPluginTests.swift */; }; 78DDF0EE8B70C78DBA2D42A2 /* SingleValueMutatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D1B5791E9F90FD89BBC36EF /* SingleValueMutatorTests.swift */; }; @@ -150,11 +171,12 @@ 7BF60D6E4F9D9DFA72418D01 /* FuzzStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC43CD4AEE4429660B4142AE /* FuzzStateMachine.swift */; }; 7C8059D0A66CF4E5422B2961 /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; 7CFBBE498A187CF8D55FF15B /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = 5A16A65CE2487BAC3C6BD67A /* Dependencies */; }; - 808DEDCEF3F72F26E4C97724 /* CorpusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E53225F99BA35278DB06DA6 /* CorpusTests.swift */; }; 8110224B7D927B57E8FEDE44 /* Data+Shrinkable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B934807ECBCAA45B83D128 /* Data+Shrinkable.swift */; }; 814309179FD818830027854B /* SanCovHooks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 82AC4438E748A02C9943F011 /* OwnershipLedgerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E37C2066A77A6FBA04095C0 /* OwnershipLedgerTests.swift */; }; 8310E72CA875CD48836F2A44 /* ScheduleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248B03EF2ED5C071ABDB9FA2 /* ScheduleController.swift */; }; 83FA5E00DDE707225B67BBB1 /* InputSizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BB4785C4BF6BA72ABDB89F /* InputSizeTests.swift */; }; + 851ABE7279BA190EF123458F /* UncheckedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9430F665548303A424B1675C /* UncheckedBox.swift */; }; 85831BC8A71C93AF8B6270D1 /* EntropicPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F66CDF058D72AAB14F4EA5 /* EntropicPolicyTests.swift */; }; 8583A6B9CC8AE06E1F34F8C4 /* CoverageBenchmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF833B020283C4892D55D53C /* CoverageBenchmarks.swift */; }; 87380133655A8CEFAE35026C /* DateClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E547A4A91BCC71FBDD10CA9 /* DateClient.swift */; }; @@ -162,10 +184,12 @@ 88B2BC9C932B354AFB8F3358 /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = 0E893A5DFC166302CEB2F46D /* Clocks */; }; 89F380D42467C6620342FBE9 /* FuzzPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85846CC9E6C6819503039FD5 /* FuzzPlugin.swift */; }; 8ACD8EE65590E12676E039BF /* DoubleBoundaryMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E13D393F8FDEFB8D8A5087D /* DoubleBoundaryMutator.swift */; }; + 8BAD61A1E97D6E59373463CE /* EdgeUnionBitmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1097CECA8C84FFD06534FF /* EdgeUnionBitmap.swift */; }; 8BD6A306A5F55973C4C54AEB /* EdgeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95BCE905C5A7F433C213114 /* EdgeObserver.swift */; }; 8CD349B9F240C63390CA1782 /* XSSMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71654D1483924FFD0DD97F8 /* XSSMutator.swift */; }; 8CF3A47866F2115E90BB94D3 /* FuzzCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 69A1455BBDC81EED5F1D2C67 /* FuzzCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8D4A983DD7DF4F96D9676B31 /* PathTrieStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCEAA419004D9808AB03E0 /* PathTrieStrategy.swift */; }; + 8E1B2283A6A7E4FB0E7BDDB2 /* FuzzInputToStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7142A4F7332556BB6AEBF60E /* FuzzInputToStateTests.swift */; }; 8EEA1B717BCD397550E158DE /* String+Shrinkable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B31238BF5C1F4F477E6940 /* String+Shrinkable.swift */; }; 8FDD8210ED2B765130F5F9E0 /* SparseCoverage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5669C63A62C2D9172D949DCE /* SparseCoverage.swift */; }; 902AD170388F6A40C15ECCA5 /* MutatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AB425C1886E9C43DA056F /* MutatorTests.swift */; }; @@ -175,10 +199,13 @@ 930C4CD6291AF3BB931A9FB3 /* GenericTimerPoller.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AAFECCE3AA98E503089E0B7 /* GenericTimerPoller.framework */; }; 938AEE5F4F543864E107511E /* RoutingBranchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2C248AA992042CBD7C555D /* RoutingBranchTests.swift */; }; 94597A6D6154EF4888C34AB1 /* FeatureOwnershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69E2CAD9A2DA23DFBCC5890 /* FeatureOwnershipTests.swift */; }; + 948D9261F5B4172712CF233F /* LockMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3353F474A52E096EE2840EBF /* LockMetricsTests.swift */; }; 958ADDE946E9CD95EC9CB590 /* StopOnFirstFailurePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9A2D6D2D787FF8BD1869F6F /* StopOnFirstFailurePluginTests.swift */; }; + 959229692ED895D7BFB923F7 /* DeterministicRNG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DA5E50B4D907BB1DBB7C99 /* DeterministicRNG.swift */; }; 95A92958FD086AD9481BA7F5 /* GenericTimerPoller.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5AAFECCE3AA98E503089E0B7 /* GenericTimerPoller.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9617C4044A96AF4D9B014040 /* IdentityMutantRateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EDEC0EE17BFE2A1E440227B /* IdentityMutantRateTests.swift */; }; 965AC1F59968645673F07841 /* corpus.json in Resources */ = {isa = PBXBuildFile; fileRef = 87C13394409DA48E4BE31930 /* corpus.json */; }; + 965E6B6DD5F652316B6B3AAC /* LockMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0428FEDF41A0A2D02435B30C /* LockMetrics.swift */; }; 9900DE76B0490D232B206DAD /* IssueDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06258AF546FB539D68605D /* IssueDetection.swift */; }; 997046243B39595955A73A07 /* CoverageGap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E710A18D4C3A68A36CF37040 /* CoverageGap.swift */; }; 99BAD167860B31B48CFBB699 /* IssueReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 27C67ABB6F1BBC4F43D83270 /* IssueReporting */; }; @@ -187,18 +214,17 @@ 9C5BBD13203D08D64D7A3B4D /* WeightedPoolHarness.swift in Sources */ = {isa = PBXBuildFile; fileRef = C50D87CF5D9935363B56D0A5 /* WeightedPoolHarness.swift */; }; 9DADB5A1F40BF13558A2BD55 /* Synchronized.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1E91685C6019AA1D8E23F9 /* Synchronized.swift */; }; 9E9C7B77CE1B7C4A4243536A /* StrategyFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C14F22721E00A7EC03198B /* StrategyFeatureTests.swift */; }; - 9EC07D8B33B17309C71B083B /* CorpusEntryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A93E0FF8EB11E1EC6615D0E /* CorpusEntryType.swift */; }; 9F1D0263653AF466AEF4DBA8 /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; - A0E40AFCF79C910B2DDB2F4B /* FailureInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030E3D95F451EC885BDF8E15 /* FailureInfo.swift */; }; A1B5C9D7B4345854FF5D488A /* PropertyTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; }; + A298ED17A8111ACF0710632F /* HitCountAccumulatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01504AA2CCB3DB6DA6C1B75A /* HitCountAccumulatorTests.swift */; }; A69B02004FE6EF9488B61B79 /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = 589731B18E21C616101A2A8C /* Clocks */; }; AB89E6673C3749E9B3F3A882 /* PlateauDetectorPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA01B2725BCFE68C918C2336 /* PlateauDetectorPluginTests.swift */; }; ADF49AE0B09D08080E515387 /* CoverageCountersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33B33085A4DB5D1981F0A /* CoverageCountersTests.swift */; }; AE3F241318A2A099029B58C7 /* ShrinkStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED66F0C3A23B5251B88672FB /* ShrinkStats.swift */; }; - AEEEBDAF7BCDBAA9B883B8A2 /* CorpusClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBBB30B9BF4F7490C786AE1D /* CorpusClient.swift */; }; AF88F549B8000717EBBAEDD3 /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = 9BB3753751B18FA893D80D7F /* Clocks */; }; AF9983B417E8F3A9D875EB4A /* SanCovHooks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; AFDDC40C6C111A0C8359403D /* FunctionSizeLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D675F3742488937DF00D923F /* FunctionSizeLookup.swift */; }; + B0B8A35796562DC499238150 /* AtomicFeatureSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F027FD9A95976E20AF15DB68 /* AtomicFeatureSetTests.swift */; }; B121724BFD823073AD820165 /* SpecialDoubleMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C90F2F7858E4FED8F0DF17 /* SpecialDoubleMutator.swift */; }; B17D5E735A382D9331EA8FEC /* SanCovHooks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B269AC60201884DB429C947C /* STADSPlateauDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 146B4C7BE9FB4A557084104F /* STADSPlateauDetectorTests.swift */; }; @@ -210,11 +236,13 @@ B635E42550C7EB8E11F78C4F /* SimpleRingBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161C7506D512FF333F2B5B5D /* SimpleRingBuffer.swift */; }; B7C8F6A30683A8502E6CE1C0 /* FunctionSpy in Frameworks */ = {isa = PBXBuildFile; productRef = F0BEB91D5B04B84629BA665A /* FunctionSpy */; }; B9388FEEBE26B8B839F43200 /* CoverageProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2877E9AFD268BEF06837122D /* CoverageProbe.swift */; }; + B990E38E9BE57627D1FD7A75 /* AdaptiveDepthMathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92E4026EC9EA5AC5B792C86E /* AdaptiveDepthMathTests.swift */; }; BA302D143C2AD3179A694BB4 /* Mutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D874BB685758F72CF6997938 /* Mutator.swift */; }; BB12D7626EF30141E6A69CE5 /* GenericTimerPollerFuzzTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F1397203B8C83BB3068B5A /* GenericTimerPollerFuzzTests.swift */; }; BB317568B96C33EA9BBBDC4F /* PropertyTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BEBBA20C60FAF1E749C4427D /* SanCovCounters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A865DFA08A7E0DE3F588EDB /* SanCovCounters.swift */; }; BF123F4F34E5D11401D26E11 /* ScheduleControl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FA1A34B8FC6F4EFE3022741B /* ScheduleControl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BFEB5CAA333D2AE13CB39B7B /* BoundaryDistanceLedgerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507D98899A90C12DB930A5F9 /* BoundaryDistanceLedgerTests.swift */; }; C0636E49780CF2A22E43DF61 /* UInt+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD0134BA5F87EB0A07CFA74 /* UInt+MutatorProviding.swift */; }; C0E5C0ED4094D06754BC00C3 /* EnergyMutationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7338026EE0E559A10E5ECC55 /* EnergyMutationTests.swift */; }; C109C8B055AFA44A7D3EB58D /* FileManagerClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AF15F08031238EDC5128DE /* FileManagerClient.swift */; }; @@ -238,12 +266,17 @@ D0C65F0813EFB9C22E7A24EC /* DWARFSymbolizerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2671C4A43D9243DDBC246A9 /* DWARFSymbolizerError.swift */; }; D12971CA15BE5320F44779DD /* GenericTimerPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8B6028F2EEA16611FDAD75 /* GenericTimerPoller.swift */; }; D1E4497F35A6A820B6BD0AA0 /* StringMutators.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52C3379771BA6E8B3D1C9EE /* StringMutators.swift */; }; + D246C8D105C8E09BDD92AD97 /* AdaptiveDepthPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7CB1D8B231D746FBE08DBC5 /* AdaptiveDepthPolicy.swift */; }; D33C542AC0A54F1C506CC6EF /* SQLInjectionMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A14C05484DADACA064A68C /* SQLInjectionMutator.swift */; }; + D50589D8527B6FEB6970623C /* AdaptiveDepthChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228A4808A96301C32C0855E2 /* AdaptiveDepthChainTests.swift */; }; D5304C43DD6EFBF89321404D /* PropertyTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; }; D5645DFA85C2ABBD0E34ACC2 /* ParallelTimingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A80CC19DCB4C4527B0777F /* ParallelTimingTest.swift */; }; + D56F1486FD92906F0DC97ADB /* SanCovCmpRecorderGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B9F89824089B30381887B6 /* SanCovCmpRecorderGateTests.swift */; }; D5E3DE5AAC935A598737FA84 /* FuzzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69A1455BBDC81EED5F1D2C67 /* FuzzCore.framework */; }; D84467E801A17C3B2575C15E /* Optional+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131A74DECB959CB9B14F1CE8 /* Optional+MutatorProviding.swift */; }; + D9062F141056F0F28EB71027 /* BoundaryDistanceStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33DF5C7CAC0D8E89CF4B43CB /* BoundaryDistanceStrategy.swift */; }; D9123B10E97A518FE1CF89BA /* Double+MutatorProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93BFA04C1570386797D50F30 /* Double+MutatorProviding.swift */; }; + D9D29FF2CF7BBEBB6C27EEF9 /* OwnershipEvaluators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273F365B8AE959073FF1C97 /* OwnershipEvaluators.swift */; }; DA06181B96501EDCC678BC3C /* SanCovHooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */; }; DA43DF2C782818DADB74D492 /* PCResolutionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BACD85D7C5B37A9C6BE9ED5 /* PCResolutionTest.swift */; }; DB3B9AFA08DCD50172E473CB /* CLLVMSymbolizer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6B4DC0FA0EFB0DDF25E4C353 /* CLLVMSymbolizer.cpp */; }; @@ -253,13 +286,13 @@ E1080B97E3B3224DE99B66E2 /* ScheduleDeterminismTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A0E69A9C4F9C8395F2A307 /* ScheduleDeterminismTest.swift */; }; E145E20630402E21F4FCCC0D /* ScheduleControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA1A34B8FC6F4EFE3022741B /* ScheduleControl.framework */; }; E22FD5BA11720007BFEC6E1E /* DWARFSourceLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801978DFD141E3190DC8219D /* DWARFSourceLocation.swift */; }; + E273DC1A5CAAB210E1A462BB /* IntInputToStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53693EB8DEF30AC22B2DCA8C /* IntInputToStateTests.swift */; }; E2D5097DE99044658C7AE9D1 /* PropertyTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; }; E3608F2CD24DEE3D15C5206C /* HTTPStatusCodeMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F4B213B50E72D273ABFA7A /* HTTPStatusCodeMutator.swift */; }; E48CBF1039022CA69BE090EE /* InstrumentationProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2125BCA3F8818FAFF6B0AE72 /* InstrumentationProbe.swift */; }; E546F7532EEF4E099063ED08 /* HitCountBucketsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD58350A367890040C1786A /* HitCountBucketsStrategyTests.swift */; }; E5CE85300E9595AE8CF1F398 /* UncoveredRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DA6786D89438D0199BF0412 /* UncoveredRegion.swift */; }; E71268BC0D1B3918F640EA29 /* SyncBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD9322AFE91D369F2DE946B /* SyncBox.swift */; }; - E7399441F6F7D6EEB785E4CA /* SanCovEdgeFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF80A96A17AD018D2CDD24A2 /* SanCovEdgeFilterTests.swift */; }; E7500FB62C682AC6EC216A82 /* ProfiledBenchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EFC8FC26036551EE41795E /* ProfiledBenchmark.swift */; }; E8ED514CBE637B3DB6879755 /* IssueDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE561F78BDA61BB34264F21 /* IssueDetectionTests.swift */; }; EA21AD66F0B620EA821A3D26 /* FastRNG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B693D0FDD90EB6A2FC7D5FD /* FastRNG.swift */; }; @@ -268,10 +301,12 @@ EBEE3E7D3979455A86AF45AD /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = 75307C61A778CA9F0809F76C /* Clocks */; }; ECD555A23EB1EF2A46A300BA /* TestCaseShrinker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A31AE640E58BD0580C569D /* TestCaseShrinker.swift */; }; ECFC2E14065A547519435451 /* FuzzEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B14BDAA52A6B67F749FA93 /* FuzzEngine.swift */; }; + ED51ED948EFC7AC8688DE5CE /* AdaptiveDepthMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FE2C6701E82D1E50C4BDAC /* AdaptiveDepthMath.swift */; }; EE31AE34D338AEF804DF8E8A /* PropertyTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F052FC5AD62C0559D4631284 /* PathTrie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0949B41FDB1F7399BF85852B /* PathTrie.swift */; }; F175208C6823ED3BC2A92224 /* PropertyTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; }; F63BA597B8068668511D3B8E /* ContextRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 257B1E9613373B2BF2E9934E /* ContextRecorderTests.swift */; }; + F6788A3D2EECC01143DBEFCC /* ComparisonObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FEA1D41310218E4667B780 /* ComparisonObserver.swift */; }; F721D9CEB032EC80C6F3DCF3 /* PropertyTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F72C3192C96211FD0E5D9C53 /* SchedulerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F00C937566BBAE28DC9DE16 /* SchedulerCore.swift */; }; F7720F80A165FC2F9985DC0A /* EmptyStringMutator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87F4E945BE97E033C1A2F94 /* EmptyStringMutator.swift */; }; @@ -280,9 +315,12 @@ F94ACDF880FA9181386E27B1 /* FuzzResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2260DD34687D47AC405111 /* FuzzResult.swift */; }; F9E2D4EA13931A5F9FFC4736 /* CorpusCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB81D025D3C307D01FD829DB /* CorpusCoordinatorTests.swift */; }; FADFA1D18BC390100662C0DF /* WeightedPoolCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB742600C6E1AC2CE85EC9C4 /* WeightedPoolCoreTests.swift */; }; + FD11BA57089157160EDB94AE /* ComparisonDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DC065206A7731002138A4A /* ComparisonDictionaryTests.swift */; }; FD94950386D6A9EEEBBB7756 /* GenericTimerPoller.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AAFECCE3AA98E503089E0B7 /* GenericTimerPoller.framework */; }; FDF49F09CE598EC505A90016 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = FA9D976ECC697F605BF51ABC /* DequeModule */; }; + FEDC78AFD377365871D400C6 /* ComparisonDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F148196F00001BDE574A8094 /* ComparisonDictionary.swift */; }; FFB6F58A91B4C9BF1E3CF6FE /* IssueReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 368BE9D34AEC6FE970414771 /* IssueReporting */; }; + FFE1FE2A95AFF9F70DD9310A /* CoverageStrategyComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F1E88DA6B9D475BD918E5B /* CoverageStrategyComposition.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -612,8 +650,11 @@ 00C7E57E4C8DFE72AE119BBD /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; 00D9A28F92E90B2E82D5DC8C /* ck_cc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_cc.h; sourceTree = ""; }; 00EBA13944AF0B757005638A /* ConcurrentFuzzLoadTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentFuzzLoadTest.swift; sourceTree = ""; }; + 01504AA2CCB3DB6DA6C1B75A /* HitCountAccumulatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitCountAccumulatorTests.swift; sourceTree = ""; }; 021ACDF8BE3B266FA44EEDBD /* CoverageBenchmarks */ = {isa = PBXFileReference; includeInIndex = 0; path = CoverageBenchmarks; sourceTree = BUILT_PRODUCTS_DIR; }; - 030E3D95F451EC885BDF8E15 /* FailureInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailureInfo.swift; sourceTree = ""; }; + 035DD8EB93B39B3A786B2B45 /* ComparisonCoverageStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonCoverageStrategyTests.swift; sourceTree = ""; }; + 0428FEDF41A0A2D02435B30C /* LockMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockMetrics.swift; sourceTree = ""; }; + 049F353FB2914B702681DEEC /* CmpOnlySchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmpOnlySchedulerTests.swift; sourceTree = ""; }; 06BA035A58BDC3A577E01065 /* CrossSessionContaminationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossSessionContaminationTest.swift; sourceTree = ""; }; 06ED1D87CAF04357C6E3DFE9 /* ScheduleHooks.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = ScheduleHooks.c; sourceTree = ""; }; 080B558DCF0B7BC8051813FB /* ScratchPad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScratchPad.swift; sourceTree = ""; }; @@ -624,9 +665,11 @@ 0BC4138150CDC1ABC2DE7C65 /* FuzzEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzEngineTests.swift; sourceTree = ""; }; 0BF3BAC10EF47D6C57388C35 /* SanCovHooks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SanCovHooks.h; sourceTree = ""; }; 0D8CCA1129D052D6BF52BCC1 /* HitCountBucketsStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitCountBucketsStrategy.swift; sourceTree = ""; }; + 0E37C2066A77A6FBA04095C0 /* OwnershipLedgerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnershipLedgerTests.swift; sourceTree = ""; }; 0E6744BAE7780BE09993D850 /* NewEdgeStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewEdgeStrategy.swift; sourceTree = ""; }; 0F9D24C887A6D36D7A6EFDB8 /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; 116EA3D65CF2576CA4164C85 /* GenericTimerPollerPropertyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericTimerPollerPropertyTests.swift; sourceTree = ""; }; + 123C6DAB5ECCBBEC26AB3C89 /* AdaptiveDepthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveDepthPolicyTests.swift; sourceTree = ""; }; 12B934807ECBCAA45B83D128 /* Data+Shrinkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Shrinkable.swift"; sourceTree = ""; }; 131A74DECB959CB9B14F1CE8 /* Optional+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+MutatorProviding.swift"; sourceTree = ""; }; 137A6EE309F1056E3F217831 /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; @@ -637,16 +680,20 @@ 1B7FA0AAC10E7E2DB5FD6CDE /* TSanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TSanTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1B9764162F7545DC89277868 /* ScheduleControlTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScheduleControlTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1BBCB1776E53F47AD0D68618 /* URLMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLMutator.swift; sourceTree = ""; }; + 1F1097CECA8C84FFD06534FF /* EdgeUnionBitmap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeUnionBitmap.swift; sourceTree = ""; }; 204BDF70E7D5F50251C7A775 /* String+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+MutatorProviding.swift"; sourceTree = ""; }; 207ADDE793917BD26C4770EB /* CLLVMSymbolizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CLLVMSymbolizer.h; sourceTree = ""; }; 2125BCA3F8818FAFF6B0AE72 /* InstrumentationProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstrumentationProbe.swift; sourceTree = ""; }; + 228A4808A96301C32C0855E2 /* AdaptiveDepthChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveDepthChainTests.swift; sourceTree = ""; }; 248B03EF2ED5C071ABDB9FA2 /* ScheduleController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleController.swift; sourceTree = ""; }; 24A467224B8821AF297298A6 /* EdgeHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeHooks.swift; sourceTree = ""; }; 24F66CDF058D72AAB14F4EA5 /* EntropicPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropicPolicyTests.swift; sourceTree = ""; }; 257B1E9613373B2BF2E9934E /* ContextRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextRecorderTests.swift; sourceTree = ""; }; 2595E9AD80DBFC77F241A7E7 /* PropertyTestingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PropertyTestingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 25D1A7AC964578057CD75715 /* ScheduleChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleChoiceTests.swift; sourceTree = ""; }; + 26FEA1D41310218E4667B780 /* ComparisonObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonObserver.swift; sourceTree = ""; }; 26FEE62F30A17C11F4DCFB4B /* ArrayMutators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayMutators.swift; sourceTree = ""; }; + 2704E8BD88F40CF9BF414641 /* ComparisonCoverageStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonCoverageStrategy.swift; sourceTree = ""; }; 2877E9AFD268BEF06837122D /* CoverageProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageProbe.swift; sourceTree = ""; }; 28AF15F08031238EDC5128DE /* FileManagerClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerClient.swift; sourceTree = ""; }; 28BE722825C9C30C1B1DE68D /* ArrayPositionAwareMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayPositionAwareMutator.swift; sourceTree = ""; }; @@ -659,8 +706,11 @@ 2E13D393F8FDEFB8D8A5087D /* DoubleBoundaryMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleBoundaryMutator.swift; sourceTree = ""; }; 2F00C937566BBAE28DC9DE16 /* SchedulerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerCore.swift; sourceTree = ""; }; 2F5D17F7EF3F37D6C60A6EBF /* ck_stdbool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_stdbool.h; sourceTree = ""; }; + 3353F474A52E096EE2840EBF /* LockMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockMetricsTests.swift; sourceTree = ""; }; + 33DF5C7CAC0D8E89CF4B43CB /* BoundaryDistanceStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundaryDistanceStrategy.swift; sourceTree = ""; }; 34658F2420967EA35E38058D /* SanCovIsolationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SanCovIsolationTests.swift; sourceTree = ""; }; 3593C7A78C4DB15293ED6F47 /* CoverageDeterminismTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageDeterminismTest.swift; sourceTree = ""; }; + 39FE2C6701E82D1E50C4BDAC /* AdaptiveDepthMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveDepthMath.swift; sourceTree = ""; }; 3B54D66375BF29A19B143AE3 /* CorpusPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusPersistence.swift; sourceTree = ""; }; 3B56C4B9B3773FB6734D0821 /* SimpleCoveragePlateauDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCoveragePlateauDetectorTests.swift; sourceTree = ""; }; 3B6534DD92D12382387899CD /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; @@ -674,8 +724,10 @@ 3F1917814603DE56511E5F24 /* DWARFSymbolizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DWARFSymbolizer.swift; sourceTree = ""; }; 3F2C248AA992042CBD7C555D /* RoutingBranchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingBranchTests.swift; sourceTree = ""; }; 41D46FE2CA83F10C2DE9B2F1 /* ck_ht.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_ht.h; sourceTree = ""; }; + 43563689823267191ED952F8 /* OwnershipEvaluatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnershipEvaluatorTests.swift; sourceTree = ""; }; 43B310BD1F88DB6894FF1F29 /* SignatureMatchStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignatureMatchStrategy.swift; sourceTree = ""; }; 447218A4047DEE5BBB9EFF09 /* Shrinkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shrinkable.swift; sourceTree = ""; }; + 46DC065206A7731002138A4A /* ComparisonDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonDictionaryTests.swift; sourceTree = ""; }; 48E05741C671DFC85D8A63A2 /* MutationScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutationScheduler.swift; sourceTree = ""; }; 4AB79C5014695DD269F6E198 /* ExecutorAffinityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutorAffinityTest.swift; sourceTree = ""; }; 4C06258AF546FB539D68605D /* IssueDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueDetection.swift; sourceTree = ""; }; @@ -683,9 +735,11 @@ 4CD58350A367890040C1786A /* HitCountBucketsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitCountBucketsStrategyTests.swift; sourceTree = ""; }; 4DD0134BA5F87EB0A07CFA74 /* UInt+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt+MutatorProviding.swift"; sourceTree = ""; }; 4F0FBDC1A9D5494CB98ECCCE /* PoolCapacityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolCapacityTests.swift; sourceTree = ""; }; + 507D98899A90C12DB930A5F9 /* BoundaryDistanceLedgerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundaryDistanceLedgerTests.swift; sourceTree = ""; }; 50E5DD3B8575BC75880E15FF /* ContinuousClockClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinuousClockClient.swift; sourceTree = ""; }; 51F60E9EF9B6498AC00EEBFE /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; 5357FB37687D2D3BDA8DABED /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; + 53693EB8DEF30AC22B2DCA8C /* IntInputToStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntInputToStateTests.swift; sourceTree = ""; }; 543E53F7A2745CDD7F2C03DE /* SaturationPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaturationPluginTests.swift; sourceTree = ""; }; 561F3B6FDFA4F004000F8E46 /* ArrayRepeatedValuesMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayRepeatedValuesMutator.swift; sourceTree = ""; }; 5669C63A62C2D9172D949DCE /* SparseCoverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparseCoverage.swift; sourceTree = ""; }; @@ -703,7 +757,9 @@ 5E849F3495589275569E76FC /* SanCovSourceLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SanCovSourceLocation.swift; sourceTree = ""; }; 5E8F9F6F98682A286CF9856A /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; 5EB948CF139436D3D40949EF /* EnvironmentClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentClient.swift; sourceTree = ""; }; + 5F7019EDAF76A64238D3D748 /* BoundarySiteAccumulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundarySiteAccumulator.swift; sourceTree = ""; }; 5F8B6028F2EEA16611FDAD75 /* GenericTimerPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericTimerPoller.swift; sourceTree = ""; }; + 5FAEEDF5D30CDE9997EDCEAE /* AtomicFeatureSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicFeatureSet.swift; sourceTree = ""; }; 601C8A74E0D1285E70084C34 /* DrainConcurrencyTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrainConcurrencyTest.swift; sourceTree = ""; }; 605B74ECA01B79EBE5253DE8 /* GenericTimerPollerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GenericTimerPollerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 6089581A8525B59F997740DA /* ck_pr_lse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr_lse.h; sourceTree = ""; }; @@ -711,18 +767,20 @@ 62F1397203B8C83BB3068B5A /* GenericTimerPollerFuzzTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericTimerPollerFuzzTests.swift; sourceTree = ""; }; 62F4B213B50E72D273ABFA7A /* HTTPStatusCodeMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCodeMutator.swift; sourceTree = ""; }; 63C99FD379289FA24BBE7A5B /* ParallelEarlyCancelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallelEarlyCancelTest.swift; sourceTree = ""; }; + 65874BE183B686F124793FB4 /* HitCountAccumulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitCountAccumulator.swift; sourceTree = ""; }; 662FDA7546F253A8CAE418AB /* SanCovHooks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SanCovHooks.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 672D5DCDC1A16C1291F8044B /* CoverageStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageStrategy.swift; sourceTree = ""; }; 676196E34E9EA63139657323 /* ck_stdint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_stdint.h; sourceTree = ""; }; 683A330BCB90F626B21D2422 /* CustomFuzzableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFuzzableTests.swift; sourceTree = ""; }; 69A1455BBDC81EED5F1D2C67 /* FuzzCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FuzzCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6A93E0FF8EB11E1EC6615D0E /* CorpusEntryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusEntryType.swift; sourceTree = ""; }; 6AD3FFE8C65FFC7DFDAABC32 /* ABAInheritanceHandleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABAInheritanceHandleTests.swift; sourceTree = ""; }; 6ADE3EEEF0705F1A7D88FD97 /* ck_stddef.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_stddef.h; sourceTree = ""; }; 6B4DC0FA0EFB0DDF25E4C353 /* CLLVMSymbolizer.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CLLVMSymbolizer.cpp; sourceTree = ""; }; + 6B76959C25CA4FE7BE3B00D0 /* AdaptiveDepthInsertedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveDepthInsertedTests.swift; sourceTree = ""; }; 6B7DA3E55A8132FE210D8EAF /* CoverageCountersClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageCountersClient.swift; sourceTree = ""; }; 6C6089DAA244C9F5FEF045C2 /* ck_ht_hash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_ht_hash.h; sourceTree = ""; }; 6E542601AA461AC5A1E637A8 /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; + 7142A4F7332556BB6AEBF60E /* FuzzInputToStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzInputToStateTests.swift; sourceTree = ""; }; 71AD2B2875AE058F4041293D /* ck_internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_internal.h; sourceTree = ""; }; 721AFD25B771D2E4B2523FD3 /* libCScheduleHooks.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCScheduleHooks.a; sourceTree = BUILT_PRODUCTS_DIR; }; 72A34E694D13EF8256D00F42 /* FlattenedScheduleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlattenedScheduleTests.swift; sourceTree = ""; }; @@ -730,8 +788,10 @@ 7338026EE0E559A10E5ECC55 /* EnergyMutationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnergyMutationTests.swift; sourceTree = ""; }; 74A14C05484DADACA064A68C /* SQLInjectionMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLInjectionMutator.swift; sourceTree = ""; }; 7592E82B282E9A2AEA8D1386 /* StragglerCoverageInheritanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StragglerCoverageInheritanceTests.swift; sourceTree = ""; }; + 75F1E88DA6B9D475BD918E5B /* CoverageStrategyComposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageStrategyComposition.swift; sourceTree = ""; }; 770BE6BB265C7DAB60FB119F /* PoollessSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoollessSchedulerTests.swift; sourceTree = ""; }; 779153C9B2EE2604BB0510F4 /* GenericTimerPollerReproductionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericTimerPollerReproductionTest.swift; sourceTree = ""; }; + 78C7626BD5E1269597F45D31 /* CoverageStrategyCompositionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageStrategyCompositionTests.swift; sourceTree = ""; }; 78CCD0EE426F8A208203ED16 /* CoverageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageView.swift; sourceTree = ""; }; 797B369A783CF8DA4F7C9190 /* RaceConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RaceConditionTests.swift; sourceTree = ""; }; 7A3A65FF014BAD23D72C0772 /* CorpusPersistenceClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusPersistenceClient.swift; sourceTree = ""; }; @@ -743,6 +803,7 @@ 805D70C5E70888046E92D052 /* SanCovTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SanCovTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 813527F798EBF4073FCCB7C9 /* NegativeIntMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NegativeIntMutator.swift; sourceTree = ""; }; 81A0E69A9C4F9C8395F2A307 /* ScheduleDeterminismTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDeterminismTest.swift; sourceTree = ""; }; + 8273F365B8AE959073FF1C97 /* OwnershipEvaluators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnershipEvaluators.swift; sourceTree = ""; }; 82C90F2F7858E4FED8F0DF17 /* SpecialDoubleMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialDoubleMutator.swift; sourceTree = ""; }; 82E302CA79A5FD97C9E37328 /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; 83DBAA9011444A01A6BC8F7E /* ArrayDuplicationMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayDuplicationMutator.swift; sourceTree = ""; }; @@ -752,12 +813,15 @@ 8955074B94D7B6D470F922F2 /* FuzzStatsAccountingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzStatsAccountingTests.swift; sourceTree = ""; }; 89B1AFF0FF50A24C9CF91760 /* ActorDeinitSchedulingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorDeinitSchedulingTest.swift; sourceTree = ""; }; 8A0CC115EE184F1E93C3A0E0 /* ck_string.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_string.h; sourceTree = ""; }; + 8AECCD66326E8AEA9BEF03D3 /* ComparisonObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonObserverTests.swift; sourceTree = ""; }; 8B2B7BE0BAA86B5197752AE1 /* ScheduleByteMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleByteMutator.swift; sourceTree = ""; }; 8D5B1DD3570EBB6E7D12F912 /* FuzzableProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzableProtocolTests.swift; sourceTree = ""; }; 9038D6C2FF93F7F004830619 /* ShrinkingPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShrinkingPluginTests.swift; sourceTree = ""; }; 909450826B23F3420A39E22F /* CartesianProductTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartesianProductTests.swift; sourceTree = ""; }; 9253FA68CDDBAF11AB22959F /* FuzzPluginHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzPluginHandler.swift; sourceTree = ""; }; + 92E4026EC9EA5AC5B792C86E /* AdaptiveDepthMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveDepthMathTests.swift; sourceTree = ""; }; 93BFA04C1570386797D50F30 /* Double+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+MutatorProviding.swift"; sourceTree = ""; }; + 9430F665548303A424B1675C /* UncheckedBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UncheckedBox.swift; sourceTree = ""; }; 9475EBCF152B8D2EEACB5111 /* STADSPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STADSPluginTests.swift; sourceTree = ""; }; 94B31238BF5C1F4F477E6940 /* String+Shrinkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Shrinkable.swift"; sourceTree = ""; }; 94EB367A0BBDEA977C219F3A /* SimpleCoveragePlateauDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCoveragePlateauDetector.swift; sourceTree = ""; }; @@ -765,14 +829,14 @@ 951E33D0078C4A59FF897AD4 /* Int+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+MutatorProviding.swift"; sourceTree = ""; }; 973A7270D302724EDD226172 /* CartesianProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartesianProduct.swift; sourceTree = ""; }; 9791710C8985E9069A0AAEA9 /* CoverageEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageEngineTests.swift; sourceTree = ""; }; + 98DA5E50B4D907BB1DBB7C99 /* DeterministicRNG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterministicRNG.swift; sourceTree = ""; }; 99DF2D2D7A9C78BEFDA1C9FF /* FuzzAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzAPITests.swift; sourceTree = ""; }; 9A0C816E3E7C04A2F173CCAF /* ArraySequenceInsertionMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArraySequenceInsertionMutator.swift; sourceTree = ""; }; 9A3E1092D79DAAC73C0C082C /* PortMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortMutator.swift; sourceTree = ""; }; 9B9F69D3B0B4EB52DD10C3C5 /* Character+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Character+MutatorProviding.swift"; sourceTree = ""; }; 9DA6786D89438D0199BF0412 /* UncoveredRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UncoveredRegion.swift; sourceTree = ""; }; 9DFF83D067AB91A079C87B7C /* Array+Shrinkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Shrinkable.swift"; sourceTree = ""; }; - 9E53225F99BA35278DB06DA6 /* CorpusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusTests.swift; sourceTree = ""; }; - 9F2E59331674D16FC32BD5A7 /* FeatureOwnershipLedger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureOwnershipLedger.swift; sourceTree = ""; }; + 9E8AA636EFB6AED289971643 /* EdgeUnionBitmapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeUnionBitmapTests.swift; sourceTree = ""; }; A0AD3E7E3F5BF9950E016EEE /* DependencyLiveValueIsolationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyLiveValueIsolationTests.swift; sourceTree = ""; }; A179A4CAD0B9C0FC0DF76A85 /* DWARFSymbolizerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DWARFSymbolizerHelper.swift; sourceTree = ""; }; A18400D950AE2D1D13443E9A /* FuzzAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzAPI.swift; sourceTree = ""; }; @@ -780,6 +844,7 @@ A3890AE7461FB58FC0FA5FAC /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; A5A7DD272E538B8E8CFE5C75 /* CoverageGapDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageGapDetectorTests.swift; sourceTree = ""; }; A6B54F79AF815D96ED3F562D /* ShrinkConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShrinkConfig.swift; sourceTree = ""; }; + A6EA529AFE126535DFFFB53A /* OwnershipLedger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnershipLedger.swift; sourceTree = ""; }; AA49DA603DB3357CF2DC2371 /* StringBoundaryMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringBoundaryMutator.swift; sourceTree = ""; }; AB742600C6E1AC2CE85EC9C4 /* WeightedPoolCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightedPoolCoreTests.swift; sourceTree = ""; }; ABBEB16314CFB5B6ACC29799 /* PercentageMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentageMutator.swift; sourceTree = ""; }; @@ -791,12 +856,14 @@ B64D06718A05E1272E84861D /* MockDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDatabase.swift; sourceTree = ""; }; B6528B38B2BEED018604E6FC /* TrieEdgeHookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrieEdgeHookTests.swift; sourceTree = ""; }; B69E2CAD9A2DA23DFBCC5890 /* FeatureOwnershipTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureOwnershipTests.swift; sourceTree = ""; }; + B7CB1D8B231D746FBE08DBC5 /* AdaptiveDepthPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveDepthPolicy.swift; sourceTree = ""; }; B96335D04A27579950460A29 /* SchedulerSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerSupport.swift; sourceTree = ""; }; BA01B2725BCFE68C918C2336 /* PlateauDetectorPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateauDetectorPluginTests.swift; sourceTree = ""; }; BAD161F33FE57E2FF36FC9D4 /* DoubleMutators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleMutators.swift; sourceTree = ""; }; BC67D944E495D340262E27CC /* InstrumentationSeamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstrumentationSeamTests.swift; sourceTree = ""; }; C02CEB72860556B925E49CC9 /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; C0E713F121B3A99D66E0AB8A /* PowerOfTwoMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerOfTwoMutator.swift; sourceTree = ""; }; + C342768E738E2FE06AEF0624 /* BoundarySiteAccumulatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundarySiteAccumulatorTests.swift; sourceTree = ""; }; C45F1F52B8DBAE4FADF5B5C0 /* ck_md.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_md.h; sourceTree = ""; }; C4B52072822CAE79551FCAB6 /* MutationLineageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutationLineageTests.swift; sourceTree = ""; }; C4C33B33085A4DB5D1981F0A /* CoverageCountersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageCountersTests.swift; sourceTree = ""; }; @@ -808,12 +875,13 @@ C9A2C049941E715713FD44DD /* UnicodeMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnicodeMutator.swift; sourceTree = ""; }; CA07D6943069C5FF636952CB /* ShrinkResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShrinkResult.swift; sourceTree = ""; }; CB119BBD24F520783D33BA6F /* SanCovHooks.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SanCovHooks.c; sourceTree = ""; }; + CB34AC6D91EE9581E4786649 /* FeatureHashSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureHashSetTests.swift; sourceTree = ""; }; CB81D025D3C307D01FD829DB /* CorpusCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusCoordinatorTests.swift; sourceTree = ""; }; CBB3E73A77D729B720FC8ADA /* PhoneNumberMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberMutator.swift; sourceTree = ""; }; - CBBB30B9BF4F7490C786AE1D /* CorpusClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusClient.swift; sourceTree = ""; }; CC43CD4AEE4429660B4142AE /* FuzzStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzStateMachine.swift; sourceTree = ""; }; CD0587CE21A2AB1B87113BEE /* EdgeHooks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EdgeHooks.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEE33D6A37CCAA419FE56BDE /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; + CF098748DE9F44058DB7BB45 /* GlobalEverCoveredTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEverCoveredTests.swift; sourceTree = ""; }; CF32C2B55AF51B9BB8C58AD8 /* CorpusEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorpusEntry.swift; sourceTree = ""; }; CF7909559B042C15C36EAABE /* PropertyTestingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PropertyTestingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CF7DD32AFECCA9E69821998D /* FuzzEngine+Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FuzzEngine+Config.swift"; sourceTree = ""; }; @@ -834,30 +902,39 @@ E0ABBB2AC9890A3F64DAF698 /* SaturationPlateauDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaturationPlateauDetector.swift; sourceTree = ""; }; E37B0F71C6AF3FAD60F074F7 /* StopWhenQueueEmptyPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopWhenQueueEmptyPluginTests.swift; sourceTree = ""; }; E52C3379771BA6E8B3D1C9EE /* StringMutators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringMutators.swift; sourceTree = ""; }; + E6BB002C2461C0A4D7BFBC66 /* BoundaryDistanceStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundaryDistanceStrategyTests.swift; sourceTree = ""; }; E710A18D4C3A68A36CF37040 /* CoverageGap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageGap.swift; sourceTree = ""; }; E751C3855D21CFCFF69930BC /* ck_pr_llsc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr_llsc.h; sourceTree = ""; }; E8BFD8EB5926A6EE25B0D4A9 /* ck_pr_rtm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr_rtm.h; sourceTree = ""; }; E9FA092D1F7A3D60D1BF95CB /* IntMutators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntMutators.swift; sourceTree = ""; }; EB988F36432EEA023A812BEA /* AlwaysInterestingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysInterestingStrategy.swift; sourceTree = ""; }; EBE8AFD5051896B66F9862E2 /* ck_malloc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_malloc.h; sourceTree = ""; }; + ED5EFB8B81DD1F34E12B2635 /* CmpRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmpRecorderTests.swift; sourceTree = ""; }; ED66F0C3A23B5251B88672FB /* ShrinkStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShrinkStats.swift; sourceTree = ""; }; EDEDCE8D50AA08E8CAF3B63A /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; EE585F0A050A5BF56B67442E /* UInt8+MutatorProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt8+MutatorProviding.swift"; sourceTree = ""; }; + EF17F797111C786B45F76BC5 /* SanCovSuppressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SanCovSuppressionTests.swift; sourceTree = ""; }; EF7AA1611BFAAB73EE70CA85 /* libCLLVMSymbolizer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCLLVMSymbolizer.a; sourceTree = BUILT_PRODUCTS_DIR; }; EF833B020283C4892D55D53C /* CoverageBenchmarks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageBenchmarks.swift; sourceTree = ""; }; F000A4108F2BF3EC22200A76 /* WeightedPoolCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightedPoolCore.swift; sourceTree = ""; }; + F027FD9A95976E20AF15DB68 /* AtomicFeatureSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicFeatureSetTests.swift; sourceTree = ""; }; F0556AB486A6844D1A3B9F04 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + F148196F00001BDE574A8094 /* ComparisonDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonDictionary.swift; sourceTree = ""; }; F1E97A029218EB361C14F01D /* DWARFSymbolizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DWARFSymbolizerTests.swift; sourceTree = ""; }; F3E63606AF341F913872CB76 /* CoverageGapReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageGapReport.swift; sourceTree = ""; }; F4063211048DE74377E41D0A /* ArrayLengthTargetedMutator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayLengthTargetedMutator.swift; sourceTree = ""; }; + F42FD9D07290973345A01F20 /* FeatureHashSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureHashSet.swift; sourceTree = ""; }; F58C560D0D81EBCA41AC8282 /* TestCaseShrinkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCaseShrinkerTests.swift; sourceTree = ""; }; F5C14F22721E00A7EC03198B /* StrategyFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrategyFeatureTests.swift; sourceTree = ""; }; F5E409E9172BADE44207E55E /* PathTrieStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathTrieStrategyTests.swift; sourceTree = ""; }; F5FAD6386630E58099EFA884 /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; F6A80CC19DCB4C4527B0777F /* ParallelTimingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallelTimingTest.swift; sourceTree = ""; }; + F7A060AE28AC76C1D9CE6F8D /* AtomicRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicRep.swift; sourceTree = ""; }; F7B5F39FFCE93451B38B3FBA /* PropertyBasedSelfTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyBasedSelfTests.swift; sourceTree = ""; }; + F7B9F89824089B30381887B6 /* SanCovCmpRecorderGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SanCovCmpRecorderGateTests.swift; sourceTree = ""; }; F7D0C7D1183C89E7346C1405 /* ScheduleHooks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScheduleHooks.h; sourceTree = ""; }; F82662CE734CB5CCF8C0C782 /* BoolMutators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolMutators.swift; sourceTree = ""; }; + F8AD67782E1C097D160C9DFD /* SchedulerProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerProbe.swift; sourceTree = ""; }; F9A2D6D2D787FF8BD1869F6F /* StopOnFirstFailurePluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopOnFirstFailurePluginTests.swift; sourceTree = ""; }; FA1A34B8FC6F4EFE3022741B /* ScheduleControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ScheduleControl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FAD9322AFE91D369F2DE946B /* SyncBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBox.swift; sourceTree = ""; }; @@ -866,7 +943,6 @@ FCA35A9F520278AFD6FC257F /* AnyShrinkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyShrinkable.swift; sourceTree = ""; }; FDD1EC805CD71A270C692864 /* CoverageEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverageEngine.swift; sourceTree = ""; }; FF7EA1B7C028962CAA9A3F56 /* ck_f_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_f_pr.h; sourceTree = ""; }; - FF80A96A17AD018D2CDD24A2 /* SanCovEdgeFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SanCovEdgeFilterTests.swift; sourceTree = ""; }; FF840C500FFAB067BC638703 /* ck_pr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ck_pr.h; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1111,8 +1187,11 @@ isa = PBXGroup; children = ( 6AD3FFE8C65FFC7DFDAABC32 /* ABAInheritanceHandleTests.swift */, + ED5EFB8B81DD1F34E12B2635 /* CmpRecorderTests.swift */, + 8AECCD66326E8AEA9BEF03D3 /* ComparisonObserverTests.swift */, 257B1E9613373B2BF2E9934E /* ContextRecorderTests.swift */, F1E97A029218EB361C14F01D /* DWARFSymbolizerTests.swift */, + CF098748DE9F44058DB7BB45 /* GlobalEverCoveredTests.swift */, A1F82BCFBF0645CBC9D5149D /* InheritanceTest.swift */, FC7508FECD8A4E1A1528B9E5 /* SignatureHashTests.swift */, ); @@ -1330,7 +1409,6 @@ isa = PBXGroup; children = ( 50E5DD3B8575BC75880E15FF /* ContinuousClockClient.swift */, - CBBB30B9BF4F7490C786AE1D /* CorpusClient.swift */, 7A3A65FF014BAD23D72C0772 /* CorpusPersistenceClient.swift */, 3E547A4A91BCC71FBDD10CA9 /* DateClient.swift */, 5EB948CF139436D3D40949EF /* EnvironmentClient.swift */, @@ -1440,6 +1518,7 @@ 5C679683B4D3CDAE4E9BD50C /* ScheduleFlatten.swift */, 94EB367A0BBDEA977C219F3A /* SimpleCoveragePlateauDetector.swift */, 1733E8C2C5D2FC2BC7394036 /* STADSPlateauDetector.swift */, + 9430F665548303A424B1675C /* UncheckedBox.swift */, 331FF506A56D2F60F9E25916 /* CoverageGap */, A529A60907EC64D6F2FE4E53 /* CoverageStrategies */, F94CA8BDDC0253B0AA6FF70C /* Plugins */, @@ -1451,6 +1530,7 @@ 9389B3080515AB75B3627EE4 /* Coverage */ = { isa = PBXGroup; children = ( + 26FEA1D41310218E4667B780 /* ComparisonObserver.swift */, C95BCE905C5A7F433C213114 /* EdgeObserver.swift */, D675F3742488937DF00D923F /* FunctionSizeLookup.swift */, 2A865DFA08A7E0DE3F588EDB /* SanCovCounters.swift */, @@ -1463,7 +1543,10 @@ 960A87E42B67EE446D240535 /* Fuzzing */ = { isa = PBXGroup; children = ( + F7A060AE28AC76C1D9CE6F8D /* AtomicRep.swift */, + F148196F00001BDE574A8094 /* ComparisonDictionary.swift */, 4C06258AF546FB539D68605D /* IssueDetection.swift */, + 0428FEDF41A0A2D02435B30C /* LockMetrics.swift */, D874BB685758F72CF6997938 /* Mutator.swift */, D96EE8FBB7CDA87A6EC73E3D /* Corpus */, 325A807D4AC7F6DF95726FC1 /* FuzzEngine */, @@ -1479,10 +1562,14 @@ 9F55EDA14DC6F058F1B3F32B /* Scheduler */ = { isa = PBXGroup; children = ( + 39FE2C6701E82D1E50C4BDAC /* AdaptiveDepthMath.swift */, + B7CB1D8B231D746FBE08DBC5 /* AdaptiveDepthPolicy.swift */, 2D9CBF00C2790631DB6EE4F9 /* EntropicWeightPolicy.swift */, - 9F2E59331674D16FC32BD5A7 /* FeatureOwnershipLedger.swift */, 48E05741C671DFC85D8A63A2 /* MutationScheduler.swift */, + 8273F365B8AE959073FF1C97 /* OwnershipEvaluators.swift */, + A6EA529AFE126535DFFFB53A /* OwnershipLedger.swift */, 8880B06469BC19A431248CDE /* PoolPlugin.swift */, + F8AD67782E1C097D160C9DFD /* SchedulerProbe.swift */, F000A4108F2BF3EC22200A76 /* WeightedPoolCore.swift */, ); path = Scheduler; @@ -1548,10 +1635,18 @@ isa = PBXGroup; children = ( EB988F36432EEA023A812BEA /* AlwaysInterestingStrategy.swift */, + 5FAEEDF5D30CDE9997EDCEAE /* AtomicFeatureSet.swift */, + 33DF5C7CAC0D8E89CF4B43CB /* BoundaryDistanceStrategy.swift */, + 5F7019EDAF76A64238D3D748 /* BoundarySiteAccumulator.swift */, + 2704E8BD88F40CF9BF414641 /* ComparisonCoverageStrategy.swift */, FDD1EC805CD71A270C692864 /* CoverageEngine.swift */, 2877E9AFD268BEF06837122D /* CoverageProbe.swift */, 672D5DCDC1A16C1291F8044B /* CoverageStrategy.swift */, + 75F1E88DA6B9D475BD918E5B /* CoverageStrategyComposition.swift */, 78CCD0EE426F8A208203ED16 /* CoverageView.swift */, + 1F1097CECA8C84FFD06534FF /* EdgeUnionBitmap.swift */, + F42FD9D07290973345A01F20 /* FeatureHashSet.swift */, + 65874BE183B686F124793FB4 /* HitCountAccumulator.swift */, 0D8CCA1129D052D6BF52BCC1 /* HitCountBucketsStrategy.swift */, 0E6744BAE7780BE09993D850 /* NewEdgeStrategy.swift */, D0BCEAA419004D9808AB03E0 /* PathTrieStrategy.swift */, @@ -1563,6 +1658,7 @@ A77974B787838CD93CE6071A /* Support */ = { isa = PBXGroup; children = ( + 98DA5E50B4D907BB1DBB7C99 /* DeterministicRNG.swift */, AF1E91685C6019AA1D8E23F9 /* Synchronized.swift */, ); path = Support; @@ -1639,9 +1735,10 @@ isa = PBXGroup; children = ( 2BACD85D7C5B37A9C6BE9ED5 /* PCResolutionTest.swift */, - FF80A96A17AD018D2CDD24A2 /* SanCovEdgeFilterTests.swift */, + F7B9F89824089B30381887B6 /* SanCovCmpRecorderGateTests.swift */, 34658F2420967EA35E38058D /* SanCovIsolationTests.swift */, 5793C170004170EB1BC50580 /* SanCovResetTests.swift */, + EF17F797111C786B45F76BC5 /* SanCovSuppressionTests.swift */, 3C4BEC4C9B5FC9BAEF5F9ECE /* WorkerPoolPatternTests.swift */, ); name = SanCovTests; @@ -1692,8 +1789,6 @@ 088E038C25201B0920E58A5A /* Corpus.swift */, FC02D8A68C9A0825BB0B672A /* CorpusCoder.swift */, CF32C2B55AF51B9BB8C58AD8 /* CorpusEntry.swift */, - 6A93E0FF8EB11E1EC6615D0E /* CorpusEntryType.swift */, - 030E3D95F451EC885BDF8E15 /* FailureInfo.swift */, ); path = Corpus; sourceTree = ""; @@ -1701,28 +1796,47 @@ DC660732E9D43D922568F89E /* Fuzzing */ = { isa = PBXGroup; children = ( + 228A4808A96301C32C0855E2 /* AdaptiveDepthChainTests.swift */, + 6B76959C25CA4FE7BE3B00D0 /* AdaptiveDepthInsertedTests.swift */, + 92E4026EC9EA5AC5B792C86E /* AdaptiveDepthMathTests.swift */, + 123C6DAB5ECCBBEC26AB3C89 /* AdaptiveDepthPolicyTests.swift */, + F027FD9A95976E20AF15DB68 /* AtomicFeatureSetTests.swift */, + 507D98899A90C12DB930A5F9 /* BoundaryDistanceLedgerTests.swift */, + E6BB002C2461C0A4D7BFBC66 /* BoundaryDistanceStrategyTests.swift */, + C342768E738E2FE06AEF0624 /* BoundarySiteAccumulatorTests.swift */, + 049F353FB2914B702681DEEC /* CmpOnlySchedulerTests.swift */, + 035DD8EB93B39B3A786B2B45 /* ComparisonCoverageStrategyTests.swift */, + 46DC065206A7731002138A4A /* ComparisonDictionaryTests.swift */, 00EBA13944AF0B757005638A /* ConcurrentFuzzLoadTest.swift */, CB81D025D3C307D01FD829DB /* CorpusCoordinatorTests.swift */, - 9E53225F99BA35278DB06DA6 /* CorpusTests.swift */, 9791710C8985E9069A0AAEA9 /* CoverageEngineTests.swift */, A5A7DD272E538B8E8CFE5C75 /* CoverageGapDetectorTests.swift */, 3DCC188A42F8F55099B6EC2C /* CoverageGapPluginTests.swift */, + 78C7626BD5E1269597F45D31 /* CoverageStrategyCompositionTests.swift */, 3CFC8EFE2F9AF6F2346D1B2D /* CustomCoverageStrategyTests.swift */, 683A330BCB90F626B21D2422 /* CustomFuzzableTests.swift */, 5DE9A7DAD99528274ED05439 /* DeterministicTimingTests.swift */, + 9E8AA636EFB6AED289971643 /* EdgeUnionBitmapTests.swift */, 7338026EE0E559A10E5ECC55 /* EnergyMutationTests.swift */, 24F66CDF058D72AAB14F4EA5 /* EntropicPolicyTests.swift */, + CB34AC6D91EE9581E4786649 /* FeatureHashSetTests.swift */, B69E2CAD9A2DA23DFBCC5890 /* FeatureOwnershipTests.swift */, 8D5B1DD3570EBB6E7D12F912 /* FuzzableProtocolTests.swift */, 99DF2D2D7A9C78BEFDA1C9FF /* FuzzAPITests.swift */, 0BC4138150CDC1ABC2DE7C65 /* FuzzEngineTests.swift */, + 7142A4F7332556BB6AEBF60E /* FuzzInputToStateTests.swift */, 8955074B94D7B6D470F922F2 /* FuzzStatsAccountingTests.swift */, + 01504AA2CCB3DB6DA6C1B75A /* HitCountAccumulatorTests.swift */, 4CD58350A367890040C1786A /* HitCountBucketsStrategyTests.swift */, 3EDEC0EE17BFE2A1E440227B /* IdentityMutantRateTests.swift */, 29BB4785C4BF6BA72ABDB89F /* InputSizeTests.swift */, BC67D944E495D340262E27CC /* InstrumentationSeamTests.swift */, + 53693EB8DEF30AC22B2DCA8C /* IntInputToStateTests.swift */, + 3353F474A52E096EE2840EBF /* LockMetricsTests.swift */, C4B52072822CAE79551FCAB6 /* MutationLineageTests.swift */, 2C2AB425C1886E9C43DA056F /* MutatorTests.swift */, + 43563689823267191ED952F8 /* OwnershipEvaluatorTests.swift */, + 0E37C2066A77A6FBA04095C0 /* OwnershipLedgerTests.swift */, 63C99FD379289FA24BBE7A5B /* ParallelEarlyCancelTest.swift */, F5E409E9172BADE44207E55E /* PathTrieStrategyTests.swift */, BA01B2725BCFE68C918C2336 /* PlateauDetectorPluginTests.swift */, @@ -2266,9 +2380,10 @@ buildActionMask = 2147483647; files = ( DA43DF2C782818DADB74D492 /* PCResolutionTest.swift in Sources */, - E7399441F6F7D6EEB785E4CA /* SanCovEdgeFilterTests.swift in Sources */, + D56F1486FD92906F0DC97ADB /* SanCovCmpRecorderGateTests.swift in Sources */, 6C92AFA4A8A89008D14C5645 /* SanCovIsolationTests.swift in Sources */, 18AD5DD480F2B7FF17911BD8 /* SanCovResetTests.swift in Sources */, + 1B9F28F98D6A4AE8216C770B /* SanCovSuppressionTests.swift in Sources */, 08723356674CF23AB08EFC98 /* WorkerPoolPatternTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2286,16 +2401,16 @@ 2A49D41C9A1A8C66E533B43B /* ArrayPositionAwareMutator.swift in Sources */, DE3BAE7B9FAD3AAAA0F550C6 /* ArrayRepeatedValuesMutator.swift in Sources */, 699B38A3B7C7104055FCE349 /* ArraySequenceInsertionMutator.swift in Sources */, + 33BBAD81800871542AD54BB0 /* AtomicRep.swift in Sources */, 54B77519E22464FF76BAA383 /* Bool+MutatorProviding.swift in Sources */, 61385ABD9D088DA7B1BCEB13 /* BoolMutators.swift in Sources */, 9042C991BBA8806B91F2295E /* CartesianProduct.swift in Sources */, 186AD859AC88863837BB5318 /* Character+MutatorProviding.swift in Sources */, + FEDC78AFD377365871D400C6 /* ComparisonDictionary.swift in Sources */, 31C68AA799FA3B1BDFD06296 /* ContinuousClockClient.swift in Sources */, 6F9841129C9DE90C6C33C057 /* Corpus.swift in Sources */, - AEEEBDAF7BCDBAA9B883B8A2 /* CorpusClient.swift in Sources */, 739024E59B1FFD77DB73EA41 /* CorpusCoder.swift in Sources */, C4CA91D28891C5AF58592017 /* CorpusEntry.swift in Sources */, - 9EC07D8B33B17309C71B083B /* CorpusEntryType.swift in Sources */, 095C1FC99432825350AF90D0 /* CorpusPersistence.swift in Sources */, 53983E0590B008B0818F58EF /* CorpusPersistenceClient.swift in Sources */, 8110224B7D927B57E8FEDE44 /* Data+Shrinkable.swift in Sources */, @@ -2306,7 +2421,6 @@ 5692687BA25F8F8BD548494D /* EmailMutator.swift in Sources */, F7720F80A165FC2F9985DC0A /* EmptyStringMutator.swift in Sources */, 08FCF2C957CE1715E574451B /* EnvironmentClient.swift in Sources */, - A0E40AFCF79C910B2DDB2F4B /* FailureInfo.swift in Sources */, EA21AD66F0B620EA821A3D26 /* FastRNG.swift in Sources */, C109C8B055AFA44A7D3EB58D /* FileManagerClient.swift in Sources */, 3DE4055B12544F96FB88B490 /* FuzzEngine+Config.swift in Sources */, @@ -2320,6 +2434,7 @@ 11950372ACD818C83D608433 /* IntBoundaryMutator.swift in Sources */, 30ABA134D5956FD446CCE3C9 /* IntMutators.swift in Sources */, 9900DE76B0490D232B206DAD /* IssueDetection.swift in Sources */, + 965E6B6DD5F652316B6B3AAC /* LockMetrics.swift in Sources */, 56B8BF93258E463ADA9DD9D6 /* MultiComponentShrinker.swift in Sources */, BA302D143C2AD3179A694BB4 /* Mutator.swift in Sources */, 2BE81B2317F7F136F5AF3E82 /* NegativeIntMutator.swift in Sources */, @@ -2402,36 +2517,59 @@ files = ( 171C6F6047C7576F09F61374 /* ABAInheritanceHandleTests.swift in Sources */, 9C2D7BC931DE426492026F2A /* ActiveContextRegistryStressTests.swift in Sources */, + D50589D8527B6FEB6970623C /* AdaptiveDepthChainTests.swift in Sources */, + 29653509BB27301F722388D4 /* AdaptiveDepthInsertedTests.swift in Sources */, + B990E38E9BE57627D1FD7A75 /* AdaptiveDepthMathTests.swift in Sources */, + 17D5DCD59887D9103E1C9C4A /* AdaptiveDepthPolicyTests.swift in Sources */, + B0B8A35796562DC499238150 /* AtomicFeatureSetTests.swift in Sources */, + BFEB5CAA333D2AE13CB39B7B /* BoundaryDistanceLedgerTests.swift in Sources */, + 0A8EA9C99291BC201A9856F9 /* BoundaryDistanceStrategyTests.swift in Sources */, + 4B2D7D666F6C29F7DDD234C7 /* BoundarySiteAccumulatorTests.swift in Sources */, B26FDBA1F2F9B6BE116325A2 /* CartesianProductTests.swift in Sources */, + 0B4D6F87B283CB18C2DF623C /* CmpOnlySchedulerTests.swift in Sources */, + 4536E5471E56302535CE66F3 /* CmpRecorderTests.swift in Sources */, + 2AABED73782D56B97CB8D409 /* ComparisonCoverageStrategyTests.swift in Sources */, + FD11BA57089157160EDB94AE /* ComparisonDictionaryTests.swift in Sources */, + 21D5E40774A2BA976365E0C3 /* ComparisonObserverTests.swift in Sources */, 2E6CDCBF7B91A07E9402C82F /* ConcurrentFuzzLoadTest.swift in Sources */, F63BA597B8068668511D3B8E /* ContextRecorderTests.swift in Sources */, F9E2D4EA13931A5F9FFC4736 /* CorpusCoordinatorTests.swift in Sources */, - 808DEDCEF3F72F26E4C97724 /* CorpusTests.swift in Sources */, ADF49AE0B09D08080E515387 /* CoverageCountersTests.swift in Sources */, 0AF273C35C44F4B57C9AE477 /* CoverageEngineTests.swift in Sources */, 0AEA251D3FF9432F04D9FC04 /* CoverageGapDetectorTests.swift in Sources */, 5D7BD139A169A1190764B4E8 /* CoverageGapPluginTests.swift in Sources */, + 322D41378EE00FAF31E761A7 /* CoverageStrategyCompositionTests.swift in Sources */, 0FE8890D1104E4FD6DA64832 /* CustomCoverageStrategyTests.swift in Sources */, 154F79A2EEDDE650511A586D /* CustomFuzzableTests.swift in Sources */, 4EB9436B27158A5C6839F9BA /* DWARFSymbolizerTests.swift in Sources */, C7E34069BEF0AD37D592911A /* DependencyLiveValueIsolationTests.swift in Sources */, + 959229692ED895D7BFB923F7 /* DeterministicRNG.swift in Sources */, CD4CF90D44574C94590CCE3F /* DeterministicTimingTests.swift in Sources */, + 295F375C0DEBF15A2495AD12 /* EdgeUnionBitmapTests.swift in Sources */, C0E5C0ED4094D06754BC00C3 /* EnergyMutationTests.swift in Sources */, 85831BC8A71C93AF8B6270D1 /* EntropicPolicyTests.swift in Sources */, + 632D2571661008F7786F766C /* FeatureHashSetTests.swift in Sources */, 94597A6D6154EF4888C34AB1 /* FeatureOwnershipTests.swift in Sources */, 3AE90F2D2F5E78080AAB081C /* FuzzAPITests.swift in Sources */, 244F543DDFAA24140A76485F /* FuzzEngineTests.swift in Sources */, + 8E1B2283A6A7E4FB0E7BDDB2 /* FuzzInputToStateTests.swift in Sources */, 6CB2ABCF9D35BB094D3D11ED /* FuzzStatsAccountingTests.swift in Sources */, B589BFFA6C70C0D16C75E1AD /* FuzzableProtocolTests.swift in Sources */, + 3C347A9952CC4C8E4AC5B11A /* GlobalEverCoveredTests.swift in Sources */, + A298ED17A8111ACF0710632F /* HitCountAccumulatorTests.swift in Sources */, E546F7532EEF4E099063ED08 /* HitCountBucketsStrategyTests.swift in Sources */, 9617C4044A96AF4D9B014040 /* IdentityMutantRateTests.swift in Sources */, 7087CB0E363CDDB5E8D0B815 /* InheritanceTest.swift in Sources */, 83FA5E00DDE707225B67BBB1 /* InputSizeTests.swift in Sources */, 31B706837C40F40D05B1352B /* InstrumentationSeamTests.swift in Sources */, + E273DC1A5CAAB210E1A462BB /* IntInputToStateTests.swift in Sources */, E8ED514CBE637B3DB6879755 /* IssueDetectionTests.swift in Sources */, + 948D9261F5B4172712CF233F /* LockMetricsTests.swift in Sources */, 19E5E7F83FA7FB0675B65818 /* MockDatabase.swift in Sources */, 2CA446146BF11AFA8C0DDD7A /* MutationLineageTests.swift in Sources */, 902AD170388F6A40C15ECCA5 /* MutatorTests.swift in Sources */, + 76399ADB3031E94C7A251B5E /* OwnershipEvaluatorTests.swift in Sources */, + 82AC4438E748A02C9943F011 /* OwnershipLedgerTests.swift in Sources */, 0BBBCECACEFDD2F0255FDB97 /* ParallelEarlyCancelTest.swift in Sources */, D5645DFA85C2ABBD0E34ACC2 /* ParallelTimingTest.swift in Sources */, 190CC6D79C904001E2EC76BF /* PathTrieStrategyTests.swift in Sources */, @@ -2484,7 +2622,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + ED51ED948EFC7AC8688DE5CE /* AdaptiveDepthMath.swift in Sources */, + D246C8D105C8E09BDD92AD97 /* AdaptiveDepthPolicy.swift in Sources */, 6278A355CE18D7FB1ED46FA9 /* AlwaysInterestingStrategy.swift in Sources */, + 3E657BB5EE826DEDF6B354D6 /* AtomicFeatureSet.swift in Sources */, + D9062F141056F0F28EB71027 /* BoundaryDistanceStrategy.swift in Sources */, + 4D3E5F1B9F6C98DBC6821F3A /* BoundarySiteAccumulator.swift in Sources */, + 1CCBBFC23E17E7C597669ED0 /* ComparisonCoverageStrategy.swift in Sources */, + F6788A3D2EECC01143DBEFCC /* ComparisonObserver.swift in Sources */, 1FC08EF5231A3765481B884F /* CorpusCoordinator.swift in Sources */, 0A0B966D3B103DDD9ADC457C /* CoverageCountersClient.swift in Sources */, 61437804FBED0EADE0671F2B /* CoverageEngine.swift in Sources */, @@ -2493,21 +2638,26 @@ 01C409E9A774CE4770B6DC3B /* CoverageGapReport.swift in Sources */, B9388FEEBE26B8B839F43200 /* CoverageProbe.swift in Sources */, 2FDA41240A10E1E9C94D007A /* CoverageStrategy.swift in Sources */, + FFE1FE2A95AFF9F70DD9310A /* CoverageStrategyComposition.swift in Sources */, 53CC4160D51D5F0BB93BF0DC /* CoverageView.swift in Sources */, E22FD5BA11720007BFEC6E1E /* DWARFSourceLocation.swift in Sources */, 4B46C972C1518B04075D7EED /* DWARFSymbolizer.swift in Sources */, D0C65F0813EFB9C22E7A24EC /* DWARFSymbolizerError.swift in Sources */, 6FC71167936E2046141EAA6B /* DWARFSymbolizerHelper.swift in Sources */, 8BD6A306A5F55973C4C54AEB /* EdgeObserver.swift in Sources */, + 8BAD61A1E97D6E59373463CE /* EdgeUnionBitmap.swift in Sources */, 924A59BD7737F5F4CDEAA00C /* EntropicWeightPolicy.swift in Sources */, - 619E8CA36EC2421D248ADCD8 /* FeatureOwnershipLedger.swift in Sources */, + 52D2F4420D90A1093759EA6A /* FeatureHashSet.swift in Sources */, AFDDC40C6C111A0C8359403D /* FunctionSizeLookup.swift in Sources */, 413720205EA64C2558BD9F04 /* FuzzAPI.swift in Sources */, 019A02AA9B1D45F097E4851D /* FuzzEngineConvenience.swift in Sources */, B5DE6B566FD9761D9E7A7612 /* FuzzPluginHandler.swift in Sources */, + 3AFFE52B1972946459F74ECC /* HitCountAccumulator.swift in Sources */, 92DE5B77D7ECE3A78779C1F6 /* HitCountBucketsStrategy.swift in Sources */, 1C9770C71F0A01C606B38EF7 /* MutationScheduler.swift in Sources */, 0BA9AD179FB702D07F12F65E /* NewEdgeStrategy.swift in Sources */, + D9D29FF2CF7BBEBB6C27EEF9 /* OwnershipEvaluators.swift in Sources */, + 19207BE9E1FF2282D2A2AE1B /* OwnershipLedger.swift in Sources */, 8D4A983DD7DF4F96D9676B31 /* PathTrieStrategy.swift in Sources */, C541AB209EF11F49F3ABD83F /* PoolPlugin.swift in Sources */, 5CE75ACE29DD3F8860FAA19D /* STADSPlateauDetector.swift in Sources */, @@ -2516,8 +2666,10 @@ 9B7DC07539CBF59272EDCC37 /* SaturationPlateauDetector.swift in Sources */, 0F22629EF545632A4492EF91 /* ScheduleByteMutator.swift in Sources */, 21C930230313DB0CC2C877D3 /* ScheduleFlatten.swift in Sources */, + 24FE24E8E3703FE7A68D9F28 /* SchedulerProbe.swift in Sources */, 02F3610CF3048E594168DC60 /* SignatureMatchStrategy.swift in Sources */, F854221602A30DC642BC0670 /* SimpleCoveragePlateauDetector.swift in Sources */, + 851ABE7279BA190EF123458F /* UncheckedBox.swift in Sources */, E5CE85300E9595AE8CF1F398 /* UncoveredRegion.swift in Sources */, 673C3E6E506B60678B4A7A01 /* WeightedPoolCore.swift in Sources */, ); diff --git a/Sources/FuzzCore/Dependencies/CorpusClient.swift b/Sources/FuzzCore/Dependencies/CorpusClient.swift deleted file mode 100644 index 2e086a12..00000000 --- a/Sources/FuzzCore/Dependencies/CorpusClient.swift +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2026 DoorDash, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Dependency for creating corpus instances with generic type support. -// - -import Dependencies -import Foundation - -// MARK: - Corpus Registry - -/// Registry for corpus instances. -/// -/// Provides a factory for creating corpus instances with the appropriate type. -/// The Corpus type is ~Copyable (non-copyable) for performance optimization, -/// so it cannot be wrapped in closure-based clients. -public struct CorpusRegistry: Sendable, CorpusRegistryProtocol { - public init() {} - - /// Create a corpus for the given input types. - public func getCorpus() -> Corpus { - return Corpus() - } -} - -public protocol CorpusRegistryProtocol: Sendable { - func getCorpus() -> Corpus -} - -// MARK: - Dependency Key - -private struct CorpusRegistryKey: DependencyKey { - static let liveValue: CorpusRegistryProtocol = CorpusRegistry() - static let testValue: CorpusRegistryProtocol = liveValue -} - -extension DependencyValues { - /// Registry for corpus instances. - /// - /// Use this to create type-specific corpus instances: - /// - /// ```swift - /// @Dependency(\.corpusRegistry) var registry - /// var corpus: Corpus = registry.getCorpus() - /// ``` - public var corpusRegistry: CorpusRegistryProtocol { - get { self[CorpusRegistryKey.self] } - set { self[CorpusRegistryKey.self] = newValue } - } -} diff --git a/Sources/FuzzCore/Dependencies/FastRNG.swift b/Sources/FuzzCore/Dependencies/FastRNG.swift index e1f2756d..e07851fa 100644 --- a/Sources/FuzzCore/Dependencies/FastRNG.swift +++ b/Sources/FuzzCore/Dependencies/FastRNG.swift @@ -96,3 +96,34 @@ extension DependencyValues { set { self[FastRNG.self] = newValue } } } + +// MARK: - Mockable RNG (withRandomNumberGenerator pattern, FastRNG default) + +/// A `\.withRandomNumberGenerator`-style dependency that defaults to `FastRNG` +/// instead of the library's `SystemRandomNumberGenerator`. We reuse Point-Free's +/// `WithRandomNumberGenerator` wrapper (the canonical pattern: a `Sendable` +/// holder yielding `inout` access to a generator through a closure) but seed its +/// live value with our thread-local XorShift64 `FastRNG`, so production keeps +/// that algorithm. Tests override it with a deterministic generator +/// (`$0.fastRandomNumberGenerator = WithRandomNumberGenerator(DeterministicRNG(...))`) +/// so weighted-draw distributions are reproducible rather than flaking on +/// near-ties. +/// +/// `testValue` is also `FastRNG`-backed (not `unimplemented`) because most +/// pool tests draw real randomness and only the distribution-sensitive ones +/// override it. +private enum FastRandomNumberGeneratorKey: DependencyKey { + static let liveValue = WithRandomNumberGenerator(FastRNG()) + static let testValue = WithRandomNumberGenerator(FastRNG()) +} + +extension DependencyValues { + /// RNG access for randomized scheduling decisions (e.g. the weighted + /// mutation pool's draw), following the `withRandomNumberGenerator` pattern + /// but backed by `FastRNG`. Override in tests with a deterministic generator + /// for reproducible draw distributions. + var fastRandomNumberGenerator: WithRandomNumberGenerator { + get { self[FastRandomNumberGeneratorKey.self] } + set { self[FastRandomNumberGeneratorKey.self] = newValue } + } +} diff --git a/Sources/FuzzCore/Fuzzing/AtomicRep.swift b/Sources/FuzzCore/Fuzzing/AtomicRep.swift new file mode 100644 index 00000000..21a60ce3 --- /dev/null +++ b/Sources/FuzzCore/Fuzzing/AtomicRep.swift @@ -0,0 +1,33 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Disambiguating alias for swift-atomics' per-type atomic storage. +// +// swift-atomics gives each `AtomicValue` an `AtomicRepresentation` associated +// type (the flat storage our lock-free accumulators allocate buffers of). Newer +// Swift toolchains ALSO conform the same integer types to the standard library's +// `AtomicRepresentable` (built-in atomics / `Synchronization`), which has its own +// member `AtomicRepresentation`. With both visible, the bare +// `UInt64.AtomicRepresentation` is ambiguous and the build breaks after an +// Xcode/SDK update — even though neither our code nor the swift-atomics pin +// changed. Funnelling the lookup through a context constrained to swift-atomics' +// `AtomicValue` resolves it to that package's storage type, unambiguously. + +import Atomics + +/// swift-atomics' atomic storage for `T` (e.g. `AtomicRep` == +/// `UInt64.AtomicRepresentation` from the `Atomics` package). Use this instead of +/// the bare `T.AtomicRepresentation`, which collides with the stdlib's +/// `AtomicRepresentable.AtomicRepresentation` on newer toolchains. +public typealias AtomicRep = T.AtomicRepresentation diff --git a/Sources/FuzzCore/Fuzzing/ComparisonDictionary.swift b/Sources/FuzzCore/Fuzzing/ComparisonDictionary.swift new file mode 100644 index 00000000..3833eaff --- /dev/null +++ b/Sources/FuzzCore/Fuzzing/ComparisonDictionary.swift @@ -0,0 +1,105 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The learned operand pool behind input-to-state (I2S) mutation. +// +// Edge coverage gives no gradient on a data condition like `x == 0xDEADBEEF` +// or a de Bruijn `i < c`: a near-miss and a hit trace the same edges, so random +// mutation must stumble onto the exact value. I2S short-circuits that — it +// feeds the OPERANDS of each instrumented comparison (delivered by the +// trace-cmp comparison observer) into this dictionary, and numeric mutators +// sample from it, jumping straight to a value some comparison cared about. This +// is the auto-dictionary / RedQueen idea (laf-intel, AFL++ cmplog) cast for +// PropertyTestingKit's typed inputs: the framework's Int/UInt mutators consult +// `ComparisonDictionary.current`, and a workload's bespoke mutator may too. +// + +import Atomics + +/// A bounded, thread-safe pool of recently-seen comparison operands. +/// +/// Backed by a fixed-capacity ring so it tracks the operands of *recent* +/// executions (most relevant to the input being mutated now) without unbounded +/// growth. `record` is on the comparison hot path; sampling is on the mutation +/// path. The active dictionary for the mutators on a given task is published +/// through the `current` task-local, installed by the engine around its loop. +public final class ComparisonDictionary: @unchecked Sendable { + private let capacity: Int + // Fixed ring of recent operands, each slot an atomic UInt64. `cursor` is a + // monotonic write counter; a writer fetch-adds it for a unique index and + // stores into `slot = cursor % capacity`. LOCK-FREE: this was an + // OSAllocatedUnfairLock taken per comparison (Finding 42 — the I2S record + // path fires for every instrumented comparison). A sampler reads a random + // already-written slot; a read racing a write sees one whole value or the + // other (per-slot atomic, no tear), which is fine for a best-effort pool. + // + // `@unchecked Sendable` because the raw atomic-storage pointer is not + // automatically `Sendable`. + private let ring: UnsafeMutablePointer> + private let cursor = UnsafeAtomic.create(0) + + /// - Parameter capacity: how many recent operands to retain (ring size). + public init(capacity: Int = 1024) { + precondition(capacity > 0, "ComparisonDictionary capacity must be positive") + self.capacity = capacity + ring = .allocate(capacity: capacity) + ring.initialize(repeating: AtomicRep(0), count: capacity) + } + + deinit { + ring.deinitialize(count: capacity); ring.deallocate() + cursor.destroy() + } + + /// Operands recorded so far, capped at `capacity` (the live ring size). + private var filled: Int { Int(min(cursor.load(ordering: .relaxed), UInt64(capacity))) } + + /// Record a comparison operand. Lock-free; called from the comparison + /// observer for every instrumented comparison. + public func record(_ value: UInt64) { + let c = cursor.loadThenWrappingIncrement(ordering: .relaxed) + let slot = Int(c % UInt64(capacity)) + UnsafeAtomic(at: ring + slot).store(value, ordering: .relaxed) + } + + /// Whether nothing has been recorded yet. + public var isEmpty: Bool { + cursor.load(ordering: .relaxed) == 0 + } + + /// Sample a uniformly-random recorded operand, or `nil` if empty. For + /// `filled < capacity` only slots `0.. UInt64? { + let n = filled + guard n > 0 else { return nil } + let idx = Int(rng.next() % UInt64(n)) + return UnsafeAtomic(at: ring + idx).load(ordering: .relaxed) + } + + /// The dictionary the current task's mutators should sample from, or `nil` + /// when I2S is not active. Installed by the engine around its mutation loop + /// via `ComparisonDictionary.$current.withValue(_:)`; numeric mutators read + /// it. A task-local so it follows the engine's task and never leaks across + /// independent engines. + @TaskLocal public static var current: ComparisonDictionary? + + /// Opt-in switch for input-to-state mutation, read by the engine. Bind it + /// around a `fuzz` call — `ComparisonDictionary.$inputToStateEnabled + /// .withValue(true) { try await fuzz(...) }` — to enable I2S for just that + /// campaign's task tree (no process-global state, so parallel campaigns and + /// tests never race). The engine also honours the `PTK_INPUT_TO_STATE` + /// environment variable for launch-time opt-in (e.g. eval harnesses). + @TaskLocal public static var inputToStateEnabled: Bool = false +} diff --git a/Sources/FuzzCore/Fuzzing/Corpus/Corpus.swift b/Sources/FuzzCore/Fuzzing/Corpus/Corpus.swift index 5d780e56..cd8deb12 100644 --- a/Sources/FuzzCore/Fuzzing/Corpus/Corpus.swift +++ b/Sources/FuzzCore/Fuzzing/Corpus/Corpus.swift @@ -12,128 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Storage and management of fuzzing inputs with coverage signatures. +// The corpus: a serializable snapshot of the inputs a scheduler chose to retain. +// +// There is no mutable corpus type. The engine holds no corpus during a run; it +// materializes this snapshot once, at run-end, from the scheduler's retained set +// (`AnyScheduler.snapshot()`). The corpus makes no retention decision and carries +// no coverage — interestingness is the scheduler's call, and the aggregate +// covered-edge set is the coverage probe's. // -import Dependencies import Foundation -// MARK: - Corpus Coding Keys - -/// A collection of test inputs the scheduler chose to retain. -/// -/// The corpus is the engine's input store: it holds the inputs a scheduler -/// admitted, each tagged with the coverage signature that was current when it -/// was retained (kept for cross-engine deduplication and serialization). The -/// *aggregate* covered-edge set is no longer the corpus's concern — that is -/// coverage-specific bookkeeping owned by the coverage probe. -/// -/// Thread safety: Access is serialized by `FuzzStateMachine`, so this -/// class does not need its own synchronization. -public final class Corpus: @unchecked Sendable { - - /// All entries in the corpus. - public internal(set) var entries: [CorpusEntry] - - public init(entries: [CorpusEntry]) { - self.entries = entries - } - - public init() { - self.entries = [] - } - - // MARK: - Serialization - - /// Create a snapshot of the corpus state for encoding. - public func snapshot() -> CorpusSnapshot { - return CorpusSnapshot(entries: entries) - } - - /// Number of entries in the corpus. - public var count: Int { entries.count } - - /// Whether the corpus is empty. - public var isEmpty: Bool { entries.isEmpty } - - /// All inputs in the corpus. - public var inputs: [(repeat each Input)] { - entries.map(\.input) - } - - /// All signatures in the corpus. - public var signatures: [SparseCoverage] { - entries.map(\.sparseCoverage) - } - - // MARK: - Adding Entries - - /// Add an entry unconditionally, tagging it with the coverage that was - /// current when it was retained. - /// - /// Storage entry point for the engine once the scheduler said keep. Internal - /// on purpose: strategies are pure judgement and never see the corpus. - public func mergeCoverageAndAdd( - input: (repeat each Input), - scheduleBytes: [UInt8]? = nil, - sparse: SparseCoverage - ) { - entries.append(CorpusEntry( - input: repeat each input, - scheduleBytes: scheduleBytes, - sparseCoverage: sparse - )) - } - - /// Add an entry if its coverage signature hash is new. - /// - /// Used by `mergeCorpusSnapshots` to deduplicate entries from parallel engines. - /// - /// - Returns: `true` if the entry was added, `false` if it was redundant. - @discardableResult - public func addIfInteresting( - input: borrowing (repeat each Input), - scheduleBytes: [UInt8]? = nil, - sparse: consuming SparseCoverage, - signatureHashes: inout Set - ) -> Bool { - let hash = sparse.signatureHash - guard !signatureHashes.contains(hash) else { - return false - } - - signatureHashes.insert(hash) - entries.append(CorpusEntry( - input: repeat each input, - scheduleBytes: scheduleBytes, - sparseCoverage: sparse - )) - return true - } - - /// Add an entry unconditionally with metadata. - public func add( - input: (repeat each Input), - scheduleBytes: [UInt8]? = nil, - sparse: SparseCoverage, - entryType: CorpusEntryType = .coverage, - failure: FailureInfo? = nil - ) { - let entry = CorpusEntry( - input: repeat each input, - scheduleBytes: scheduleBytes, - sparseCoverage: sparse, - entryType: entryType, - failure: failure - ) - entries.append(entry) - } -} - -// MARK: - Corpus Snapshot - -/// A serializable snapshot of corpus state. -/// On disk this is a plain JSON array of entries: `[{input: ...}, ...]` +/// A serializable snapshot of the retained corpus. +/// On disk this is a plain JSON array of entries: `[[42], ["hello", 3]]`. public struct CorpusSnapshot: Sendable, Codable { public let entries: [CorpusEntry] @@ -146,6 +37,11 @@ public struct CorpusSnapshot: Sendable, Codable public var count: Int { entries.count } public var isEmpty: Bool { entries.isEmpty } + /// All inputs in the corpus. + public var inputs: [(repeat each Input)] { + entries.map(\.input) + } + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(entries) @@ -156,10 +52,3 @@ public struct CorpusSnapshot: Sendable, Codable self.entries = try container.decode([CorpusEntry].self) } } - -extension Corpus { - /// Create a corpus from a snapshot. - public convenience init(from snapshot: CorpusSnapshot) { - self.init(entries: snapshot.entries) - } -} diff --git a/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntry.swift b/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntry.swift index 7f9b3d1e..3eac6eb0 100644 --- a/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntry.swift +++ b/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntry.swift @@ -15,7 +15,7 @@ import Foundation import Dependencies -/// A single entry in the corpus: an input and its coverage data. +/// A single entry in the corpus: an input the scheduler chose to retain. public struct CorpusEntry: Sendable, Codable { /// The test input. public let input: (repeat each Input) @@ -24,28 +24,12 @@ public struct CorpusEntry: Sendable, Codable { /// Non-nil when schedule fuzzing is enabled. public let scheduleBytes: [UInt8]? - /// The sparse coverage data. - public let sparseCoverage: SparseCoverage - - /// The reason this entry was added to the corpus. - public let entryType: CorpusEntryType - - // TODO: Move failureInfo into corpus entry type - /// Failure information if this entry caused a test failure. - public let failure: FailureInfo? - public init( input: repeat each Input, - scheduleBytes: [UInt8]? = nil, - sparseCoverage: consuming SparseCoverage, - entryType: CorpusEntryType = .coverage, - failure: FailureInfo? = nil + scheduleBytes: [UInt8]? = nil ) { self.input = (repeat each input) self.scheduleBytes = scheduleBytes - self.sparseCoverage = sparseCoverage - self.entryType = entryType - self.failure = failure } /// Encodes as a plain JSON array of the input pack: `[42]` or `["hello", 3]`. @@ -70,8 +54,5 @@ public struct CorpusEntry: Sendable, Codable { var container = try decoder.unkeyedContainer() self.scheduleBytes = nil self.input = (repeat try container.decode((each Input).self)) - self.sparseCoverage = SparseCoverage() - self.entryType = .coverage - self.failure = nil } } diff --git a/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntryType.swift b/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntryType.swift deleted file mode 100644 index 96b482aa..00000000 --- a/Sources/FuzzCore/Fuzzing/Corpus/CorpusEntryType.swift +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2026 DoorDash, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// The reason this entry was added to the corpus. -public enum CorpusEntryType: String, Codable, Sendable { - /// Entry was added because it discovered new coverage. - case coverage - - /// Entry was added because it caused a test failure. - case failure -} diff --git a/Sources/FuzzCore/Fuzzing/Corpus/FailureInfo.swift b/Sources/FuzzCore/Fuzzing/Corpus/FailureInfo.swift deleted file mode 100644 index cb106a99..00000000 --- a/Sources/FuzzCore/Fuzzing/Corpus/FailureInfo.swift +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2026 DoorDash, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation -import Dependencies - -/// Information about a failure caused by a corpus entry. -/// -/// Based on Elhage 2020 "Property Testing Like AFL" - preserving failure-inducing -/// inputs is critical for regression testing and preventing bug recurrence. -public struct FailureInfo: Codable, Sendable { - /// The type name of the error that occurred. - public let errorType: String - - /// The localized error message. - public let message: String - - /// Optional stack trace (if available). - public let stackTrace: String? - - public init(error: Error, stackTrace: String? = nil) { - self.errorType = String(describing: type(of: error)) - self.message = error.localizedDescription - self.stackTrace = stackTrace - } - - public init( - errorType: String, - message: String, - stackTrace: String? = nil - ) { - self.errorType = errorType - self.message = message - self.stackTrace = stackTrace - } -} diff --git a/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzEngine.swift b/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzEngine.swift index 53439f83..b159bd78 100644 --- a/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzEngine.swift +++ b/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzEngine.swift @@ -41,7 +41,6 @@ import Testing /// public final class FuzzEngine: @unchecked Sendable { @Dependency(\.dateClient) private var dateClient - @Dependency(\.corpusRegistry) private var corpusRegistry // Type alias for the combined input tuple public typealias InputTuple = (repeat each Input) @@ -172,13 +171,10 @@ public final class FuzzEngine: @unchecked Sendab ) } - let corpus: Corpus = corpusRegistry.getCorpus() - let stateMachine = FuzzStateMachine( seeds: seeds, mutators: mutators, inputSize: inputSize, - corpus: corpus, instrumentationProviders: makeInstrumentationProviders(), scheduler: schedulerFactory.makeScheduler(mutators: repeat each mutators), processSyncPlugins: processSyncPlugins, @@ -191,12 +187,11 @@ public final class FuzzEngine: @unchecked Sendab let stateMachineResult = await stateMachine.start() - // Extract copyable fields + // Extract copyable fields. The corpus is already a value snapshot, + // materialized by the state machine from the scheduler at run-end. let stats = stateMachineResult.stats let failures = stateMachineResult.failures - let resultCorpus = stateMachineResult.corpus - - let finalSnapshot = resultCorpus.snapshot() + let finalSnapshot = stateMachineResult.corpus // Send .end event to plugins (for coverage gap analysis, etc.). The // run-spanning summary is assembled from the installed probes (e.g. the diff --git a/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzStateMachine.swift b/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzStateMachine.swift index bb3fb0c7..1635c2d5 100644 --- a/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzStateMachine.swift +++ b/Sources/FuzzCore/Fuzzing/FuzzEngine/FuzzStateMachine.swift @@ -38,7 +38,6 @@ final class FuzzStateMachine: @unchecked Sendabl /// Async plugin processor closure for rare events. private let processAsyncPlugins: AsyncPluginProcessorFn private let config: FuzzEngineConfig - private var corpus: Corpus private let mutators: (repeat Mutator) private let inputSize: Int private let seeds: [(repeat each Input)] @@ -96,7 +95,6 @@ final class FuzzStateMachine: @unchecked Sendabl seeds: [(repeat each Input)], mutators: (repeat Mutator), inputSize: Int, - corpus: Corpus, instrumentationProviders: [any InstrumentationProvider], scheduler: AnyScheduler, processSyncPlugins: @escaping SyncPluginProcessorFn, @@ -120,7 +118,6 @@ final class FuzzStateMachine: @unchecked Sendabl self.processSyncPlugins = processSyncPlugins self.processAsyncPlugins = processAsyncPlugins self.config = config - self.corpus = corpus self.test = test self.scheduleBytesExtractor = scheduleBytesExtractor self.pendingInputs = SimpleRingBuffer(minimumCapacity: 16) @@ -137,7 +134,10 @@ final class FuzzStateMachine: @unchecked Sendabl struct FuzzStateMachineResult { let stats: FuzzStats - let corpus: Corpus + /// The corpus, materialized once at run-end from the scheduler's retained + /// set — the engine holds no corpus during the run. A value snapshot, not + /// a live store. + let corpus: CorpusSnapshot let failures: [(input: (repeat each Input), error: Error, timeElapsed: TimeInterval, scheduleBytes: [UInt8]?)] /// Run-spanning instrumentation summary, assembled from the installed /// probes at campaign end and surfaced to the `.end` event. The engine @@ -224,6 +224,16 @@ final class FuzzStateMachine: @unchecked Sendabl let input: (repeat each Input) let parentID: Int? let source: SchedulerSource + // Suppress coverage dispatch while PRODUCING the input: the + // scheduler's generate/mutate runs instrumented SUT code + // (e.g. a type-directed generator calling getTyp), but that + // is not the property under test and is reset away below — + // dispatching and recording it wasted ~25% of the process + // (Finding 41p). Straight-line synchronous (no await → no + // thread hop), cleared before resetCoverage so the test is + // always measured. Per-thread, so concurrent engines never + // suppress each other. + sancov_set_dispatch_suppressed(true) if !pendingInputs.isEmpty { assert( pendingParents.count == pendingInputs.count, @@ -254,6 +264,9 @@ final class FuzzStateMachine: @unchecked Sendabl } } let currentScheduleBytes: [UInt8]? = scheduleBytesExtractor(input) + // Done producing — re-enable dispatch so the test below is + // measured (must precede every probe's reset and the test). + sancov_set_dispatch_suppressed(false) // Inputs still queued after taking this one. A plugin can use // `queueCount == 0` to detect that the queue has drained — e.g. to @@ -297,19 +310,12 @@ final class FuzzStateMachine: @unchecked Sendabl for probe in probes { probe.contribute(to: &execContext) } // Retention is the scheduler's call. `observe` reads whatever - // signals it needs out of the context, folds them into its - // own state (and its own working set, if any), and returns - // the signature to persist when the input should be retained - // in the corpus — or nil to retain nothing. The engine names - // no signal and owns no pool: it only persists the corpus - // entry (the result/dedup/cross-engine-merge store). - if let signature = scheduler.observe(input, execContext, source) { - corpus.mergeCoverageAndAdd( - input: input, - scheduleBytes: currentScheduleBytes, - sparse: signature - ) - } + // signals it needs out of the context and folds them into its + // own state and working set. The engine persists nothing here: + // the corpus is built once, after the loop, from the + // scheduler's retained set (`snapshot()`). The engine names no + // signal and owns no pool. + scheduler.observe(input, execContext, source) // Iteration event (sync, hot path) before failure event. // Dispatched directly — not via an `[PluginEvent]` array — to @@ -373,6 +379,16 @@ final class FuzzStateMachine: @unchecked Sendabl "[FUZZ] FuzzStateMachine.start() finished: totalInputs=\(stats.totalInputs), duration=\(stats.duration), stopReason=\(stats.stopReason)" ) } + // Materialize the corpus from the scheduler's retained set, now that the + // run is over. The corpus is the scheduler's working set serialized as a + // value snapshot — the engine holds no corpus during the run and there + // are no per-iteration writers. Evicted inputs are already absent. + let corpus = CorpusSnapshot( + entries: scheduler.snapshot().map { (input: (repeat each Input)) in + CorpusEntry(input: repeat each input) + } + ) + // Assemble the run-spanning summary from the installed probes (e.g. the // coverage probe contributes its union of covered edges). The engine // names no signal — each probe contributes its own aggregate by key. @@ -465,15 +481,6 @@ final class FuzzStateMachine: @unchecked Sendabl ) } enqueuePending(mutants, parent: mutationAction.originID) - - case .submitToCorpus(let corpusAction): - addToCorpus( - corpusAction.input, - scheduleBytes: corpusAction.scheduleBytes, - sparse: corpusAction.sparseCoverage, - type: corpusAction.entryType, - failureInfo: corpusAction.failureInfo - ) } } @@ -492,11 +499,4 @@ final class FuzzStateMachine: @unchecked Sendabl if scope == .campaign { haltScope = .campaign } } - private func addToCorpus( - _ input: (repeat each Input), scheduleBytes: [UInt8]? = nil, sparse: SparseCoverage, - type: CorpusEntryType, failureInfo: FailureInfo? - ) { - corpus.add(input: input, scheduleBytes: scheduleBytes, sparse: sparse, entryType: type, failure: failureInfo) - } - } diff --git a/Sources/FuzzCore/Fuzzing/LockMetrics.swift b/Sources/FuzzCore/Fuzzing/LockMetrics.swift new file mode 100644 index 00000000..4ec6975f --- /dev/null +++ b/Sources/FuzzCore/Fuzzing/LockMetrics.swift @@ -0,0 +1,100 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Env-gated lock-acquisition metrics (PTK_LOCK_METRICS). A measurement scaffold +// to validate empirically which locks (SyncBox, ComparisonDictionary) sit on the +// per-dispatch hot path and whether they ever contend — before deciding which to +// make lock-free. OFF by default: a disabled lock takes the plain acquire path +// and pays nothing. When on, each acquisition bumps a per-label atomic counter, +// and acquisitions that found the lock already held bump a separate "contended" +// counter (via a non-blocking try first). Counters dump to stderr at exit. + +import Foundation +import Atomics + +/// Per-label aggregate acquisition counters. One instance per distinct label, +/// shared across every lock created with that label. +final class LockMetrics: @unchecked Sendable { + let label: String + let acquisitions = ManagedAtomic(0) + let contended = ManagedAtomic(0) + + private init(label: String) { self.label = label } + + /// Process-wide enable, read from the environment. Not cached so tests can + /// opt in per-instance via `forceMetrics` without depending on launch env. + static var envEnabled: Bool { + guard let v = ProcessInfo.processInfo.environment["PTK_LOCK_METRICS"] else { return false } + return !v.isEmpty && v != "0" + } + + /// Mutable registry state behind a single immutable `static let` so there is + /// no nonisolated mutable global. All access is guarded by `lock`. + private final class Store: @unchecked Sendable { + let lock = NSLock() + var registry: [String: LockMetrics] = [:] + var atexitInstalled = false + } + private static let store = Store() + + /// Return the shared counter for `label`, creating it once. Returns nil when + /// metrics are disabled (and not force-enabled) — the caller then takes the + /// plain, uninstrumented lock path. + static func register(_ label: String, force: Bool = false) -> LockMetrics? { + guard force || envEnabled else { return nil } + store.lock.lock() + defer { store.lock.unlock() } + if !store.atexitInstalled { + store.atexitInstalled = true + atexit { LockMetrics.dump() } // non-capturing → @convention(c) + } + if let m = store.registry[label] { return m } + let m = LockMetrics(label: label) + store.registry[label] = m + return m + } + + /// Test accessor: aggregate counts for a label, or nil if never registered. + static func snapshotForTesting(_ label: String) -> (acquisitions: Int, contended: Int)? { + store.lock.lock() + defer { store.lock.unlock() } + guard let m = store.registry[label] else { return nil } + return (m.acquisitions.load(ordering: .relaxed), m.contended.load(ordering: .relaxed)) + } + + /// Write the per-label table to stderr, busiest first. + static func dump() { + store.lock.lock() + let all = Array(store.registry.values) + store.lock.unlock() + guard !all.isEmpty else { return } + let sorted = all.sorted { + $0.acquisitions.load(ordering: .relaxed) > $1.acquisitions.load(ordering: .relaxed) + } + func pad(_ s: String, _ w: Int) -> String { + s.count >= w ? s : s + String(repeating: " ", count: w - s.count) + } + func lpad(_ s: String, _ w: Int) -> String { + s.count >= w ? s : String(repeating: " ", count: w - s.count) + s + } + var out = "=== PTK_LOCK_METRICS ===\n" + out += pad("label", 40) + lpad("acquisitions", 16) + lpad("contended", 14) + "\n" + for m in sorted { + let a = m.acquisitions.load(ordering: .relaxed) + let c = m.contended.load(ordering: .relaxed) + out += pad(m.label, 40) + lpad("\(a)", 16) + lpad("\(c)", 14) + "\n" + } + FileHandle.standardError.write(Data(out.utf8)) + } +} diff --git a/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Int+MutatorProviding.swift b/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Int+MutatorProviding.swift index a08c68ba..1ee13e9d 100644 --- a/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Int+MutatorProviding.swift +++ b/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Int+MutatorProviding.swift @@ -53,7 +53,29 @@ private let _intSeeds: [Int] = [ -1_000_000, ] +/// Input-to-state candidate: when a comparison dictionary is installed on the +/// current task (the engine's I2S channel), half the time jump straight to a +/// recorded comparison operand — or a ±1 neighbour, since `<`/`<=` boundary +/// bugs differ from the operand by one. Returns nil when I2S is inactive (no +/// dictionary, empty dictionary, or the coin lands on normal mutation), so the +/// caller falls through to its ordinary strategy. This is the auto-dictionary / +/// RedQueen mechanism for the framework's integer inputs. +private func _intInputToState(_ rng: inout FastRNG) -> Int? { + guard let dict = ComparisonDictionary.current else { return nil } + // Leave half the draws to ordinary mutation so I2S guides without starving + // the rest of the search (the over-bias failure mode of value profile). + guard rng.next() & 1 == 0 else { return nil } + guard let operand = dict.randomValue(using: &rng) else { return nil } + let base = Int(truncatingIfNeeded: operand) + switch rng.next() & 3 { + case 0: return base &+ 1 + case 1: return base &- 1 + default: return base + } +} + private func _intMutate(_ value: Int, _ rng: inout FastRNG) -> Int { + if let i2s = _intInputToState(&rng) { return i2s } // Enumerate the applicable candidate closures, then pick ONE lazily: the // mutator's job is variety per call, not effort (issue #41). var strategies: [() -> Int] = [] @@ -87,6 +109,7 @@ private func _intMutate(_ value: Int, _ rng: inout FastRNG) -> Int { } private func _intGenerate(_ rng: inout FastRNG) -> Int { + if let i2s = _intInputToState(&rng) { return i2s } // Mix of strategies for interesting random generation let strategy = Int.random(in: 0..<10, using: &rng) switch strategy { diff --git a/Sources/FuzzCore/Fuzzing/Plugins/FuzzPlugin.swift b/Sources/FuzzCore/Fuzzing/Plugins/FuzzPlugin.swift index 5ad5c9e6..277661d6 100644 --- a/Sources/FuzzCore/Fuzzing/Plugins/FuzzPlugin.swift +++ b/Sources/FuzzCore/Fuzzing/Plugins/FuzzPlugin.swift @@ -191,8 +191,6 @@ public enum FuzzPluginAction: Sendable { case queueInputs(QueueInputsAction) /// Select an input for mutation (e.g., shrunk input). case selectForMutation(SelectForMutationAction) - /// Submit an input to the corpus. - case submitToCorpus(SubmitToCorpusAction) /// Action to stop fuzzing. public struct StopAction: Sendable { @@ -255,30 +253,6 @@ public enum FuzzPluginAction: Sendable { } } - /// Action to submit an input to the corpus. - public struct SubmitToCorpusAction: Sendable { - /// The input to submit to the corpus. - public let input: (repeat each T) - /// Schedule bytes for this corpus entry. - public let scheduleBytes: [UInt8]? - public let sparseCoverage: SparseCoverage - public let entryType: CorpusEntryType - public let failureInfo: FailureInfo? - - public init( - input: consuming (repeat each T), - scheduleBytes: [UInt8]? = nil, - sparseCoverage: SparseCoverage, - entryType: CorpusEntryType, - failureInfo: FailureInfo? = nil - ) { - self.input = input - self.scheduleBytes = scheduleBytes - self.sparseCoverage = sparseCoverage - self.entryType = entryType - self.failureInfo = failureInfo - } - } } // MARK: - Analysis Actions (regression-valid subset) @@ -288,7 +262,7 @@ public enum FuzzPluginAction: Sendable { /// A replay runs a fixed set of inputs (the saved corpus) and treats the on-disk /// corpus as authoritative, so the only meaningful actions are *control* and /// *observation* — `stop` and `recordIssue`. The *write* actions that mutate the -/// run (`queueInputs`, `selectForMutation`, `submitToCorpus`) are deliberately +/// run (`queueInputs`, `selectForMutation`) are deliberately /// absent: a handler typed to emit `AnalysisAction` literally cannot name them, so /// `regress(...)` can only ever be handed plugins that emit valid actions. This is /// the compile-time guarantee — there is no runtime gate. diff --git a/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerCore.swift b/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerCore.swift index ae7ca175..d7954adb 100644 --- a/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerCore.swift +++ b/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerCore.swift @@ -65,14 +65,17 @@ public enum SchedulerSource: Equatable, Sendable { /// per-engine scheduler implementation. Built by a `SchedulerFactory`, one fresh /// instance per engine, so the captured state needs no synchronization. /// -/// The engine consults it through exactly two calls per iteration: +/// The engine consults it through three calls: /// - `next()` — produce the next input to run (generate fresh or mutate one of /// the scheduler's own retained inputs). /// - `observe(...)` — report what just ran (the typed input + the per-execution -/// `RawExecutionContext`); the scheduler folds the signals into its state and -/// returns a signature to persist when the input should be retained in the -/// corpus, or `nil` to retain nothing. The scheduler stores the input in its -/// own working set as it sees fit; the engine only persists the corpus entry. +/// `RawExecutionContext`); the scheduler folds the signals into its own state +/// and working set as it sees fit. It returns nothing: retention is no longer +/// a per-iteration corpus write. The engine names no signal and owns no pool. +/// - `snapshot()` — vend the inputs the scheduler currently retains. The engine +/// calls this ONCE, after the run loop, to build the corpus. The corpus is the +/// scheduler's retained set serialized; it is no longer maintained as the run +/// proceeds, and coverage is no longer a corpus concern. public struct AnyScheduler { /// The instrumentation signals this scheduler reads. The engine installs /// only these probes, so a scheduler that wants nothing exotic pays for @@ -80,29 +83,46 @@ public struct AnyScheduler { public let requiredProbes: [any InstrumentationKey.Type] /// Produce the next input to run. public let next: () -> ScheduledInput - /// Report one executed iteration; return the signature to persist if the - /// input should be retained in the corpus, else `nil`. - public let observe: ((repeat each Input), RawExecutionContext, SchedulerSource) -> SparseCoverage? + /// Report one executed iteration; the scheduler folds the signals into its + /// own state. No return: the engine persists nothing per iteration. + public let observe: ((repeat each Input), RawExecutionContext, SchedulerSource) -> Void + /// The inputs the scheduler currently retains, vended once at run-end to + /// build the corpus. A pool-less scheduler returns `[]`. + public let snapshot: () -> [(repeat each Input)] public init( requiredProbes: [any InstrumentationKey.Type], next: @escaping () -> ScheduledInput, - observe: @escaping ((repeat each Input), RawExecutionContext, SchedulerSource) -> SparseCoverage? + observe: @escaping ((repeat each Input), RawExecutionContext, SchedulerSource) -> Void, + snapshot: @escaping () -> [(repeat each Input)] ) { self.requiredProbes = requiredProbes self.next = next self.observe = observe + self.snapshot = snapshot } } /// Builds a fresh per-engine scheduler at the engine's input pack. /// -/// Pack-generic so the produced `AnyScheduler` can own typed inputs — mirrors -/// `CorpusRegistryProtocol.getCorpus()`. `Sendable` because one factory -/// is shared across parallel engines (each calls `makeScheduler` to get its own -/// scheduler); the produced scheduler is per-engine and not shared. +/// Pack-generic so the produced `AnyScheduler` can own the typed inputs it +/// schedules. `Sendable` because one factory is shared across parallel engines +/// (each calls `makeScheduler` to get its own scheduler); the produced scheduler +/// is per-engine and not shared. public protocol SchedulerFactory: Sendable { func makeScheduler( mutators: repeat Mutator ) -> AnyScheduler + + /// The instrumentation providers this scheduler's signals need, built fresh + /// per engine. The signal travels WITH the scheduler: a coverage scheduler + /// vends a coverage provider, a cmp scheduler a cmp provider, a pool-less or + /// blackbox scheduler none. The engine installs only the providers whose key + /// some scheduler's `requiredProbes` names, so over-vending is harmless. + /// Default: no providers (the scheduler reads no instrumentation signal). + func makeProviders() -> [any InstrumentationProvider] +} + +public extension SchedulerFactory { + func makeProviders() -> [any InstrumentationProvider] { [] } } diff --git a/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerSupport.swift b/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerSupport.swift index bae918df..e6a17234 100644 --- a/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerSupport.swift +++ b/Sources/FuzzCore/Fuzzing/Scheduler/SchedulerSupport.swift @@ -59,3 +59,23 @@ public func mutateOneRandomPosition( let position = inputSize == 1 ? 0 : Int.random(in: 0..( + _ input: (repeat each Input), + depth: Int, + inputSize: Int, + rng: inout FastRNG, + mutators: repeat Mutator +) -> (repeat each Input) { + var current = input + for _ in 0..: @unchecked Sendable { private var storage: T private let lock = NSLock() + /// Non-nil only when PTK_LOCK_METRICS is on (or `forceMetrics`). Off by + /// default — `acquire()` then takes the plain `lock.lock()` path. + private let metrics: LockMetrics? /// Read or write the wrapped value in a thread-safe manner. public var value: T { get { - lock.lock() + acquire() defer { lock.unlock() } return storage } set { - lock.lock() + acquire() defer { lock.unlock() } storage = newValue } } - public init(_ value: T) { + /// - Parameters: + /// - label: identifies this box in the PTK_LOCK_METRICS dump (the + /// call-site, e.g. "hitCountBuckets.state"). Empty = unlabeled. + /// - forceMetrics: enable counting regardless of the env var (tests). + public init(_ value: T, label: String = "", forceMetrics: Bool = false) { self.storage = value + self.metrics = label.isEmpty && !forceMetrics + ? nil + : LockMetrics.register(label, force: forceMetrics) + } + + /// Take the lock, counting the acquisition (and whether it was contended) + /// when metrics are enabled. Zero overhead when disabled. + private func acquire() { + if let m = metrics { + if !lock.try() { + m.contended.wrappingIncrement(ordering: .relaxed) + lock.lock() + } + m.acquisitions.wrappingIncrement(ordering: .relaxed) + } else { + lock.lock() + } } /// Atomically update the value with a transform closure. @discardableResult public func update(_ transform: (inout T) throws -> Result) rethrows -> Result { - lock.lock() + acquire() defer { lock.unlock() } return try transform(&storage) } diff --git a/Sources/PropertyTestingKit/Coverage/ComparisonObserver.swift b/Sources/PropertyTestingKit/Coverage/ComparisonObserver.swift new file mode 100644 index 00000000..893699c1 --- /dev/null +++ b/Sources/PropertyTestingKit/Coverage/ComparisonObserver.swift @@ -0,0 +1,120 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Swift per-comparison callbacks for coverage strategies (the trace-cmp half). +// +// A `ComparisonObserver` is how a strategy expresses per-comparison work in +// Swift: a value-profile strategy's observer hashes each comparison's +// (pc, popcount(arg1 ^ arg2)) into a feature set, giving a gradient as an +// input nears a boundary `i < c` — the signal pure edge coverage is blind to. +// Like `EdgeObserver`, the observer is attached to a measurement context which +// CO-OWNS it: retained at attach, released when the context's last reference +// drops. It rides the INDEPENDENT cmp recorder slot, so a strategy can attach +// both an edge observer and a comparison observer to the same context. +// +// This file lives in PropertyTestingKit, which is NOT compiled with +// -sanitize-coverage — the recorder below fires no comparisons of its own. +// Comparisons fired by an `onCompare` closure that lives in instrumented code +// are kept from re-entering it by the SAME per-thread gate edges use +// (`sancov_observer_enter`): re-entry would deadlock any non-reentrant lock +// the callback holds. +// + +import Foundation +import SanCovHooks + +/// A strategy's per-comparison callback (and optional per-iteration reset), +/// called from the cmp-dispatch path for comparisons that route to the context +/// it is attached to. +final class ComparisonObserver: Sendable { + /// Called for EVERY instrumented comparison that routes to the context: + /// the comparison site's PC, both operands (zero-extended to 64 bits), and + /// the operand width in bytes. Because Swift instruments its own runtime + /// comparisons (refcounts, bounds checks, address compares), a strategy + /// MUST key on `pc` to isolate the comparisons it cares about from chatter. + /// + /// - Important: this runs once per COMPARISON on the hot path — a hot loop + /// can call it millions of times per second. + let onCompare: @Sendable (_ pc: UInt, _ arg1: UInt64, _ arg2: UInt64, _ size: UInt32) -> Void + + /// Called when the context's coverage is reset between iterations, so + /// per-iteration state (e.g. this run's value-profile feature buffer) + /// starts each run clean. + let onReset: (@Sendable () -> Void)? + + init(onCompare: @escaping @Sendable (UInt, UInt64, UInt64, UInt32) -> Void, + onReset: (@Sendable () -> Void)? = nil) { + self.onCompare = onCompare + self.onReset = onReset + } +} + +/// The recorder behind every `ComparisonObserver`: reach the observer box +/// through one acquire load on the context, then call `onCompare` under the +/// shared observer gate. Unlike the edge recorder there is no map to touch — +/// cmp recording is a parallel channel that only delivers operands. +let comparisonObserverRecorder: SanCovCmpRecorder = { pc, arg1, arg2, size, context in + guard let context else { return } + guard let data = sancov_context_get_cmp_recorder_data(context) else { return } + guard sancov_observer_enter() else { return } + defer { sancov_observer_exit() } + // `_withUnsafeGuaranteedRef`, not `takeUnretainedValue`: the context CO-OWNS + // the observer (retained at attach, released only when the context's last + // reference drops), so while this recorder runs — holding `context` — the + // observer is provably alive. takeUnretainedValue returns a managed +0 + // reference that the compiler still brackets with a retain/release pair PER + // COMPARISON (profiled ARC churn, Finding 41d). The guaranteed-ref form tells + // the optimiser the object can't die for the closure's duration, eliding + // that pair entirely. + Unmanaged.fromOpaque(data)._withUnsafeGuaranteedRef { + $0.onCompare(pc, arg1, arg2, size) + } +} + +/// Reset hook: forwards `sancov_reset_coverage` to the observer. Shares the +/// observer gate so `onCompare` never runs for comparisons fired by `onReset`. +private let comparisonObserverReset: @convention(c) (UnsafeMutableRawPointer?) -> Void = { data in + guard let data else { return } + guard sancov_observer_enter() else { return } + defer { sancov_observer_exit() } + // Guaranteed-ref for the same reason as the recorder: the context co-owns + // the observer and is alive across this call. + Unmanaged.fromOpaque(data)._withUnsafeGuaranteedRef { + $0.onReset?() + } +} + +/// Release hook: balances the attach-time retain when the context drops its +/// last reference (or the recorder is replaced). +private let comparisonObserverRelease: @convention(c) (UnsafeMutableRawPointer?) -> Void = { data in + guard let data else { return } + Unmanaged.fromOpaque(data).release() +} + +extension SanCovCounters { + /// Attach a Swift comparison observer to a measurement context. The context + /// retains the observer until its own last reference drops — attaching + /// transfers shared ownership, so the caller may drop the observer (and + /// everything its closures capture) immediately. Independent of any edge + /// observer attached to the same context. + static func attachComparisonObserver(_ observer: ComparisonObserver, to context: MeasurementContext) { + sancov_context_set_cmp_recorder( + context.rawContext, + comparisonObserverRecorder, + Unmanaged.passRetained(observer).toOpaque(), + comparisonObserverReset, + comparisonObserverRelease + ) + } +} diff --git a/Sources/PropertyTestingKit/Coverage/SanCovCounters.swift b/Sources/PropertyTestingKit/Coverage/SanCovCounters.swift index 14475dcc..3e8958ac 100644 --- a/Sources/PropertyTestingKit/Coverage/SanCovCounters.swift +++ b/Sources/PropertyTestingKit/Coverage/SanCovCounters.swift @@ -117,20 +117,42 @@ enum SanCovCounters { sancov_get_counter_count() } - /// Filter out compiler-generated edges (outlined destroyers, lazy witness - /// table accessors, lazy metadata accessors, etc.) by setting their guard - /// values to `UINT32_MAX`. This makes the hot-path check - /// `*guard < g_guard_count` fail for these edges — zero overhead. - /// - /// Call once before any measurement begins. Safe to call multiple times - /// (subsequent calls re-scan, which is harmless). - static func applyEdgeFilter() { - sancov_apply_edge_filter() + // Compiler-generated edges are now filtered at COMPILE time by the + // TagCompilerGenerated LLVM pass plugin (see Package.swift); the former + // runtime `applyEdgeFilter()` / `filteredEdgeCount` API has been removed. + + // MARK: - Global ever-covered bitmap (diagnostic) + // + // A process-global accumulator that records every allowed edge fire, + // independent of any measurement context, the engine's per-iteration reset, + // and corpus banking. Use it to answer "did a whole run reach full SUT + // coverage?" — a question the per-context snapshot (reset each iteration) + // and the corpus union (admitted inputs only) cannot answer. Disabled by + // default; enable once, reset between runs, read the count/indices. + + /// Enable global ever-covered recording (idempotent). + static func enableGlobalEverCovered() { + sancov_enable_global_ever_covered() + } + + /// Clear the global ever-covered bitmap (keeps recording enabled). + static func resetGlobalEverCovered() { + sancov_reset_global_ever_covered() } - /// The number of edges disabled by `applyEdgeFilter()`. - static var filteredEdgeCount: Int { - sancov_get_filtered_count() + /// Number of distinct edges fired since the last reset (0 if disabled). + static var globalEverCoveredCount: Int { + sancov_global_ever_covered_count() + } + + /// Sorted indices of every edge fired since the last reset. + static func snapshotGlobalEverCovered() -> [UInt32] { + var count = 0 + guard let ptr = sancov_snapshot_global_ever_covered(&count), count > 0 else { + return [] + } + defer { free(ptr) } + return Array(UnsafeBufferPointer(start: ptr, count: count)) } } diff --git a/Sources/PropertyTestingKit/Fuzzing/CorpusCoordinator.swift b/Sources/PropertyTestingKit/Fuzzing/CorpusCoordinator.swift index f9b37e94..6674fc52 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CorpusCoordinator.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CorpusCoordinator.swift @@ -86,7 +86,6 @@ func runFuzz( parallelism: Int, duration: Duration, verbose: Bool, - coverageStrategy: CoverageStrategy, scheduler: any SchedulerFactory, projectPath: String?, sourceFileID: String, @@ -122,7 +121,6 @@ func runFuzz( mutators: mutators, verbose: verbose, config: makeConfig(), - coverageStrategy: .alwaysInteresting, scheduleBytesExtractor: scheduleBytesExtractor, plugins: { [] }, test: test @@ -136,7 +134,6 @@ func runFuzz( verbose: verbose, persist: true, config: makeConfig(), - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleBytesExtractor: scheduleBytesExtractor, makeHandlers: makeHandlers, @@ -158,7 +155,6 @@ func runFuzz( verbose: verbose, persist: true, config: makeConfig(), - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleBytesExtractor: scheduleBytesExtractor, makeHandlers: makeHandlers, @@ -182,7 +178,6 @@ func runFuzz( verbose: verbose, persist: true, config: makeConfig(), - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleBytesExtractor: scheduleBytesExtractor, makeHandlers: makeHandlers, @@ -199,7 +194,6 @@ func runFuzz( verbose: verbose, persist: false, config: makeConfig(), - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleBytesExtractor: scheduleBytesExtractor, makeHandlers: makeHandlers, @@ -253,7 +247,6 @@ func runReplay( mutators: mutators, verbose: verbose, config: config, - coverageStrategy: .alwaysInteresting, plugins: plugins, test: test ) @@ -275,7 +268,6 @@ private func replayRegression( mutators: (repeat Mutator), verbose: Bool, config: FuzzEngineConfig, - coverageStrategy: CoverageStrategy, scheduleBytesExtractor: @escaping @Sendable ((repeat each Input)) -> [UInt8]? = { _ in nil }, plugins: @escaping @Sendable () -> [AnalysisPlugin], test: @escaping @Sendable ((repeat each Input)) async throws -> Void @@ -290,7 +282,10 @@ private func replayRegression( parallelism: 1, verbose: verbose, config: config, - coverageStrategy: coverageStrategy, + // Replay measures coverage with `.alwaysInteresting` so the campaign-end + // union sees every replayed input; the pool itself is unused (the seeded + // queue drains and the run stops). Coverage rides the scheduler now. + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), scheduleBytesExtractor: scheduleBytesExtractor, makeProcessor: { let lifted = (plugins() + [AnalysisPlugin.stopWhenQueueEmpty()]) @@ -322,7 +317,6 @@ private func fuzzCampaign( verbose: Bool, persist: Bool, config: FuzzEngineConfig, - coverageStrategy: CoverageStrategy, scheduler: any SchedulerFactory, scheduleBytesExtractor: @escaping @Sendable ((repeat each Input)) -> [UInt8]? = { _ in nil }, makeHandlers: @escaping @Sendable () -> [FuzzPlugin], @@ -345,7 +339,6 @@ private func fuzzCampaign( parallelism: max(1, parallelism), verbose: verbose, config: config, - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleBytesExtractor: scheduleBytesExtractor, makeProcessor: { @@ -392,7 +385,6 @@ private func runEngines( parallelism: Int, verbose: Bool, config: FuzzEngineConfig, - coverageStrategy: CoverageStrategy, scheduler: any SchedulerFactory = MutationScheduler.weightedPool(), scheduleBytesExtractor: @escaping @Sendable ((repeat each Input)) -> [UInt8]? = { _ in nil }, makeProcessor: @escaping @Sendable () -> PluginProcessor, @@ -402,16 +394,9 @@ private func runEngines( print("[Fuzz] Running \(parallelism) fuzz engine\(parallelism == 1 ? "" : "s")") } - // Filter compiler-generated edges before any measurement (one-time global - // scan). This is coverage-specific, so it lives in the coordinator/batteries - // layer rather than the signal-agnostic engine. - SanCovCounters.applyEdgeFilter() - if verbose { - let filtered = SanCovCounters.filteredEdgeCount - if filtered > 0 { - print("[Fuzz] Filtered \(filtered) compiler-generated edges") - } - } + // Compiler-generated edges are filtered at COMPILE time by the + // TagCompilerGenerated LLVM pass plugin (see Package.swift), so there is no + // longer a runtime filter scan to run here. var distributedSeeds: [[(repeat each Input)]] = Array(repeating: [], count: parallelism) for (index, seed) in seeds.enumerated() { @@ -426,10 +411,12 @@ private func runEngines( let engine = FuzzEngine( mutators: repeat each mutators, config: config, - makeInstrumentationProviders: { - @Dependency(\.coverageCounters) var client - return [CoverageProvider(evaluator: coverageStrategy.makeEvaluator(), client: client)] - }, + // The scheduler vends its own instrumentation providers — the + // engine names no signal, and coverage is no longer hardcoded + // here. The engine installs only providers matching the + // scheduler's requiredProbes, so a pool-less or cmp-only + // scheduler pays for nothing it didn't ask for. + makeInstrumentationProviders: { scheduler.makeProviders() }, schedulerFactory: scheduler, scheduleBytesExtractor: scheduleBytesExtractor ) @@ -530,34 +517,38 @@ private func mergeResults( ) } -/// Merges multiple corpus snapshots into one, combining coverage. -private func mergeCorpusSnapshots( +/// Merges multiple parallel engines' corpus snapshots into one, deduplicating by +/// input identity. +/// +/// Coverage is no longer a corpus concern, so the dedup key is the entry's +/// encoded input bytes — two engines that retained the same input keep one copy. +/// `CorpusEntry.encode` writes exactly the input array, so identical inputs +/// encode identically; an entry that fails to encode is kept rather than dropped. +/// +/// Internal (not private) so the dedup contract can be unit-tested directly. +func mergeCorpusSnapshots( _ snapshots: [CorpusSnapshot] ) -> CorpusSnapshot { - @Dependency(\.corpusRegistry) var corpusRegistry - guard let first = snapshots.first else { - return CorpusSnapshot( - entries: [] - ) + return CorpusSnapshot(entries: []) } guard snapshots.count > 1 else { return first } - // Create a temporary corpus to deduplicate entries - let mergedCorpus: Corpus = corpusRegistry.getCorpus() + let encoder = JSONEncoder() + var seen = Set() + var merged: [CorpusEntry] = [] - // Use a local signature hash set for deduplication - var signatureHashes = Set() - - // Add all entries - addIfInteresting handles deduplication by coverage for snapshot in snapshots { for entry in snapshot.entries { - _ = mergedCorpus.addIfInteresting(input: entry.input, sparse: entry.sparseCoverage, signatureHashes: &signatureHashes) + if let key = try? encoder.encode(entry), !seen.insert(key).inserted { + continue // an identical input from another engine already kept + } + merged.append(entry) } } - return mergedCorpus.snapshot() + return CorpusSnapshot(entries: merged) } diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/AtomicFeatureSet.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/AtomicFeatureSet.swift new file mode 100644 index 00000000..63830186 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/AtomicFeatureSet.swift @@ -0,0 +1,145 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Lock-free insert-only UInt64 set for ComparisonCoverageStrategy's onCompare +// half — the distinct value-profile features seen this run. +// +// Replaces the per-dispatch SyncBox(NSLock) (Finding 42) the same way +// HitCountAccumulator/BoundarySiteAccumulator do: a FIXED-capacity open- +// addressing table over a flat atomic array, claimed per-slot via CAS. The +// steady-state hit (a feature already present) is one relaxed load + compare. +// + +import Atomics + +/// Open-addressing insert-only set of pre-mixed UInt64 feature hashes. LOCK-FREE +/// and concurrency-safe (inherited child tasks route cmp hooks from several +/// threads into one context — see BoundarySiteAccumulator's note). A fixed buffer +/// of per-slot atomics; `reset`/`snapshot` run at `decide`, and a straggler can at +/// worst lose its own late insert, never corrupt memory. +/// +/// `@unchecked Sendable` because the raw atomic-storage pointer is not +/// automatically `Sendable`. +final class AtomicFeatureSet: @unchecked Sendable { + // `keys[i] == 0` marks an empty slot. The feature value 0 is legal, so it is + // tracked separately by `zeroSeen` rather than stored in the table (same + // split FeatureHashSet uses for its literal-0 sentinel). Capacity is a power + // of two (mask, not modulo) and FIXED for the set's life. + private let keys: UnsafeMutablePointer> + // Claimed slot indices in claim order → O(occupied) snapshot/reset. -1 = unset. + private let occ: UnsafeMutablePointer> + private let occCount = UnsafeAtomic.create(0) + private let zeroSeen = UnsafeAtomic.create(false) + private let overflowed = UnsafeAtomic.create(false) + private let capacity: Int + private let mask: Int + + init(initialCapacity: Int = 8192) { + var cap = 1 + while cap < initialCapacity { cap <<= 1 } + capacity = cap + mask = cap - 1 + keys = .allocate(capacity: cap) + occ = .allocate(capacity: cap) + keys.initialize(repeating: AtomicRep(0), count: cap) + occ.initialize(repeating: AtomicRep(-1), count: cap) + } + + deinit { + keys.deinitialize(count: capacity); keys.deallocate() + occ.deinitialize(count: capacity); occ.deallocate() + occCount.destroy() + zeroSeen.destroy() + overflowed.destroy() + } + + /// True iff the fixed table ever filled and dropped an insert. Diagnostic. + var didOverflow: Bool { overflowed.load(ordering: .relaxed) } + + /// splitmix64 finaliser — cheap, well-distributed. NOT `Swift.Hasher`. The + /// feature is already a mixed hash, but re-mixing decorrelates it from the + /// caller's own bucketing so probe chains stay short. + @inline(__always) + private static func hash(_ x: UInt64) -> UInt64 { + var z = x &+ 0x9E37_79B9_7F4A_7C15 + z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 + z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB + return z ^ (z >> 31) + } + + /// Insert one feature. Idempotent; lock-free; safe to call concurrently. + func insert(_ feature: UInt64) { + if feature == 0 { + zeroSeen.store(true, ordering: .relaxed) + return + } + var i = Int(Self.hash(feature) & UInt64(mask)) + var probes = 0 + while probes <= mask { + let kAtom = UnsafeAtomic(at: keys + i) + let k = kAtom.load(ordering: .relaxed) + if k == feature { return } // already present + if k == 0 { + let (won, _) = kAtom.compareExchange( + expected: 0, desired: feature, ordering: .acquiringAndReleasing) + if won { + let slot = occCount.loadThenWrappingIncrement(ordering: .relaxed) + if slot < capacity { + UnsafeAtomic(at: occ + slot).store(i, ordering: .relaxed) + } + return + } + // Lost the claim: if to OUR feature it's present; else keep probing. + if kAtom.load(ordering: .relaxed) == feature { return } + } + i = (i &+ 1) & mask + probes &+= 1 + } + overflowed.store(true, ordering: .relaxed) + } + + /// The distinct inserted features. Built once per iteration in `decide`. + func snapshot() -> [UInt64] { + let n = min(occCount.load(ordering: .acquiring), capacity) + var out: [UInt64] = [] + out.reserveCapacity(n + 1) + if zeroSeen.load(ordering: .relaxed) { out.append(0) } + var j = 0 + while j < n { + let i = UnsafeAtomic(at: occ + j).load(ordering: .relaxed) + if i >= 0 && i < capacity { + let k = UnsafeAtomic(at: keys + i).load(ordering: .relaxed) + if k != 0 { out.append(k) } + } + j &+= 1 + } + return out + } + + /// Clear every occupied slot, keeping capacity for the next run. O(occupied). + func reset() { + let n = min(occCount.load(ordering: .relaxed), capacity) + var j = 0 + while j < n { + let i = UnsafeAtomic(at: occ + j).load(ordering: .relaxed) + if i >= 0 && i < capacity { + UnsafeAtomic(at: keys + i).store(0, ordering: .relaxed) + UnsafeAtomic(at: occ + j).store(-1, ordering: .relaxed) + } + j &+= 1 + } + occCount.store(0, ordering: .relaxed) + zeroSeen.store(false, ordering: .relaxed) + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/BoundaryDistanceStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/BoundaryDistanceStrategy.swift new file mode 100644 index 00000000..5ffa3ecf --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/BoundaryDistanceStrategy.swift @@ -0,0 +1,190 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Boundary-distance strategy (experimental). The acceptance/publishing half of +// boundary-distance ownership: accept inputs that get a comparison's operands +// CLOSER than seen, and publish the run's per-site minimum |arg1 - arg2| for +// the pool's `featureOwnership` admission to cull on. +// + +extension CoverageStrategy { + /// Comparison-distance strategy: an input is interesting iff it drives some + /// comparison site's operands strictly closer together than this engine has + /// seen (lower `|arg1 - arg2|`), OR it covers a new edge (union with + /// `.newEdge`). It publishes the run's per-site minimum distance as its + /// pool vocabulary, so `PoolAdmission.featureOwnership` retains, per + /// site, the single closest witness. + /// + /// Unlike `.comparisonCoverage` (value-profile acceptance, which keeps every + /// *novel* distance — including ones FARTHER from the boundary — and bloats + /// the corpus), acceptance here is monotone: only a strict improvement + /// counts. The metric is the absolute numeric difference, not Hamming + /// distance, so it is a true gradient on the integer line (`8` vs `7` is + /// Hamming-4 but numeric-1). + /// + /// Requires the target to be built with `-sanitize-coverage=…,trace-cmp`; + /// without it the comparison channel stays silent and this degrades to + /// plain edge novelty. + public static var boundaryDistance: CoverageStrategy { + CoverageStrategy(makeEngine: { makeBoundaryEngine() }) + } + + /// The comparison channel ALONE, without the edge-coverage union: an input + /// is interesting iff it drives some comparison site strictly closer than + /// seen. This is the mix-and-match building block — compose it with any edge + /// strategy to add the boundary-distance signal without double-counting + /// edges, e.g. `.pathTrie.combined(with: .boundaryDistanceOnly)`. (Plain + /// `.boundaryDistance` is exactly `.newEdge` unioned with this.) Publishes + /// the run's per-site minimum distance; requires a target built with + /// `-sanitize-coverage=…,trace-cmp` (else the cmp channel stays silent). + public static var boundaryDistanceOnly: CoverageStrategy { + CoverageStrategy(makeEngine: { makeBoundaryOnlyEngine() }) + } +} + +/// Overflow-safe absolute difference of two comparison operands. +/// +/// Computes the wrapped difference ONCE and conditionally negates it, rather +/// than evaluating both `a &- b` and `b &- a` and selecting. `a &- b` and +/// `b &- a` are two's-complement negations of each other, so `b - a == 0 &- (a &- b)`. +/// On arm64 this lowers to `subs` + `cneg` (2 instructions, branchless) vs the +/// `sub` + `subs` + `csel` (3) the two-subtraction ternary emits — and it's on +/// the per-comparison hot path. +private func absoluteDifference(_ a: UInt64, _ b: UInt64) -> UInt64 { + let d = a &- b + return a < b ? 0 &- d : d +} + +/// The comparison-distance channel without the edge-coverage union (see +/// `.boundaryDistanceOnly`). Identical to `makeBoundaryEngine` minus the +/// `seenEdges` union in `decide`: novelty comes solely from a strictly closer +/// per-site distance. Still publishes the run's per-site distances every +/// iteration so a composed edge-novel input can also claim its boundaries. +private func makeBoundaryOnlyEngine() -> CoverageEngine { + let accumulator = BoundarySiteAccumulator() + + struct DistanceState { + var bestDistance: [UInt64: UInt64] = [:] + var lastAccepted: [BoundarySiteAccumulator.Site] = [] + } + let state = UncheckedBox(DistanceState()) + + let onCompare: @Sendable (UInt, UInt64, UInt64, UInt32) -> Void = { pc, arg1, arg2, _ in + accumulator.record(pc: UInt64(truncatingIfNeeded: pc), distance: absoluteDifference(arg1, arg2)) + } + let onReset: @Sendable () -> Void = { accumulator.reset() } + let distancesClosure: @Sendable () -> [UInt64: UInt64] = { + state.update { st in + var d: [UInt64: UInt64] = [:] + d.reserveCapacity(st.lastAccepted.count) + for s in st.lastAccepted { d[s.pc] = s.distance } + return d + } + } + + return CoverageEngine( + onCompare: onCompare, + onReset: onReset, + boundaryDistances: distancesClosure + ) { _ in + let sites = accumulator.snapshot() + accumulator.reset() + return state.update { st in + var interesting = false + for s in sites where s.distance < (st.bestDistance[s.pc] ?? .max) { + st.bestDistance[s.pc] = s.distance + interesting = true + } + st.lastAccepted = sites + return interesting + } + } +} + +private func makeBoundaryEngine() -> CoverageEngine { + // The per-comparison hot path writes into `accumulator` (a concrete + // open-addressing PC -> minDistance map); the engine-lifetime acceptance + // oracle lives in `state`, touched only once per iteration in + // `decide`/`distances`. Splitting them keeps Swift.Dictionary + generic + // `SyncBox.update` off the comparison hot path (Finding 41). + let accumulator = BoundarySiteAccumulator() + + struct DistanceState { + /// Engine-lifetime lowest distance ever seen per site — the monotone + /// acceptance oracle. + var bestDistance: [UInt64: UInt64] = [:] + /// Engine-lifetime edges, for the edge-coverage union. + var seenEdges = EdgeUnionBitmap() + /// The last accepted run's per-site closest approach, handed to the pool. + var lastAccepted: [BoundarySiteAccumulator.Site] = [] + } + let state = UncheckedBox(DistanceState()) + + let onCompare: @Sendable (UInt, UInt64, UInt64, UInt32) -> Void = { pc, arg1, arg2, _ in + let site = UInt64(truncatingIfNeeded: pc) + accumulator.record(pc: site, distance: absoluteDifference(arg1, arg2)) + } + let onReset: @Sendable () -> Void = { + accumulator.reset() + } + let distancesClosure: @Sendable () -> [UInt64: UInt64] = { + state.update { st in + var d: [UInt64: UInt64] = [:] + d.reserveCapacity(st.lastAccepted.count) + for s in st.lastAccepted { d[s.pc] = s.distance } + return d + } + } + + return CoverageEngine( + onCompare: onCompare, + onReset: onReset, + boundaryDistances: distancesClosure + ) { coverage in + // Snapshot the run's edges BEFORE any bookkeeping below: this closure + // runs in (gated) instrumented code, so its own dict work fires edges + // that land in the map. Materializing first caches the snapshot the + // edge union (and storage) read, so our bookkeeping can't pollute it — + // the same first-read discipline `.newEdge` follows. + let sparse = coverage.materialized() + // Drain the per-comparison accumulator once (off the hot path), then + // reset it for the next run. snapshot/reset fire no comparisons of their + // own (this module is uninstrumented), so they cannot pollute `sparse`. + let sites = accumulator.snapshot() + accumulator.reset() + return state.update { st in + var interesting = false + + // Edge-coverage union: never weaker than .newEdge. + if let sparse { + for edge in sparse.indices where st.seenEdges.insert(edge) { + interesting = true + } + } + + // Monotone distance novelty: any site driven strictly closer. + for s in sites { + if s.distance < (st.bestDistance[s.pc] ?? .max) { + st.bestDistance[s.pc] = s.distance + interesting = true + } + } + + // Publish this run's per-site closest approach regardless of WHY it + // was accepted, so an edge-novel input can still claim boundaries. + st.lastAccepted = sites + return interesting + } + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/BoundarySiteAccumulator.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/BoundarySiteAccumulator.swift new file mode 100644 index 00000000..1a818c6a --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/BoundarySiteAccumulator.swift @@ -0,0 +1,259 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Concrete per-run accumulator for the boundary comparison hot path. +// +// Profiling the cmp dispatch (notebook Finding 41) found the per-comparison +// cost was NOT the lock but (a) Swift.Dictionary's SipHash + copy-on-write ARC +// and (b) unspecialized generic metadata. This type removed both with an +// open-addressing map over FLAT CONCRETE arrays of trivial element types. +// +// Finding 41d then found the os_unfair_lock — kept because task-inherited +// child tasks route cmp hooks from several threads into the SAME accumulator — +// had itself become the #1 cost (~26% of the cmp channel): the lock/unlock pair +// is an out-of-line libsystem CALL per comparison. The lock came out by making +// `record` LOCK-FREE over a FIXED-capacity table (the realloc-under-readers +// race was the only reason a lock was required), updated with per-slot atomics. +// +// Findings 45/46/47 then removed the sign dimension entirely: the A/B showed +// the boundary sign vocabulary bought zero bug-finding over the distance +// gradient, so the accumulator now stores ONLY the per-site minimum distance — +// one atomic word per bucket, no packing, no sign, no near-window. The +// steady-state cost is a single relaxed load and a compare. +// + +import Atomics + +/// Open-addressing PC → minDistance map specialised for the per-comparison hot +/// path. +/// +/// LOCK-FREE and concurrency-safe. Coverage contexts are keyed by Swift task and +/// INHERITED by child tasks (`g_coverage_inheritance_key` in SanCovHooks.c), so a +/// property that spawns concurrent work (`async let`, `TaskGroup`) routes edge +/// AND cmp hooks from several threads into the SAME context — and thus the same +/// accumulator — at once. (The edge map handles this with an atomic CAS; +/// `.pathTrie` locks its trie.) Here every shared field is a per-slot atomic over +/// a FIXED buffer, so concurrent `record`s never tear and never touch reallocated +/// memory. `reset`/`snapshot` run at `decide`; a straggler child task racing them +/// can at worst lose its own (unwanted) late write — never corrupt memory. +/// +/// `@unchecked Sendable` because the raw atomic-storage pointers are not +/// automatically `Sendable`. +final class BoundarySiteAccumulator: @unchecked Sendable { + /// One occupied slot's snapshot, handed to `decide` once per iteration: + /// a comparison site and the smallest `|arg1 - arg2|` the run drove it to. + struct Site { + var pc: UInt64 + var distance: UInt64 + } + + // ONE interleaved buffer of ATOMIC storage (Array-of-Structs): `2 * capacity` + // words, where bucket `i`'s KEY is at word `2*i` and its minimum-distance + // VALUE is at `2*i + 1`. The two words of a bucket are adjacent (a 16-byte + // span), so a steady-state hit reads the key and — on a match — its value + // from the SAME cache line: one miss per comparison, not the two + // separate-array misses the old Structure-of-Arrays layout cost (`bucket` is + // hash-derived, so each access is an effectively random table index). A KEY + // of 0 marks an empty bucket — a comparison-site PC is + // `__builtin_return_address`, never 0. The VALUE word starts at `.max` (no + // distance recorded yet) so the compare-then-CAS min works uniformly for the + // claiming writer and every later updater (no claim/min race). Capacity is a + // power of two so the hash maps with a mask, not a modulo, and is FIXED for + // the accumulator's life. + private let cells: UnsafeMutablePointer> + // Occupied bucket indices, in claim order, so `snapshot`/`reset` are + // O(occupied) instead of O(capacity). Written only by the thread that wins a + // bucket's key-claim CAS; `-1` marks an entry not yet published. + private let occ: UnsafeMutablePointer> + + /// Atomic handle for bucket `i`'s KEY word (`cells[2*i]`). + @inline(__always) + private func keyWord(_ i: Int) -> UnsafeAtomic { + UnsafeAtomic(at: cells + (i &<< 1)) + } + /// Atomic handle for bucket `i`'s packed VALUE word (`cells[2*i + 1]`), + /// adjacent to its key so the two share a cache line. + @inline(__always) + private func valueWord(_ i: Int) -> UnsafeAtomic { + UnsafeAtomic(at: cells + ((i &<< 1) &+ 1)) + } + private let occCount = UnsafeAtomic.create(0) + // Set once if the table ever fills and a record is dropped (best-effort + // signal; surfaced for diagnostics/tests). Real workloads have far fewer + // distinct comparison sites than `capacity`, so this stays false. + private let overflowed = UnsafeAtomic.create(false) + private let capacity: Int + private let mask: Int + + init(initialCapacity: Int = 8192) { + var cap = 1 + while cap < initialCapacity { cap <<= 1 } + capacity = cap + mask = cap - 1 + cells = .allocate(capacity: cap * 2) + occ = .allocate(capacity: cap) + // Interleave: even words = keys (empty sentinel 0), odd words = packed + // values (min sentinel .max). Bulk-initialize to 0, then raise the value + // words to the sentinel. + cells.initialize(repeating: AtomicRep(0), count: cap * 2) + var j = 0 + while j < cap { + cells[j * 2 + 1] = AtomicRep(UInt64.max) + j &+= 1 + } + occ.initialize(repeating: AtomicRep(-1), count: cap) + } + + deinit { + cells.deinitialize(count: capacity * 2); cells.deallocate() + occ.deinitialize(count: capacity); occ.deallocate() + occCount.destroy() + overflowed.destroy() + } + + /// True iff the fixed table ever filled and dropped a record. Diagnostic. + var didOverflow: Bool { overflowed.load(ordering: .relaxed) } + + /// splitmix64 finaliser — a cheap, well-distributed mix of the PC. NOT + /// `Swift.Hasher` (per-process seeded + SipHash, the cost we are removing). + @inline(__always) + private static func hash(_ x: UInt64) -> UInt64 { + var z = x &+ 0x9E37_79B9_7F4A_7C15 + z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 + z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB + return z ^ (z >> 31) + } + + /// Lower bucket `i`'s value to `distance` if it is a closer approach. The min + /// is a relaxed load + early-out, then a weak-CAS loop only when the distance + /// actually improves (rare after a site's first few hits) — so the + /// steady-state cost is a single relaxed load and a compare, no + /// read-modify-write and no call. + @inline(__always) + private func updateSlot(_ i: Int, distance: UInt64) { + let value = valueWord(i) + var cur = value.load(ordering: .relaxed) + while distance < cur { + let (done, original) = value.weakCompareExchange( + expected: cur, desired: distance, ordering: .relaxed) + if done { break } + cur = original + } + } + + /// Record one comparison: keep, for `pc`, the minimum `distance` + /// (`|arg1 - arg2|`) any hit drove it to this run. Lock-free; safe to call + /// concurrently from inherited child tasks. + /// + /// `@inline(__always)` because the module builds non-WMO (one `.o` per + /// source file), so without it this stays an out-of-line cross-file call + /// from `onCompare` — a tail-branch plus a prologue/epilogue on the + /// per-comparison hot path. It has a single hot caller (the boundary + /// engine's `onCompare` closure), so folding it in costs no code size. The + /// probe-loop helpers (`keyWord`/`valueWord`/`hash`/`updateSlot`) are + /// already inlined into this body; this carries the whole thing into the + /// closure. + @inline(__always) + func record(pc: UInt64, distance: UInt64) { + // Open-addressing linear probe: start at this PC's home bucket and walk + // forward (wrapping with `mask`) until we find the PC, claim an empty + // slot for it, or exhaust the table. The bound runs at most `mask + 1` + // times = one full pass over the table. `mask` is hoisted to a local so + // the loop condition doesn't reload the stored property each iteration + // (the atomic accesses below are optimizer barriers that would otherwise + // force a reread of `self`). + let mask = self.mask + var bucket = Int(Self.hash(pc) & UInt64(mask)) + var probeCount = 0 + while probeCount <= mask { + let keyCell = keyWord(bucket) + let occupant = keyCell.load(ordering: .relaxed) + + // This bucket already belongs to our PC (the steady-state case): + // fold this hit into its running minimum and we're done. + if occupant == pc { + updateSlot(bucket, distance: distance) + return + } + + // Empty bucket: try to claim it for our PC with a single CAS. + if occupant == 0 { + let (claimedByUs, _) = keyCell.compareExchange( + expected: 0, desired: pc, ordering: .acquiringAndReleasing) + if claimedByUs { + updateSlot(bucket, distance: distance) + // Append this bucket to the occupied-index list so snapshot + // and reset are O(occupied) instead of O(capacity). + let occupiedIndex = occCount.loadThenWrappingIncrement(ordering: .relaxed) + if occupiedIndex < capacity { + UnsafeAtomic(at: occ + occupiedIndex).store(bucket, ordering: .relaxed) + } + return + } + // We lost the claim race to a concurrent (inherited-child-task) + // writer. If that writer claimed this bucket for OUR PC too, + // update it in place; otherwise it took it for some other PC, so + // keep probing past it. + if keyCell.load(ordering: .relaxed) == pc { + updateSlot(bucket, distance: distance) + return + } + } + + // Bucket taken by a different PC — advance to the next one. + bucket = (bucket &+ 1) & mask + probeCount &+= 1 + } + // Table full — drop this record (best-effort signal). Never happens for + // real workloads (distinct cmp sites ≪ capacity). + overflowed.store(true, ordering: .relaxed) + } + + /// The occupied slots. Built once per iteration in `decide`; off the hot path. + func snapshot() -> [Site] { + let n = min(occCount.load(ordering: .acquiring), capacity) + var out: [Site] = [] + out.reserveCapacity(n) + var j = 0 + while j < n { + let i = UnsafeAtomic(at: occ + j).load(ordering: .relaxed) + if i >= 0 && i < capacity { + let k = keyWord(i).load(ordering: .relaxed) + if k != 0 { + let distance = valueWord(i).load(ordering: .relaxed) + out.append(Site(pc: k, distance: distance)) + } + } + j &+= 1 + } + return out + } + + /// Clear every occupied slot, keeping the allocated capacity for the next + /// run. Touches only the slots claimed this iteration (O(occupied)). + func reset() { + let n = min(occCount.load(ordering: .relaxed), capacity) + var j = 0 + while j < n { + let i = UnsafeAtomic(at: occ + j).load(ordering: .relaxed) + if i >= 0 && i < capacity { + keyWord(i).store(0, ordering: .relaxed) + valueWord(i).store(UInt64.max, ordering: .relaxed) + UnsafeAtomic(at: occ + j).store(-1, ordering: .relaxed) + } + j &+= 1 + } + occCount.store(0, ordering: .relaxed) + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/ComparisonCoverageStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/ComparisonCoverageStrategy.swift new file mode 100644 index 00000000..2239adc0 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/ComparisonCoverageStrategy.swift @@ -0,0 +1,119 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Comparison-coverage (value-profile / cmplog) strategy. +// +// Edge coverage is blind to bugs whose distinguishing condition is a DATA +// relationship — a de Bruijn `i < c`, a magic-byte compare — because a +// near-miss input and a witnessing input trace the SAME edges. This strategy +// adds the signal libFuzzer's value profile (and RedQueen / laf-intel) use: +// for every instrumented comparison it records the pair +// `(comparison-site PC, popcount(arg1 ^ arg2))`. As an input nears a boundary +// the Hamming distance of the operands changes, so new distances at a known +// site keep surfacing as novelty — a gradient that drives mutation toward the +// boundary even when the edge set never changes. Unions with edge coverage so +// it is never weaker than `.newEdge`. +// + +extension CoverageStrategy { + /// Value-profile / comparison-coverage strategy: an input is interesting iff + /// it produces a `(comparison-site, Hamming-distance-of-operands)` pair this + /// engine hasn't seen, OR it covers a new edge (union with `.newEdge`). + /// + /// Requires the target to be built with `-sanitize-coverage=…,trace-cmp` + /// (in addition to the usual `edge,pc-table`) so the comparison hooks fire; + /// without trace-cmp the comparison channel stays silent and this degrades + /// to plain edge novelty. + /// + /// Publishes no culling vocabulary — the pool culls on covered edges. A + /// value-profile vocabulary would equal this strategy's own acceptance + /// criterion, and a culling vocabulary equal to acceptance is a tautology + /// that silently disables culling (see `.hitCountBuckets`). + /// + /// - Warning: measured to UNDER-perform `.newEdge` on the de Bruijn + /// `shift_var_leq` mutant (stlc): accepting every new `(site, distance)` + /// pair floods the corpus and dilutes mutation energy, dropping the solve + /// rate (4/8 vs newEdge's 8/8 at a 20s cap) even though it is a strict + /// superset of newEdge's acceptance. The comparison operands are better + /// spent on input-to-state MUTATION than on acceptance. Kept as the + /// measured baseline for that future work; not recommended as a default. + public static var comparisonCoverage: CoverageStrategy { + CoverageStrategy(makeEngine: { makeComparisonCoverageEngine() }) + } +} + +/// One value-profile feature: the comparison site mixed with the Hamming +/// distance of its operands (FNV-1a). Two comparisons at the same site with +/// operands the same distance apart collide deliberately — that is the feature. +private func comparisonFeature(pc: UInt, hammingDistance: Int) -> UInt64 { + var h: UInt64 = 1469598103934665603 // FNV-1a offset basis + h = (h ^ UInt64(truncatingIfNeeded: pc)) &* 1099511628211 + h = (h ^ UInt64(hammingDistance)) &* 1099511628211 + return h +} + +/// Comparison-coverage engine. `onCompare` is the measurement half (hash each +/// comparison into this run's value-profile feature set); `decide` the +/// judgement half (interesting iff some feature or some edge is new to this +/// engine). The novelty oracle is the STRATEGY's own per-engine state. +private func makeComparisonCoverageEngine() -> CoverageEngine { + // Per-COMPARISON half (onCompare/onReset): a lock-free feature set — the + // SyncBox here was a per-dispatch NSLock (Finding 42, same shape as + // hitCountBuckets). Engine-lifetime half (decide): seenFeatures + seenEdges, + // touched ONLY in decide, which the fuzz loop calls serially on one thread per + // engine — so a plain holder needs no lock. onCompare never reads them, so + // there is no onCompare/decide race; stragglers race only the atomic set. + let currentRun = AtomicFeatureSet() + + /// Engine-lifetime novelty oracle. Decide-only; a reference so the @Sendable + /// decide closure can mutate it, @unchecked Sendable because decide is + /// serialized per engine. + final class EngineSeen: @unchecked Sendable { + /// Keys are pre-mixed comparisonFeature hashes → no-SipHash set (41n). + var features = FeatureHashSet() + /// Engine-lifetime edges, for the edge-coverage union. + var edges = EdgeUnionBitmap() + } + let seen = EngineSeen() + + return CoverageEngine( + onCompare: { pc, arg1, arg2, _ in + let distance = (arg1 ^ arg2).nonzeroBitCount + let feature = comparisonFeature(pc: pc, hammingDistance: distance) + currentRun.insert(feature) + }, + onReset: { + currentRun.reset() + } + ) { coverage in + defer { currentRun.reset() } + var interesting = false + + // Value-profile novelty: any comparison feature new to this engine. + for feature in currentRun.snapshot() where seen.features.insert(feature) { + interesting = true + } + + // Edge-coverage union: never weaker than .newEdge. The snapshot is + // the one the evaluator reuses for storage, so reading it is free + // for accepted inputs (and the cost of the union for rejected ones). + if let sparse = coverage.materialized() { + for edge in sparse.indices where seen.edges.insert(edge) { + interesting = true + } + } + + return interesting + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageEngine.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageEngine.swift index 0b178644..31170223 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageEngine.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageEngine.swift @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FuzzCore - // The per-engine bundle a coverage strategy is built from. // @@ -38,8 +36,19 @@ public struct CoverageEngine: Sendable { /// on first hits (loop immunity, like `.pathTrie`) get it for free. let onEdge: (@Sendable (_ edge: UInt32, _ isFirstHit: Bool) -> Void)? + /// Called for every instrumented comparison that routes to this engine's + /// measurement context: the comparison site's PC, both operands, and the + /// operand width in bytes. This is the trace-cmp / value-profile channel — + /// it gives a gradient (e.g. `popcount(arg1 ^ arg2)` as an input nears a + /// boundary) that edge coverage is blind to. Independent of `onEdge`; a + /// strategy may use both. Because Swift instruments its own runtime + /// comparisons, a strategy MUST key on `pc`. `nil` (the default) leaves the + /// cmp channel dormant (no per-comparison overhead). + let onCompare: (@Sendable (_ pc: UInt, _ arg1: UInt64, _ arg2: UInt64, _ size: UInt32) -> Void)? + /// Called when the engine's coverage resets between iterations, so - /// per-iteration state starts each run clean. + /// per-iteration state starts each run clean. Routed to the engine's edge + /// observer when one is attached, otherwise to its comparison observer. let onReset: (@Sendable () -> Void)? /// The judgement half: decides per iteration whether the run's coverage @@ -52,24 +61,33 @@ public struct CoverageEngine: Sendable { let decide: CoverageDecision /// The strategy's culling vocabulary for the LAST accepted decision — - /// the features the mutation pool's ledger accounts ownership over. The - /// only built-in that publishes one is `.pathTrie(gramLength:)` (sliding - /// k-grams of the ordered first-hit path); called only after `decide` - /// returns `true`, inside the same gated window. `nil` (the default for - /// every other strategy, including the default `.pathTrie` and - /// `.hitCountBuckets`) means the pool falls back to the covered edge - /// indices. + /// the features the mutation pool's ledger accounts ownership over + /// (`.pathTrie`: sliding k-grams of the ordered first-hit path; + /// `.hitCountBuckets`: (edge, bucket) pairs). Called only after `decide` + /// returns `true`, inside the same gated window. `nil` (the default) + /// means the pool falls back to the covered edge indices. let features: (@Sendable () -> [UInt64])? + /// The per-comparison-site distances of the LAST accepted decision: site + /// `pc` → the lowest `|arg1 - arg2|` the run drove it to. The vocabulary + /// `PoolAdmission.featureOwnership` culls over. Called only after + /// `decide` returns `true`, inside the same gated window as `features`. + /// `nil` (the default) means the run publishes no boundary distances. + let boundaryDistances: (@Sendable () -> [UInt64: UInt64])? + public init( onEdge: (@Sendable (UInt32, Bool) -> Void)? = nil, + onCompare: (@Sendable (UInt, UInt64, UInt64, UInt32) -> Void)? = nil, onReset: (@Sendable () -> Void)? = nil, features: (@Sendable () -> [UInt64])? = nil, + boundaryDistances: (@Sendable () -> [UInt64: UInt64])? = nil, _ decide: @escaping CoverageDecision ) { self.onEdge = onEdge + self.onCompare = onCompare self.onReset = onReset self.features = features + self.boundaryDistances = boundaryDistances self.decide = decide } } diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageProbe.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageProbe.swift index 9dec19c7..cad49bcf 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageProbe.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageProbe.swift @@ -17,6 +17,7 @@ // per-execution verdict view through the generic `RawExecutionContext` seam. // +import Foundation import FuzzCore import SanCovHooks @@ -37,10 +38,19 @@ public struct CoverageVerdict { /// publishes none — the scheduler then falls back to the covered edge /// indices. public let features: [UInt64]? - - public init(coverage: SparseCoverage?, features: [UInt64]? = nil) { + /// The run's per-comparison-site distances (`pc` → lowest `|arg1 - arg2|`) + /// when the strategy publishes them (`.boundaryDistance`); `nil` otherwise. + /// The scheduler's boundary-distance ownership ledger culls over these. + public let boundaryDistances: [UInt64: UInt64]? + + public init( + coverage: SparseCoverage?, + features: [UInt64]? = nil, + boundaryDistances: [UInt64: UInt64]? = nil + ) { self.coverage = coverage self.features = features + self.boundaryDistances = boundaryDistances } } @@ -69,6 +79,14 @@ final class CoverageProbe: InstrumentationProbe { /// `tearDown`. `nil` before setup / after teardown. private var context: SanCovCounters.MeasurementContext? + /// Per-engine input-to-state dictionary, non-nil only when I2S is enabled + /// and the strategy left the comparison-recorder slot free. Populated by an + /// observer attached in `setUp`, bound as `ComparisonDictionary.current` for + /// the loop in `withCampaignScope`, and sampled by the numeric mutators. The + /// measurement context owns the recording observer, so I2S rides the same + /// per-engine context as coverage rather than allocating a second one. + private var i2sDictionary: ComparisonDictionary? + /// Union of edge indices across every run this probe judged interesting, /// accumulated from the verdicts (not from retained entries, so pool culling /// does not shrink the reported total coverage). Surfaced at campaign end for @@ -90,6 +108,29 @@ final class CoverageProbe: InstrumentationProbe { let ctx = client.beginMeasurement() context = ctx evaluator.setup?(ctx) + + // Input-to-state (I2S): when enabled AND the strategy left the cmp + // recorder slot free (i.e. not `.comparisonCoverage`, which uses it + // itself), attach an observer that feeds each instrumented comparison's + // operands into a per-engine dictionary the numeric mutators sample + // from — so mutation can jump straight to a value a comparison cared + // about (magic constants, boundary cutoffs). Opt-in via the task-local + // or the PTK_INPUT_TO_STATE env var; only bites on targets built with + // the cmp channel. `getenv` reads the live environ, not ProcessInfo's + // snapshot. The dictionary is bound for the loop in `withCampaignScope`. + let i2sEnabled = + ComparisonDictionary.inputToStateEnabled || getenv("PTK_INPUT_TO_STATE") != nil + if i2sEnabled, sancov_context_get_cmp_recorder_data(ctx.rawContext) == nil { + let dict = ComparisonDictionary() + i2sDictionary = dict + SanCovCounters.attachComparisonObserver( + ComparisonObserver(onCompare: { _, arg1, arg2, _ in + dict.record(arg1) + dict.record(arg2) + }), + to: ctx + ) + } } func tearDown() { @@ -108,9 +149,15 @@ final class CoverageProbe: InstrumentationProbe { // inside the test body are attributed to this engine's context via the // inheritance task-local, set once for the whole loop. let bits = context.inheritanceHandle - try await CoverageInheritance.$context.withValue(bits) { - CoverageInheritance.captureKeyIfNeeded(contextBits: bits) - try await body() + // Bind the I2S dictionary for the whole loop in the engine's task so + // every mutate/generate call sees it (the task-local follows thread + // hops). `nil` when I2S is disabled — the numeric mutators' I2S branch + // then stays inert. + try await ComparisonDictionary.$current.withValue(i2sDictionary) { + try await CoverageInheritance.$context.withValue(bits) { + CoverageInheritance.captureKeyIfNeeded(contextBits: bits) + try await body() + } } } @@ -126,7 +173,9 @@ final class CoverageProbe: InstrumentationProbe { if let coverage = acceptance?.sparse { coveredIndices.formUnion(coverage.indices) } - return CoverageVerdict(coverage: acceptance?.sparse, features: acceptance?.features) + return CoverageVerdict( + coverage: acceptance?.sparse, features: acceptance?.features, + boundaryDistances: acceptance?.boundaryDistances) } /// Surface the union of every interesting run's covered edges to the diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategy.swift index 7a78aa1a..1a32b1ec 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategy.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategy.swift @@ -112,13 +112,29 @@ extension CoverageStrategy { func makeEvaluator() -> CoverageEvaluator { let engine = makeEngine() // No hooks → nothing to attach: a cleared recorder field already - // means "default recording". - let setup: CoverageStrategySetup? = (engine.onEdge != nil || engine.onReset != nil) + // means "default recording". An edge observer carries onReset when one + // is attached; otherwise a lone comparison observer carries it (a bare + // onReset with no measurement hook still rides an edge observer, the + // historical behavior). + let attachEdge = engine.onEdge != nil || (engine.onReset != nil && engine.onCompare == nil) + let attachCompare = engine.onCompare != nil + let setup: CoverageStrategySetup? = (attachEdge || attachCompare) ? { context in - SanCovCounters.attachObserver( - EdgeObserver(onEdge: engine.onEdge ?? { _, _ in }, onReset: engine.onReset), - to: context - ) + if attachEdge { + SanCovCounters.attachObserver( + EdgeObserver(onEdge: engine.onEdge ?? { _, _ in }, onReset: engine.onReset), + to: context + ) + } + if let onCompare = engine.onCompare { + // The edge observer already owns onReset when one was + // attached; route it to the comparison observer only when + // it is the sole observer. + SanCovCounters.attachComparisonObserver( + ComparisonObserver(onCompare: onCompare, onReset: attachEdge ? nil : engine.onReset), + to: context + ) + } } : nil return CoverageEvaluator(setup: setup, evaluate: { context, coverageClient in @@ -139,6 +155,8 @@ extension CoverageStrategy { // The vocabulary is collected inside the same gated window as the // decision — its closure reads the same engine state. let features: [UInt64]? = interesting ? engine.features.map { $0() } : nil + let boundaryDistances: [UInt64: UInt64]? = + interesting ? engine.boundaryDistances.map { $0() } : nil if gated { sancov_observer_exit() } guard interesting else { return nil @@ -151,7 +169,9 @@ extension CoverageStrategy { guard let sparse = coverage.materialized() else { return nil } - return CoverageAcceptance(sparse: sparse, features: features) + return CoverageAcceptance( + sparse: sparse, features: features, + boundaryDistances: boundaryDistances) }) } } @@ -165,6 +185,19 @@ struct CoverageAcceptance { /// strategy has no vocabulary of its own (the pool falls back to the /// covered edge indices). let features: [UInt64]? + /// The run's per-comparison-site distances (`pc` → lowest `|arg1 - arg2|`), + /// `nil` when the strategy publishes none. + let boundaryDistances: [UInt64: UInt64]? + + init( + sparse: SparseCoverage, + features: [UInt64]?, + boundaryDistances: [UInt64: UInt64]? = nil + ) { + self.sparse = sparse + self.features = features + self.boundaryDistances = boundaryDistances + } } /// A coverage-interestingness decision: pure judgement over the edges the run diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategyComposition.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategyComposition.swift new file mode 100644 index 00000000..be65bef2 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/CoverageStrategyComposition.swift @@ -0,0 +1,143 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Composing coverage strategies: union several strategies into one so the +// comparison channel can mix-and-match with edge strategies (and edge +// strategies with each other). +// + +extension CoverageStrategy { + /// Compose strategies into one whose acceptance is their **union** — an + /// input is interesting iff ANY substrategy finds it interesting — and whose + /// pool vocabularies are the namespaced union of the substrategies'. + /// + /// Each substrategy keeps its own per-engine state: every substrategy's + /// measurement hooks (`onEdge`/`onCompare`/`onReset`) run each iteration, and + /// every substrategy's decision runs (none is short-circuited, so each + /// updates its own novelty oracle) before the results are OR-ed. The + /// published `features` are namespaced per substrategy (see + /// `namespacedFeature`) so two strategies' raw vocabularies can never collide + /// in the pool's shared ownership space; `boundaryDistances` are merged + /// per-site by the closer (lower) value. + /// + /// The canonical use is mixing the comparison channel with an edge strategy, + /// e.g. `.pathTrie.combined(with: .boundaryDistanceOnly)` — pair it with + /// `PoolAdmission.featureOwnership`, which culls over both the + /// (namespaced) features and the boundary distances. + public static func compose(_ strategies: [CoverageStrategy]) -> CoverageStrategy { + precondition(!strategies.isEmpty, "CoverageStrategy.compose requires at least one strategy") + guard strategies.count > 1 else { return strategies[0] } + return CoverageStrategy(makeEngine: { + mergeEngines(strategies.map { $0.makeEngine() }) + }) + } + + /// Union this strategy with another (see ``compose(_:)``). + public func combined(with other: CoverageStrategy) -> CoverageStrategy { + .compose([self, other]) + } +} + +/// Mix a feature value into a per-substrategy namespace so the same raw value +/// emitted by two different substrategies maps to two distinct features in the +/// pool's shared ownership space. A SplitMix64 finalizer on `value + salt·φ`: +/// deterministic, and effectively injective (cross-namespace collision is as +/// unlikely as the hash collisions the feature space already tolerates). +func namespacedFeature(_ value: UInt64, salt: UInt64) -> UInt64 { + var x = value &+ (salt &* 0x9E37_79B9_7F4A_7C15) + x = (x ^ (x >> 30)) &* 0xBF58_476D_1CE4_E5B9 + x = (x ^ (x >> 27)) &* 0x94D0_49BB_1331_11EB + return x ^ (x >> 31) +} + +/// Merge several per-engine bundles into one (see ``CoverageStrategy/compose(_:)``). +private func mergeEngines(_ engines: [CoverageEngine]) -> CoverageEngine { + // Measurement: run every present hook. Capture only the non-nil ones so the + // merged hook is nil (dormant, no per-event cost) when no substrategy uses + // that channel — preserving e.g. "no cmp recorder attached" for edge-only + // compositions. + let edgeHooks = engines.compactMap(\.onEdge) + let cmpHooks = engines.compactMap(\.onCompare) + let resetHooks = engines.compactMap(\.onReset) + + let onEdge: (@Sendable (UInt32, Bool) -> Void)? + if edgeHooks.isEmpty { + onEdge = nil + } else { + onEdge = { edge, first in for h in edgeHooks { h(edge, first) } } + } + + let onCompare: (@Sendable (UInt, UInt64, UInt64, UInt32) -> Void)? + if cmpHooks.isEmpty { + onCompare = nil + } else { + onCompare = { pc, a, b, s in for h in cmpHooks { h(pc, a, b, s) } } + } + + let onReset: (@Sendable () -> Void)? + if resetHooks.isEmpty { + onReset = nil + } else { + onReset = { for h in resetHooks { h() } } + } + + // Vocabularies. Features are namespaced by the substrategy's index; distances + // are merged per site by the closer value. + let featureClosures: [(UInt64, @Sendable () -> [UInt64])] = + engines.enumerated().compactMap { i, e in e.features.map { (UInt64(i), $0) } } + let features: (@Sendable () -> [UInt64])? + if featureClosures.isEmpty { + features = nil + } else { + features = { + var out: [UInt64] = [] + for (salt, produce) in featureClosures { + for v in produce() { out.append(namespacedFeature(v, salt: salt)) } + } + return out + } + } + + let distanceClosures = engines.compactMap(\.boundaryDistances) + let boundaryDistances: (@Sendable () -> [UInt64: UInt64])? + if distanceClosures.isEmpty { + boundaryDistances = nil + } else { + boundaryDistances = { + var merged: [UInt64: UInt64] = [:] + for produce in distanceClosures { + for (pc, d) in produce() { merged[pc] = min(merged[pc] ?? .max, d) } + } + return merged + } + } + + // Judgement: run EVERY decision (so each substrategy updates its own novelty + // oracle — no short-circuit) and OR the results. + let decides = engines.map(\.decide) + let decide: CoverageDecision = { coverage in + var interesting = false + for d in decides where d(coverage) { interesting = true } + return interesting + } + + return CoverageEngine( + onEdge: onEdge, + onCompare: onCompare, + onReset: onReset, + features: features, + boundaryDistances: boundaryDistances, + decide + ) +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/EdgeUnionBitmap.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/EdgeUnionBitmap.swift new file mode 100644 index 00000000..8a08717f --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/EdgeUnionBitmap.swift @@ -0,0 +1,60 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The edge-coverage UNION oracle: "has any run this engine has seen ever hit +// this edge?". Every coverage strategy maintains one (it is never weaker than +// .newEdge). It was a Set, but SanCov edge indices are dense and bounded +// by the guard count, so the per-iteration `seenEdges.insert(edge).inserted` +// loop over every covered edge paid Set hashing + bucket work on the hottest +// path in the whole fuzzer — ~7% of the process across every strategy +// (scheduler-lab Finding 41m). A packed bit array gives the same `.inserted` +// answer in O(1) with no hashing and, after warm-up, no allocation. + +/// A test-and-set bitmap over SanCov edge indices. Drop-in for the union half of +/// `Set`: `insert(_:)` returns whether the edge was newly covered, the +/// same contract as `Set.insert(_:).inserted`. +struct EdgeUnionBitmap { + /// Packed bits, 64 edges per word. Grown lazily to cover the highest edge + /// index seen; after the first runs touch the full edge set it never grows + /// again, so steady-state inserts allocate nothing. + private var words: [UInt64] = [] + + init() {} + + /// Mark `edge` covered. Returns `true` iff it was NOT already covered. + @inline(__always) + mutating func insert(_ edge: UInt32) -> Bool { + let word = Int(edge >> 6) + let bit = UInt64(1) << (UInt64(edge) & 63) + if word >= words.count { + words.append(contentsOf: repeatElement(0, count: word - words.count + 1)) + } + if words[word] & bit != 0 { return false } + words[word] |= bit + return true + } + + /// Whether `edge` has been covered (membership without mutating). + @inline(__always) + func contains(_ edge: UInt32) -> Bool { + let word = Int(edge >> 6) + guard word < words.count else { return false } + return words[word] & (UInt64(1) << (UInt64(edge) & 63)) != 0 + } + + /// Number of distinct edges covered. + var count: Int { words.reduce(0) { $0 + $1.nonzeroBitCount } } + + var isEmpty: Bool { words.allSatisfy { $0 == 0 } } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/FeatureHashSet.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/FeatureHashSet.swift new file mode 100644 index 00000000..2ceefb22 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/FeatureHashSet.swift @@ -0,0 +1,85 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// An open-addressing UInt64 membership set keyed on the value directly — NO +// Swift Hasher (SipHash). The value-aware novelty oracle (comparisonCoverage's +// seenFeatures) stores feature keys that are ALREADY splitmix64-mixed hashes +// (see comparisonFeature). Running them through Set re-hashed already- +// uniform bits with SipHash on the hottest per-iteration path — ~2.5% of the +// process purely in Hasher (scheduler-lab Finding 41n). Indexing on the value's +// own (already-mixed) low bits removes that entirely; the same trick +// BoundarySiteAccumulator uses for its PC keys. + +/// Open-addressing set of UInt64 feature keys with `.inserted` semantics. The +/// keys are assumed pre-mixed (uniform low bits), so the probe index is the +/// value itself masked — no secondary hashing. Linear probing; grows at a 0.75 +/// load factor. The literal value `0` is tracked separately so an empty slot +/// (also 0) is never mistaken for a stored 0. +struct FeatureHashSet { + /// 0 marks an empty slot; a stored literal 0 is tracked by `hasZero`. + private var slots: [UInt64] + private var mask: UInt64 + /// Non-zero members held in `slots` (excludes the separately-tracked 0). + private var occupied: Int + private var hasZero: Bool + + init(minimumCapacity: Int = 64) { + var cap = 64 + while cap < minimumCapacity { cap <<= 1 } + slots = [UInt64](repeating: 0, count: cap) + mask = UInt64(cap - 1) + occupied = 0 + hasZero = false + } + + var count: Int { occupied + (hasZero ? 1 : 0) } + var isEmpty: Bool { count == 0 } + + /// Insert `value`; returns `true` iff it was NOT already present (the + /// `Set.insert(_:).inserted` contract). + @inline(__always) + mutating func insert(_ value: UInt64) -> Bool { + if value == 0 { + if hasZero { return false } + hasZero = true + return true + } + // Grow before insert when load would exceed 0.75 (count*4 >= cap*3). + if (occupied + 1) &* 4 >= slots.count &* 3 { grow() } + var i = Int(value & mask) + while true { + let s = slots[i] + if s == 0 { + slots[i] = value + occupied += 1 + return true + } + if s == value { return false } + i = Int((UInt64(i) &+ 1) & mask) + } + } + + private mutating func grow() { + let newCap = slots.count << 1 + var newSlots = [UInt64](repeating: 0, count: newCap) + let newMask = UInt64(newCap - 1) + for s in slots where s != 0 { + var i = Int(s & newMask) + while newSlots[i] != 0 { i = Int((UInt64(i) &+ 1) & newMask) } + newSlots[i] = s + } + slots = newSlots + mask = newMask + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountAccumulator.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountAccumulator.swift new file mode 100644 index 00000000..76f1a580 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountAccumulator.swift @@ -0,0 +1,167 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Lock-free per-edge hit counter for HitCountBucketsStrategy's onEdge half. +// +// Profiling (notebook Finding 42) measured the previous SyncBox(NSLock) being +// taken ~714x per test (once per edge hit) — the per-DISPATCH lock leak SyncBox +// was never meant to carry. This removes it the same way BoundarySiteAccumulator +// removed the cmp-channel lock: a FIXED-capacity open-addressing table over flat +// atomic arrays, each edge's count bumped with a per-slot atomic add. No lock, +// no Dictionary SipHash, no copy-on-write ARC on the hot path. +// + +import Atomics + +/// Open-addressing edge → hitCount map for the per-edge hot path. LOCK-FREE and +/// concurrency-safe (a property that spawns child tasks routes edge hooks from +/// several threads into one inherited context — see BoundarySiteAccumulator's +/// note). Every shared field is a per-slot atomic over a FIXED buffer, so +/// concurrent `record`s never tear and never touch reallocated memory; `reset`/ +/// `snapshot` run at `decide`, and a straggler racing them can at worst lose its +/// own late increment, never corrupt memory. +/// +/// `@unchecked Sendable` because the raw atomic-storage pointers are not +/// automatically `Sendable`. +final class HitCountAccumulator: @unchecked Sendable { + /// One occupied slot's snapshot, handed to `decide` once per iteration. + struct EdgeCount { + var edge: UInt32 + var count: UInt32 + } + + // Parallel flat buffers (Structure-of-Arrays). `keys[i]` holds `edge + 1`, so + // 0 marks an empty slot AND edge 0 (a valid index) is representable. `count` + // is the per-edge hit tally. Capacity is a power of two (mask, not modulo) and + // FIXED for the accumulator's life. + private let keys: UnsafeMutablePointer> + private let count: UnsafeMutablePointer> + // Occupied slot indices in claim order → O(occupied) snapshot/reset. Written + // only by the thread that wins a slot's key-claim CAS; -1 = not yet published. + private let occ: UnsafeMutablePointer> + private let occCount = UnsafeAtomic.create(0) + // Set once if the table ever fills and an increment is dropped (best-effort; + // real workloads have far fewer distinct edges-per-run than capacity). + private let overflowed = UnsafeAtomic.create(false) + private let capacity: Int + private let mask: Int + + init(initialCapacity: Int = 8192) { + var cap = 1 + while cap < initialCapacity { cap <<= 1 } + capacity = cap + mask = cap - 1 + keys = .allocate(capacity: cap) + count = .allocate(capacity: cap) + occ = .allocate(capacity: cap) + keys.initialize(repeating: AtomicRep(0), count: cap) + count.initialize(repeating: AtomicRep(0), count: cap) + occ.initialize(repeating: AtomicRep(-1), count: cap) + } + + deinit { + keys.deinitialize(count: capacity); keys.deallocate() + count.deinitialize(count: capacity); count.deallocate() + occ.deinitialize(count: capacity); occ.deallocate() + occCount.destroy() + overflowed.destroy() + } + + /// True iff the fixed table ever filled and dropped an increment. Diagnostic. + var didOverflow: Bool { overflowed.load(ordering: .relaxed) } + + /// splitmix64 finaliser — cheap, well-distributed. NOT `Swift.Hasher`. + @inline(__always) + private static func hash(_ x: UInt64) -> UInt64 { + var z = x &+ 0x9E37_79B9_7F4A_7C15 + z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 + z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB + return z ^ (z >> 31) + } + + /// Record one hit of `edge`. Lock-free; safe to call concurrently from + /// inherited child tasks. Steady-state cost is a relaxed load + an atomic add. + func record(edge: UInt32) { + let key = UInt64(edge) &+ 1 // edge 0 → key 1; 0 stays the empty sentinel + var i = Int(Self.hash(key) & UInt64(mask)) + var probes = 0 + while probes <= mask { + let kAtom = UnsafeAtomic(at: keys + i) + let k = kAtom.load(ordering: .relaxed) + if k == key { + UnsafeAtomic(at: count + i).wrappingIncrement(ordering: .relaxed) + return + } + if k == 0 { + let (won, _) = kAtom.compareExchange( + expected: 0, desired: key, ordering: .acquiringAndReleasing) + if won { + UnsafeAtomic(at: count + i).wrappingIncrement(ordering: .relaxed) + let slot = occCount.loadThenWrappingIncrement(ordering: .relaxed) + if slot < capacity { + UnsafeAtomic(at: occ + slot).store(i, ordering: .relaxed) + } + return + } + // Lost the claim: if the winner took it for OUR key, bump in place; + // otherwise keep probing. + if kAtom.load(ordering: .relaxed) == key { + UnsafeAtomic(at: count + i).wrappingIncrement(ordering: .relaxed) + return + } + } + i = (i &+ 1) & mask + probes &+= 1 + } + // Table full — drop (best-effort signal). Never happens for real workloads. + overflowed.store(true, ordering: .relaxed) + } + + /// The occupied (edge, count) pairs. Built once per iteration in `decide`. + func snapshot() -> [EdgeCount] { + let n = min(occCount.load(ordering: .acquiring), capacity) + var out: [EdgeCount] = [] + out.reserveCapacity(n) + var j = 0 + while j < n { + let i = UnsafeAtomic(at: occ + j).load(ordering: .relaxed) + if i >= 0 && i < capacity { + let k = UnsafeAtomic(at: keys + i).load(ordering: .relaxed) + if k != 0 { + out.append(EdgeCount( + edge: UInt32(truncatingIfNeeded: k &- 1), + count: UnsafeAtomic(at: count + i).load(ordering: .relaxed))) + } + } + j &+= 1 + } + return out + } + + /// Clear every occupied slot, keeping capacity for the next run. O(occupied). + func reset() { + let n = min(occCount.load(ordering: .relaxed), capacity) + var j = 0 + while j < n { + let i = UnsafeAtomic(at: occ + j).load(ordering: .relaxed) + if i >= 0 && i < capacity { + UnsafeAtomic(at: keys + i).store(0, ordering: .relaxed) + UnsafeAtomic(at: count + i).store(0, ordering: .relaxed) + UnsafeAtomic(at: occ + j).store(-1, ordering: .relaxed) + } + j &+= 1 + } + occCount.store(0, ordering: .relaxed) + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountBucketsStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountBucketsStrategy.swift index 2dfde922..680c931e 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountBucketsStrategy.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/HitCountBucketsStrategy.swift @@ -57,36 +57,39 @@ private func bucketBit(forHitCount count: UInt32) -> UInt8 { /// the STRATEGY's own per-engine state — the corpus stores results, it /// doesn't judge them. private func makeHitCountBucketsEngine() -> CoverageEngine { - // One lock for both halves is safe: onEdge, onReset, and decide all run - // under the per-thread observer gate, so edges their own code fires are - // recorded but never dispatched back into onEdge. - struct BucketState { - /// This iteration's per-edge hit counts (cleared on reset). - var hitCounts: [UInt32: UInt32] = [:] - /// Engine-lifetime per-edge bitmask of observed buckets. - var seenBuckets: [UInt32: UInt8] = [:] + // Per-EDGE half (onEdge/onReset): a lock-free accumulator — the SyncBox here + // was taken ~714x per test (Finding 42). Engine-lifetime half (decide): + // seenBuckets, touched ONLY in decide, which the fuzz loop calls serially on + // one thread per engine — so a plain holder needs no lock. onEdge never reads + // or writes seenBuckets, so there is no onEdge/decide race on it; stragglers + // race only the accumulator, which is atomic. + let hits = HitCountAccumulator() + + /// Engine-lifetime per-edge bitmask of observed buckets. Decide-only; a + /// reference so the @Sendable decide closure can mutate it, @unchecked + /// Sendable because decide is serialized per engine. + final class SeenBuckets: @unchecked Sendable { + var map: [UInt32: UInt8] = [:] } - let state = SyncBox(BucketState()) + let seen = SeenBuckets() return CoverageEngine( onEdge: { edge, _ in - state.update { $0.hitCounts[edge, default: 0] += 1 } + hits.record(edge: edge) }, onReset: { - state.update { $0.hitCounts.removeAll(keepingCapacity: true) } + hits.reset() } ) { _ in - state.update { state in - defer { state.hitCounts.removeAll(keepingCapacity: true) } - var foundNewBucket = false - for (edge, count) in state.hitCounts { - let bucket = bucketBit(forHitCount: count) - if state.seenBuckets[edge, default: 0] & bucket == 0 { - state.seenBuckets[edge, default: 0] |= bucket - foundNewBucket = true - } + defer { hits.reset() } + var foundNewBucket = false + for ec in hits.snapshot() { + let bucket = bucketBit(forHitCount: ec.count) + if seen.map[ec.edge, default: 0] & bucket == 0 { + seen.map[ec.edge, default: 0] |= bucket + foundNewBucket = true } - return foundNewBucket } + return foundNewBucket } } diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/NewEdgeStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/NewEdgeStrategy.swift index 2dee6280..9f97488d 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/NewEdgeStrategy.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/NewEdgeStrategy.swift @@ -28,12 +28,12 @@ extension CoverageStrategy { /// engine hasn't seen before. The novelty oracle is the STRATEGY's own /// per-engine state — the corpus stores results, it doesn't judge them. private func makeNewEdgeEngine() -> CoverageEngine { - let seen = SyncBox>([]) + let seen = UncheckedBox(EdgeUnionBitmap()) return CoverageEngine { sparse in seen.update { seenEdges in var foundNew = false - for edge in sparse.indices where seenEdges.insert(edge).inserted { + for edge in sparse.indices where seenEdges.insert(edge) { foundNew = true } return foundNew diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/PathTrieStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/PathTrieStrategy.swift index d1991a8a..f79fdfc5 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/PathTrieStrategy.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/PathTrieStrategy.swift @@ -82,7 +82,7 @@ private func makePathTrieEngine(gramLength: Int?) -> CoverageEngine { // Grams are collected inside decide's critical section (the trie resets // before decide returns); the stash carries them to the engine's // `features` call. - let lastGrams = SyncBox<[UInt64]>([]) + let lastGrams = UncheckedBox<[UInt64]>([]) return CoverageEngine( onEdge: hooks.onEdge, diff --git a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/SignatureMatchStrategy.swift b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/SignatureMatchStrategy.swift index 4a59f4b4..aa3aebc1 100644 --- a/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/SignatureMatchStrategy.swift +++ b/Sources/PropertyTestingKit/Fuzzing/CoverageStrategies/SignatureMatchStrategy.swift @@ -108,7 +108,7 @@ private struct SignatureIndex { /// interesting. The inverted index is this engine's state, wrapped in a /// `SyncBox` because the decision closure is `@Sendable`. private func makeSignatureMatchEngine() -> CoverageEngine { - let index = SyncBox(SignatureIndex()) + let index = UncheckedBox(SignatureIndex()) return CoverageEngine { sparse in let isDuplicate = index.update { idx in diff --git a/Sources/PropertyTestingKit/Fuzzing/FuzzAPI.swift b/Sources/PropertyTestingKit/Fuzzing/FuzzAPI.swift index 3d7c19a6..3775e9e2 100644 --- a/Sources/PropertyTestingKit/Fuzzing/FuzzAPI.swift +++ b/Sources/PropertyTestingKit/Fuzzing/FuzzAPI.swift @@ -100,7 +100,6 @@ import Dependencies /// or `.extend` (load corpus as seeds, then fuzz). To verify a corpus without /// fuzzing, use `regress(...)` instead. Can be overridden suite-wide via the /// `FUZZ_CORPUS_MODE` environment variable. -/// - coverageStrategy: How an input is judged "interesting" (default: `.pathTrie`). /// A strategy carries its own per-edge measurement (`onEdge` sees every /// hit, with the first-hit bit) and judgement (`decide`); build a custom /// `CoverageStrategy` for custom per-edge measurement — e.g. tallying @@ -136,7 +135,6 @@ public func fuzz( seeds: [(repeat each Input)] = [], duration: Duration = .seconds(60), persistence: CorpusPersistence = .auto, - coverageStrategy: CoverageStrategy = .pathTrie, scheduler: any SchedulerFactory = MutationScheduler.weightedPool(), scheduleFuzzing: Bool = false, parallelism: Int = ProcessInfo.processInfo.processorCount, @@ -151,7 +149,6 @@ public func fuzz( seeds: seeds, duration: duration, persistence: persistence, - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleFuzzing: scheduleFuzzing, parallelism: parallelism, @@ -170,7 +167,6 @@ func fuzzInternal( seeds: [(repeat each Input)], duration: Duration, persistence: CorpusPersistence, - coverageStrategy: CoverageStrategy, scheduler: any SchedulerFactory, scheduleFuzzing: Bool, parallelism: Int, @@ -220,7 +216,6 @@ func fuzzInternal( persistence: persistence, duration: duration, verbose: verbose, - coverageStrategy: coverageStrategy, scheduler: scheduler, projectPath: projectPath(from: filePath), sourceFileID: testFilePath, @@ -246,7 +241,6 @@ func fuzzInternal( parallelism: effectiveParallelism, duration: duration, verbose: verbose, - coverageStrategy: coverageStrategy, scheduler: scheduler, projectPath: projectPath(from: filePath), sourceFileID: testFilePath, @@ -323,7 +317,6 @@ func regressInternal( /// - persistence: How the on-disk corpus is treated (`.auto`/`.replace`/`.extend`). /// To verify a corpus without fuzzing, use `regress(...)`. Can be overridden /// suite-wide via `FUZZ_CORPUS_MODE`. -/// - coverageStrategy: How an input is judged "interesting" (default: `.pathTrie`). /// A strategy carries its own per-edge measurement (`onEdge` sees every /// hit, with the first-hit bit) and judgement (`decide`); build a custom /// `CoverageStrategy` for custom per-edge measurement — e.g. tallying @@ -351,7 +344,6 @@ public func fuzz( seeds: [(repeat each Input)] = [], duration: Duration = .seconds(60), persistence: CorpusPersistence = .auto, - coverageStrategy: CoverageStrategy = .pathTrie, scheduler: any SchedulerFactory = MutationScheduler.weightedPool(), scheduleFuzzing: Bool = false, parallelism: Int = ProcessInfo.processInfo.processorCount, @@ -366,7 +358,6 @@ public func fuzz( seeds: seeds, duration: duration, persistence: persistence, - coverageStrategy: coverageStrategy, scheduler: scheduler, scheduleFuzzing: scheduleFuzzing, parallelism: parallelism, @@ -384,7 +375,7 @@ public func fuzz( /// /// Unlike `fuzz(...)`, this never explores: it runs exactly the inputs in the saved /// corpus and fails if any of them now trips the test. Because it only replays, it -/// takes none of the fuzz-only knobs (`seeds`, `coverageStrategy`, `parallelism`, +/// takes none of the fuzz-only knobs (`seeds`, `scheduler`, `parallelism`, /// mutators) — they would be meaningless here. Its plugins are /// `AnalysisPlugin`s, which can only emit `stop`/`recordIssue`, so a replay can never be /// handed a plugin that would mutate the run or the corpus. If no corpus exists, the run diff --git a/Sources/PropertyTestingKit/Fuzzing/FuzzEngineConvenience.swift b/Sources/PropertyTestingKit/Fuzzing/FuzzEngineConvenience.swift index fe2964aa..854a07b6 100644 --- a/Sources/PropertyTestingKit/Fuzzing/FuzzEngineConvenience.swift +++ b/Sources/PropertyTestingKit/Fuzzing/FuzzEngineConvenience.swift @@ -22,22 +22,19 @@ import FuzzCore import Dependencies extension FuzzEngine { - /// Non-scheduled convenience initializer with the default coverage strategy - /// (`.pathTrie`) and scheduler (`.weightedPool()`), and a no-op - /// schedule-bytes extractor. + /// Non-scheduled convenience initializer with the default scheduler + /// (`.weightedPool()`, edge coverage via `.pathTrie`) and a no-op + /// schedule-bytes extractor. The scheduler vends its own instrumentation + /// providers, so coverage is no longer a separate knob here. convenience init( mutators: repeat Mutator, config: FuzzEngineConfig = FuzzEngineConfig(), - coverageStrategy: CoverageStrategy = .pathTrie, scheduler: any SchedulerFactory = MutationScheduler.weightedPool() ) { self.init( mutators: repeat each mutators, config: config, - makeInstrumentationProviders: { - @Dependency(\.coverageCounters) var client - return [CoverageProvider(evaluator: coverageStrategy.makeEvaluator(), client: client)] - }, + makeInstrumentationProviders: { scheduler.makeProviders() }, schedulerFactory: scheduler, scheduleBytesExtractor: { _ in nil } ) diff --git a/Sources/PropertyTestingKit/Fuzzing/Plugins/FuzzPluginHandler.swift b/Sources/PropertyTestingKit/Fuzzing/Plugins/FuzzPluginHandler.swift index 73bf9dc3..ac67b4f3 100644 --- a/Sources/PropertyTestingKit/Fuzzing/Plugins/FuzzPluginHandler.swift +++ b/Sources/PropertyTestingKit/Fuzzing/Plugins/FuzzPluginHandler.swift @@ -164,15 +164,12 @@ extension FuzzPlugin { print(message) } - // Return actions: select for mutation, add to corpus, and record issue + // Return actions: bias mutation toward the bug's neighbourhood + // and record the issue. The minimized input is NOT persisted to + // the corpus — failure retention for regression is tracked + // separately (corpus is now purely the scheduler's retained set). return [ .selectForMutation(.init(input: minimized, scheduleBytes: context.scheduleBytes)), - .submitToCorpus(.init( - input: minimized, - scheduleBytes: context.scheduleBytes, - sparseCoverage: context.executionContext[CoverageProbeKey.self]?.coverage ?? SparseCoverage(), - entryType: .failure - )), .recordIssue(.init( comment: Comment(rawValue: message), sourceLocation: context.sourceLocation diff --git a/Sources/PropertyTestingKit/Fuzzing/ScheduleFlatten.swift b/Sources/PropertyTestingKit/Fuzzing/ScheduleFlatten.swift index 11bf5810..dca200aa 100644 --- a/Sources/PropertyTestingKit/Fuzzing/ScheduleFlatten.swift +++ b/Sources/PropertyTestingKit/Fuzzing/ScheduleFlatten.swift @@ -66,10 +66,7 @@ func peelScheduleResult( // exactly as before the flattening. CorpusEntry( input: repeat each entry.input.1, - scheduleBytes: entry.input.0, - sparseCoverage: entry.sparseCoverage, - entryType: entry.entryType, - failure: entry.failure + scheduleBytes: entry.input.0 ) } @@ -105,7 +102,6 @@ func runFlattenedSchedule( persistence: CorpusPersistence, duration: Duration, verbose: Bool, - coverageStrategy: CoverageStrategy, scheduler: any SchedulerFactory, projectPath: String?, sourceFileID: String, @@ -143,7 +139,6 @@ func runFlattenedSchedule( parallelism: 1, duration: duration, verbose: verbose, - coverageStrategy: coverageStrategy, scheduler: scheduler, projectPath: projectPath, sourceFileID: sourceFileID, diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/AdaptiveDepthMath.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/AdaptiveDepthMath.swift new file mode 100644 index 00000000..de87334c --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/AdaptiveDepthMath.swift @@ -0,0 +1,73 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Pure scoring math for the productivity-weighted, adaptive-depth pool policy. +// Kept as free functions, pinned by characterization tests (AdaptiveDepthMathTests), +// so the formulas are decided once and the policy just wires them onto events. +// + +/// Score 1 — per-seed draw weight. A mutant that *owns* `n ≥ 1` coverage +/// features spikes its parent's weight by `×(1 + n)` (more ownership → bigger +/// spike); a fruitless mutant decays it by `×decay`. The decay is asymptotic to +/// zero — a seed's draw chance shrinks indefinitely but never vanishes — so the +/// `floor` exists only to keep floating-point from underflowing to a literal 0. +func adaptiveDrawWeightUpdate( + _ weight: Double, + ownedFeatures n: Int, + decay: Double = 0.95, + floor: Double = 1e-9 +) -> Double { + let next = n > 0 ? weight * (1.0 + Double(n)) : weight * decay + return max(floor, next) +} + +/// Score 2 — one level of the per-seed depth cascade. `score` is the "advance +/// past this depth" likelihood (×100). A **miss** at this level climbs it slowly +/// toward `ceiling` by a fraction `alpha` of the remaining gap — an exponential +/// approach that never reaches the ceiling, so (with `ceiling < 100`) every +/// level always keeps a positive chance of *stopping*, which is exactly what +/// makes depth self-cap geometrically. A **hit** anchors the productive depth by +/// decaying the score back down. +/// +/// Defaults `alpha=0.02, ceiling=45` are the swept optimum (2026-06-14, Finding +/// 31): the original `0.05/90` escalated depth to a mean of ~7 straight into the +/// 0%-productive deep tail, halving the solve rate on compound-structure bugs. +/// The shallower climb keeps depth in the productive band while the cascade can +/// still reach deep rungs when a seed genuinely stalls (ceiling is an asymptote, +/// not a hard cap). +func depthAdvanceUpdate( + _ score: Double, + hit: Bool, + alpha: Double = 0.02, + ceiling: Double = 45.0, + anchorDecay: Double = 0.95 +) -> Double { + hit ? score * anchorDecay : score + alpha * (ceiling - score) +} + +/// Score 2 — sample a mutation depth from the per-seed cascade. `scores[i]` is +/// the advance-past likelihood (×100) for depth `i + 1`. Walking from the +/// shallowest level: a roll `r ∈ [0, 100)` below `scores[i]` advances to the +/// next level, otherwise we stop and emit depth `i + 1`. Advancing past every +/// known level emits a brand-new deeper rung (`scores.count + 1`) — how depth +/// ratchets up one step at a time. `rolls` injects the per-level draws for +/// determinism; an exhausted roll stream stops (treated as `100`). +func sampleMutationDepth(scores: [Double], rolls: [Double]) -> Int { + var i = 0 + while i < scores.count { + let r = i < rolls.count ? rolls[i] : 100.0 + if r < scores[i] { i += 1 } else { return i + 1 } + } + return scores.count + 1 +} diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/AdaptiveDepthPolicy.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/AdaptiveDepthPolicy.swift new file mode 100644 index 00000000..ce8d7d10 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/AdaptiveDepthPolicy.swift @@ -0,0 +1,141 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Productivity-weighted, adaptive-depth pool policy. Two per-seed scores spend +// the mutation budget where it has been paying off, and dig DEEPER on seeds +// whose shallow neighborhood has been mined out (instead of forever drawing +// depth-1 siblings of a saturated pool). +// + +/// Per-seed scheduling by mutation productivity, advising both the draw weight +/// and the mutation depth: +/// +/// - **Score 1 (draw weight).** A mutant that *owns* `n ≥ 1` features spikes its +/// parent's weight `×(1 + n)`; a fruitless one decays it `×decay`. Asymptotic +/// to zero — never zero — so every seed keeps a vanishing-but-positive draw +/// chance (`adaptiveDrawWeightUpdate`). +/// - **Score 2 (mutation depth).** A per-seed cascade of "advance past this +/// depth" scores. Each resolved mutant updates its stop level: a miss climbs +/// it slowly toward a ceiling `< 100` (never reaching it → depth self-caps +/// geometrically), a hit anchors the productive depth. The next depth is +/// re-sampled from the cascade and pushed to the core via `.setMutationDepth`. +/// +/// Attribution: a mutant's outcome is only fully known across two events — +/// `.iteration` (fires for every execution, before admission) then maybe +/// `.inserted` (fires only on admission, carrying the parent + owned count). So +/// the policy resolves each mutant on a one-step defer: it stashes the pool +/// iteration's parent + depth, lets a following `.inserted` upgrade it to a hit, +/// and flushes the weight/depth update on the next `.iteration` or `.willDraw`. +public final class AdaptiveDepthPolicy: PoolPlugin { + private let decay: Double + private let alpha: Double + private let ceiling: Double + private let weightFloor: Double + private let roll: @Sendable () -> Double + + /// Per-entry state, index == entry ID (append-only, mirrors the core's IDs). + private var weights: [Double] = [] + private var depthScores: [[Double]] = [] + private var depthFor: [Int] = [] + + /// The mutant awaiting resolution (set on a pool `.iteration`, upgraded by a + /// following `.inserted`, applied on the next flush). + private var pendingParent: Int? + private var pendingDepth = 1 + private var pendingHit = false + private var pendingClaimed = 0 + + public init( + decay: Double = 0.95, + alpha: Double = 0.02, + ceiling: Double = 45.0, + weightFloor: Double = 1e-9, + roll: (@Sendable () -> Double)? = nil + ) { + self.decay = decay + self.alpha = alpha + self.ceiling = ceiling + self.weightFloor = weightFloor + self.roll = roll ?? { var r = FastRNG(); return Double.random(in: 0..<100, using: &r) } + } + + public func handle(event: PoolEvent) -> [PoolAction] { + switch event { + case let .iteration(outcome): + let actions = flush() + if case let .pool(parent) = outcome.source, parent < weights.count { + pendingParent = parent + pendingDepth = depthFor[parent] + pendingHit = false + pendingClaimed = 0 + } else { + pendingParent = nil + } + return actions + + case let .inserted(id, _, _, parent, claimed): + // Sequential IDs (admission is the only inserter), so a new entry + // always extends the arrays by one. + if id == weights.count { + weights.append(1.0) + depthScores.append([0.0]) + depthFor.append(1) + } + // A just-admitted mutant of the pending parent is that parent's hit. + if let parent, parent == pendingParent { + pendingHit = true + pendingClaimed = claimed + } + return [] + + case .removed: + return [] + + case .willDraw: + return flush() + } + } + + /// Resolve the pending mutant: update its parent's draw weight and the + /// depth-cascade level it stopped at, then re-sample the parent's next depth. + private func flush() -> [PoolAction] { + guard let p = pendingParent, p < weights.count else { + pendingParent = nil + return [] + } + pendingParent = nil + var actions: [PoolAction] = [] + + // Score 1 — draw weight. + weights[p] = adaptiveDrawWeightUpdate( + weights[p], ownedFeatures: pendingHit ? pendingClaimed : 0, + decay: decay, floor: weightFloor) + actions.append(.setWeight(id: p, weights[p])) + + // Score 2 — climb/anchor the level the mutant stopped at... + let idx = pendingDepth - 1 + while depthScores[p].count <= idx { depthScores[p].append(0.0) } + depthScores[p][idx] = depthAdvanceUpdate( + depthScores[p][idx], hit: pendingHit, alpha: alpha, ceiling: ceiling) + + // ...then re-sample the next depth from the cascade. + let rolls = (0.. Verdict { - var claimed: [UInt64] = [] - for feature in features { - if let owner = featureOwners[feature] { - if size < entrySize[owner] { claimed.append(feature) } - } else { - claimed.append(feature) - } - } - guard !claimed.isEmpty else { - return Verdict(admit: false, evict: []) - } - - let id = entrySize.count - entrySize.append(size) - entryOwnedCount.append(claimed.count) - - var evicted: [Int] = [] - for feature in claimed { - if let loser = featureOwners[feature] { - entryOwnedCount[loser] -= 1 - if entryOwnedCount[loser] == 0 { - evicted.append(loser) - } - } - featureOwners[feature] = id - } - return Verdict(admit: true, evict: evicted) - } -} diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/MutationScheduler.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/MutationScheduler.swift index ef4b1343..f00f8c13 100644 --- a/Sources/PropertyTestingKit/Fuzzing/Scheduler/MutationScheduler.swift +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/MutationScheduler.swift @@ -30,21 +30,29 @@ /// applies, whoever caused it. import FuzzCore +/// Decides which inputs the engine mutates and when it generates fresh ones, and +/// vends the instrumentation the chosen signal needs. Wrapping `any SchedulerFactory` +/// (rather than being one concrete factory) keeps the design's promise that a +/// `MutationScheduler` can vend any scheduler, not only the weighted pool. The +/// signal config (coverage strategy, and in future a cmp source) travels with the +/// scheduler the caller picks rather than being a top-level `fuzz()` knob. public struct MutationScheduler: SchedulerFactory { - /// The underlying factory. Wrapping `any SchedulerFactory` (rather than - /// being one concrete factory) keeps the design's promise that a - /// `MutationScheduler` can vend any scheduler, not only the weighted pool. let factory: any SchedulerFactory /// Build this engine's scheduler at its input pack by forwarding to the - /// wrapped factory. One fresh scheduler per engine (the factory captures the - /// engine's mutators so the scheduler owns input production). + /// wrapped factory (one fresh scheduler per engine). public func makeScheduler( mutators: repeat Mutator ) -> AnyScheduler { factory.makeScheduler(mutators: repeat each mutators) } + /// Forward the wrapped factory's instrumentation providers (e.g. the weighted + /// pool's coverage provider). + public func makeProviders() -> [any InstrumentationProvider] { + factory.makeProviders() + } + /// A weighted mutation pool that picks generation vs mutation by a ratio. /// /// - Parameters: @@ -63,17 +71,24 @@ public struct MutationScheduler: SchedulerFactory { /// vocabulary distinguishes inputs from how many of them may stay — /// without it, a fine vocabulary silently raises the population /// ceiling. + /// - coverageStrategy: How a run is judged "interesting" and what culling + /// vocabulary it publishes (default: `.pathTrie`). This is the scheduler's + /// coverage signal — it travels with the scheduler rather than being a + /// top-level `fuzz()` knob, so coverage is one battery among possible + /// others, not a privileged library concept. public static func weightedPool( admission: PoolAdmission = .featureOwnership, policies: @escaping @Sendable () -> [any PoolPlugin] = { [] }, generationRatio: Double = 0.1, - capacity: Int? = nil + capacity: Int? = nil, + coverageStrategy: CoverageStrategy = .pathTrie ) -> MutationScheduler { MutationScheduler(factory: WeightedPoolFactory( admission: admission, makePolicies: policies, generationRatio: generationRatio, - capacity: capacity + capacity: capacity, + coverageStrategy: coverageStrategy )) } } diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/OwnershipEvaluators.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/OwnershipEvaluators.swift new file mode 100644 index 00000000..a8be4208 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/OwnershipEvaluators.swift @@ -0,0 +1,73 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Ownership evaluators: signal-specific, stateful, they own the ownership +// *criterion*. Each consumes a run's signal plus the input's size, updates its +// own metric state, and emits the `Feature`s the run won — for the generic +// `OwnershipLedger` to record. The metric (edge size, comparison distance) +// lives here and never reaches the ledger, so adding a new signal is "add an +// evaluator", not "teach the ledger a new comparison". +// + +/// Feature namespaces. Keeps an edge index and a comparison-site `pc` of the +/// same numeric value from colliding in the shared ledger. +enum FeatureDomain { + static let edge: UInt8 = 0 + static let comparison: UInt8 = 1 +} + +/// Edge ownership (libFuzzer REDUCE): an edge is owned by the smallest input +/// exhibiting it; ties don't steal, so ownership only ever moves to strictly +/// simpler inputs and the churn terminates. `claims` returns the edges this run +/// won — newly seen, or stolen from a strictly larger owner. +struct EdgeOwnershipEvaluator { + /// Edge id → smallest owning input's size. + private var bestSize: [UInt64: Int] = [:] + + mutating func claims(edges: [UInt64], size: Int) -> [Feature] { + var won: [Feature] = [] + for id in edges { + if let best = bestSize[id] { + guard size < best else { continue } // ties don't steal + } + bestSize[id] = size + won.append(Feature(domain: FeatureDomain.edge, id: id)) + } + return won + } +} + +/// Boundary-distance ownership (the value-axis gradient): a comparison site +/// (`pc`) is owned by the input that drove its operands closest together +/// (lowest `|arg1 - arg2|`). A strictly closer input steals; on an exact tie the +/// smaller input steals (so ownership prefers the simpler witness). Distance and +/// size both only decrease, so the churn terminates the same way REDUCE does. +struct BoundaryDistanceEvaluator { + /// Comparison site `pc` → its current owner's (distance, size). + private var best: [UInt64: (distance: UInt64, size: Int)] = [:] + + mutating func claims(distances: [UInt64: UInt64], size: Int) -> [Feature] { + var won: [Feature] = [] + for (pc, distance) in distances { + if let cur = best[pc] { + let closer = distance < cur.distance + let tieToSmaller = distance == cur.distance && size < cur.size + guard closer || tieToSmaller else { continue } + } + best[pc] = (distance, size) + won.append(Feature(domain: FeatureDomain.comparison, id: pc)) + } + return won + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/OwnershipLedger.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/OwnershipLedger.swift new file mode 100644 index 00000000..9aace76b --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/OwnershipLedger.swift @@ -0,0 +1,93 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The generic ownership ledger: pool-residence bookkeeping, with no notion of +// WHY a feature was claimed. An evaluator decides — using its own metric (edge +// size, comparison distance, …) — which features a run wins; the ledger only +// records the resulting feature → owner roster, reassigns claimed features, and +// evicts an entry the moment it owns nothing. Splitting the criterion (in the +// evaluators) from the bookkeeping (here) lets edge coverage, comparison +// distance, and any future signal share one pool roster. +// + +/// A feature is an opaque fact a run exhibited, namespaced by the evaluator that +/// emits it. `domain` keeps distinct signals from colliding — an edge index and +/// a comparison-site `pc` of the same numeric value are different features. +struct Feature: Hashable, Sendable { + /// The emitting evaluator's namespace (e.g. edges vs. comparison sites). + let domain: UInt8 + /// The feature's identity within its domain (an edge index, a k-gram hash, + /// a comparison-site `pc`, …). + let id: UInt64 + + init(domain: UInt8, id: UInt64) { + self.domain = domain + self.id = id + } +} + +/// Pool-residence ownership over `Feature`s, metric-agnostic. +/// +/// `record` takes the features an evaluator decided this run OWNS (claimed from +/// a prior owner, or previously unowned) and applies them: a fresh entry ID is +/// assigned, each feature is reassigned to it, and any prior owner that loses +/// its last feature is evicted. Entry IDs are sequential and never reused, +/// mirroring `WeightedPoolCore`'s ID assignment — the two stay aligned because +/// admission is the only path that mints an ID. +/// +/// Ownership deliberately outlives pool membership: an entry evicted for +/// capacity by `WeightedPoolCore` stays a *ghost owner* of its features here, so +/// re-witnessing a represented feature earns nothing (the evaluator's metric +/// state ghosts in lockstep). +struct OwnershipLedger { + struct Verdict { + /// The run claimed ≥ 1 feature and joins the pool. + let admit: Bool + /// The entry ID minted for an admitted run; `nil` when rejected (no ID + /// is consumed). + let entryID: Int? + /// Entries that lost their last owned feature to this claim. + let evict: [Int] + /// How many features this run newly owned (0 when rejected) — the + /// draw-weight signal. + let claimed: Int + } + + /// Feature → owning entry ID. + private var featureOwners: [Feature: Int] = [:] + /// Features currently owned per entry, index == ID. + private var entryOwnedCount: [Int] = [] + + /// Apply the features an evaluator decided this run owns. + mutating func record(claimed: [Feature]) -> Verdict { + guard !claimed.isEmpty else { + return Verdict(admit: false, entryID: nil, evict: [], claimed: 0) + } + + let id = entryOwnedCount.count + entryOwnedCount.append(claimed.count) + + var evicted: [Int] = [] + for feature in claimed { + if let loser = featureOwners[feature] { + entryOwnedCount[loser] -= 1 + if entryOwnedCount[loser] == 0 { + evicted.append(loser) + } + } + featureOwners[feature] = id + } + return Verdict(admit: true, entryID: id, evict: evicted, claimed: claimed.count) + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/PoolPlugin.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/PoolPlugin.swift index f41723d9..8a1625cf 100644 --- a/Sources/PropertyTestingKit/Fuzzing/Scheduler/PoolPlugin.swift +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/PoolPlugin.swift @@ -44,16 +44,24 @@ public struct PoolIterationOutcome: Sendable { /// back to the covered-edge count as its REDUCE/eviction size metric. public let inputSize: Int? + /// Per-comparison-site distance witnessed by the accepted run: site `pc` + /// → the lowest `|arg1 - arg2|` it drove the operands to. `featureOwnership`'s + /// `BoundaryDistanceEvaluator` owns over these (lowest distance per site + /// wins). `nil` when the strategy publishes none. + public let boundaryDistances: [UInt64: UInt64]? + public init( source: PoolIterationSource, newCoverage: SparseCoverage?, features: [UInt64]? = nil, - inputSize: Int? = nil + inputSize: Int? = nil, + boundaryDistances: [UInt64: UInt64]? = nil ) { self.source = source self.newCoverage = newCoverage self.features = features self.inputSize = inputSize + self.boundaryDistances = boundaryDistances } /// The one vocabulary every pool component accounts in: the strategy's @@ -73,7 +81,11 @@ public enum PoolEvent { case iteration(PoolIterationOutcome) /// An entry was admitted to the pool. `features` is the entry's resolved /// culling vocabulary (strategy-defined, or widened edge indices). - case inserted(id: Int, coverage: SparseCoverage, features: [UInt64]) + /// `parent` is the pool entry this input was mutated from (`nil` if it was + /// freshly generated or came off the queue), and `claimed` is how many + /// features it newly OWNED — together these let a draw-weight policy credit + /// the right parent by how much its mutant found. + case inserted(id: Int, coverage: SparseCoverage, features: [UInt64], parent: Int?, claimed: Int) /// An entry left the pool (its ID is never reused). case removed(id: Int) /// The owner is about to draw a new focus entry. The moment for lazy @@ -93,6 +105,11 @@ public enum PoolAction { /// Set an entry's draw weight. Negative values clamp to zero; an /// all-zero pool falls back to uniform draws. case setWeight(id: Int, Double) + /// Set how many times the mutator is chained when this entry is mutated + /// (depth-d = mutate∘mutate∘… d times). Clamped to ≥ 1; defaults to 1 + /// (single-step) for entries no policy has set. Lets a policy escalate + /// depth on a seed whose shallow neighborhood has been mined out. + case setMutationDepth(id: Int, depth: Int) } /// A composable policy attached to the mutation pool. @@ -115,11 +132,13 @@ public struct PoolAdmission: Sendable { let admit: Bool /// Existing entries this admission displaces from the pool. let evict: [Int] + /// How many features this input newly owned (the draw-weight signal). + let claimed: Int } - /// Builds a fresh per-engine judge over the accepted input's resolved - /// features and its size metric (real input size when a mutator - /// measures it, covered-edge count otherwise). + /// Builds a fresh per-engine judge over one accepted iteration: its + /// resolved features, its size metric (real input size when a mutator + /// measures it, covered-edge count otherwise), and any per-site distances. /// /// Admission bookkeeping deliberately outlives pool membership: an /// entry evicted for capacity stays a *ghost owner* of its features. @@ -127,38 +146,51 @@ public struct PoolAdmission: Sendable { /// claims was measured to turn a capacity-bounded pool into a revolving /// door of re-claimers); only genuinely new features, or strictly /// smaller witnesses, win residence. - let makeJudge: @Sendable () -> (_ features: [UInt64], _ size: Int) -> Verdict + let makeJudge: @Sendable () -> (_ outcome: PoolIterationOutcome) -> Verdict - init(makeJudge: @escaping @Sendable () -> ([UInt64], Int) -> Verdict) { + init(makeJudge: @escaping @Sendable () -> (PoolIterationOutcome) -> Verdict) { self.makeJudge = makeJudge } + /// The size metric for an accepted outcome: the mutator-measured input + /// size when present, the covered-edge count otherwise. + static func size(of outcome: PoolIterationOutcome) -> Int { + outcome.inputSize ?? outcome.newCoverage?.count ?? 0 + } + /// Every strategy-accepted input joins the pool, nothing ever leaves. /// The behavior of the classic corpus-mutation loop. public static let everyDiscovery = PoolAdmission( - makeJudge: { { _, _ in Verdict(admit: true, evict: []) } }) - - /// libFuzzer's corpus model: an input joins the pool only by *owning* - /// coverage features — claiming unowned ones, or stealing from a larger - /// owner (REDUCE; the size metric is the mutator-measured input size - /// when available, the covered-edge count otherwise; ties don't - /// steal). An entry that loses its last feature leaves the pool. Bounds - /// the pool by the feature space regardless of how often the coverage - /// strategy says "interesting"; rejected accepts get no burst and no - /// residence (strict semantics). + makeJudge: { { outcome in + Verdict(admit: true, evict: [], claimed: outcome.resolvedFeatures.count) + } }) + + /// libFuzzer's corpus model: an input joins the pool only by *owning* a + /// feature. Ownership is decided by per-signal evaluators and recorded in + /// one generic `OwnershipLedger`; an entry that loses its last feature + /// across all signals leaves the pool. Bounds the pool by the feature space + /// regardless of how often the coverage strategy says "interesting". /// - /// Ownership is accounted in the strategy's own vocabulary when it - /// publishes one (today only `.pathTrie(gramLength:)`'s path k-grams), and - /// the covered edge indices otherwise — so the pool retains exactly the - /// diversity the strategy accepts for. (The default `.pathTrie` and - /// `.hitCountBuckets` publish none and cull on edges; an (edge, bucket) - /// vocabulary equal to hcb's acceptance criterion would be a tautology - /// that disables culling.) + /// Two evaluators feed the ledger: + /// - `EdgeOwnershipEvaluator` — edges (the strategy's vocabulary when it + /// publishes one, the covered edge indices otherwise) owned by the + /// SMALLEST input (REDUCE; ties don't steal). + /// - `BoundaryDistanceEvaluator` — each comparison site (`pc`) owned by the + /// input that drove its operands closest (lowest `|arg1 - arg2|`; ties + /// break toward the smaller input). Inert unless the strategy publishes + /// `boundaryDistances` (`.boundaryDistance`, a `-sanitize-coverage=…,trace-cmp` + /// target) — so this admission subsumes the former `boundaryDistanceOwnership`: + /// add the cmp signal and the same admission culls over it too. public static let featureOwnership = PoolAdmission(makeJudge: { - var ledger = FeatureOwnershipLedger() - return { features, size in - let verdict = ledger.judge(features: features, size: size) - return Verdict(admit: verdict.admit, evict: verdict.evict) + var edges = EdgeOwnershipEvaluator() + var boundaries = BoundaryDistanceEvaluator() + var ledger = OwnershipLedger() + return { outcome in + let size = size(of: outcome) + var claimed = edges.claims(edges: outcome.resolvedFeatures, size: size) + claimed += boundaries.claims(distances: outcome.boundaryDistances ?? [:], size: size) + let verdict = ledger.record(claimed: claimed) + return Verdict(admit: verdict.admit, evict: verdict.evict, claimed: verdict.claimed) } }) } diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/SchedulerProbe.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/SchedulerProbe.swift new file mode 100644 index 00000000..218f9a21 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/SchedulerProbe.swift @@ -0,0 +1,32 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Diagnostic hook on the scheduler's per-iteration draw decision. Fires once +// per scheduler-driven iteration with what the scheduler chose (generate / +// queue / mutate which pool seed), at what mutation depth, and whether the +// resulting input was admitted to the pool (i.e. took ownership of a feature). +// +// This is how experiments answer "are we spending iterations productively": +// draw concentration (counts per parent), depth spread (depth histogram), and +// productivity (accepted / total per source) are all reconstructable from the +// event stream. `nil` (the default) is zero overhead — same pattern as the +// STLC `ShiftProbe`. Set via `SchedulerProbe.$observe.withValue { ... }`. + +public enum SchedulerProbe { + /// `(source, depth, accepted)` — fired after the scheduler observes the + /// iteration's outcome. `depth` is the executed mutation depth (1 for + /// generate/queue/seed inputs); `accepted` is true when the input was + /// admitted to the pool. + @TaskLocal public static var observe: (@Sendable (_ source: PoolIterationSource, _ depth: Int, _ accepted: Bool) -> Void)? +} diff --git a/Sources/PropertyTestingKit/Fuzzing/Scheduler/WeightedPoolCore.swift b/Sources/PropertyTestingKit/Fuzzing/Scheduler/WeightedPoolCore.swift index d221be03..719391e5 100644 --- a/Sources/PropertyTestingKit/Fuzzing/Scheduler/WeightedPoolCore.swift +++ b/Sources/PropertyTestingKit/Fuzzing/Scheduler/WeightedPoolCore.swift @@ -35,7 +35,7 @@ import FuzzCore final class WeightedPoolCore { private let mutators: (repeat Mutator) private let packArity: Int - private let judge: (_ features: [UInt64], _ size: Int) -> PoolAdmission.Verdict + private let judge: (_ outcome: PoolIterationOutcome) -> PoolAdmission.Verdict private let policies: [any PoolPlugin] /// Probability each step generates a fresh input rather than mutating a pool /// entry: `1` is all generation, `0` is all mutation. Clamped to `0...1`. @@ -50,6 +50,11 @@ final class WeightedPoolCore { private var pool: [(repeat each Input)] = [] /// Draw weight per entry ID (index == ID; grows append-only). private var weights: [Double] = [] + /// Per-entry mutation depth override (entry ID → chain length). Absent + /// entries mutate at depth 1; set by a policy via `.setMutationDepth` + /// (e.g. `AdaptiveDepthPolicy`). `next()` chains the mutator this many + /// times for a `.mutate(id)` directive. + private var entryDepth: [Int: Int] = [:] /// Real (mutator-measured) input size per entry ID at admission, `nil` /// when unmeasured (index == ID, grows append-only). Deliberately NOT /// the covered-edge fallback: more covered edges mark a *better* entry, @@ -104,9 +109,14 @@ final class WeightedPoolCore { ) case .mutate(let id): lastProduced = .pool(parent: id) + // Chain the mutator `entryDepth[id]` times (default 1). A depth + // policy raises it to push deeper into a productive entry's + // neighbourhood; each step mutates one random position of the + // prior result. return ScheduledInput( - input: mutateOneRandomPosition( - pool[id], inputSize: packArity, rng: &rng, mutators: repeat each mutators + input: chainMutate( + pool[id], depth: mutationDepth(for: id), inputSize: packArity, + rng: &rng, mutators: repeat each mutators ), poolParentID: id ) @@ -115,19 +125,23 @@ final class WeightedPoolCore { /// Report one executed iteration. Reads the coverage verdict out of the /// context (the only probe this scheduler requires) and, when the input was - /// interesting AND admitted, stores the typed input in its own pool and - /// returns the coverage to persist in the corpus — so the engine never reads - /// a coverage signal itself and owns no pool. + /// interesting AND admitted, stores the typed input in its own pool — so the + /// engine never reads a coverage signal itself and owns no pool. Retention is + /// no longer a per-iteration corpus write: the pool IS the retained set, and + /// the engine reads it back via `snapshot()` once at run-end. func observe( _ input: (repeat each Input), _ context: RawExecutionContext, source: SchedulerSource - ) -> SparseCoverage? { + ) { let verdict = context[CoverageProbeKey.self] let coverage = verdict?.coverage // The strategy's culling vocabulary (k-grams, edge-buckets) when it // publishes one; the pool widens covered edges otherwise. let features = verdict?.features + // Per-comparison-site distances when the strategy publishes them + // (`.boundaryDistance`); consumed only by `featureOwnership`. + let boundaryDistances = verdict?.boundaryDistances // The engine only knows external (seed/queue) vs scheduled; for a // scheduler-produced input we reconstruct the finer source from the // lineage of our own most recent `next()`. @@ -138,8 +152,20 @@ final class WeightedPoolCore { let size = coverage != nil ? measuredSize(of: repeat each input) : nil // Returns the new entry id on admission; the engine wants the signature // to persist, which is exactly the coverage that was admitted. - return admit(input, coverage: coverage, features: features, inputSize: size, - poolSource: poolSource) != nil ? coverage : nil + let admittedID = admit(input, coverage: coverage, features: features, inputSize: size, + boundaryDistances: boundaryDistances, poolSource: poolSource) + // Diagnostic: report what we drew, at what depth, and whether it stuck. + let depth: Int + if case let .pool(parent) = poolSource { depth = mutationDepth(for: parent) } else { depth = 1 } + SchedulerProbe.observe?(poolSource, depth, admittedID != nil) + } + + /// The inputs the pool currently retains, in live (drawable) order — the + /// scheduler's contribution to the corpus, read once by the engine at + /// run-end. Evicted entries are already gone from `live`, so the snapshot is + /// exactly the surviving working set, not a running tally. + func snapshot() -> [(repeat each Input)] { + live.map { pool[$0] } } /// Sum of the mutator-measured sizes across the input pack — the pool's @@ -167,18 +193,21 @@ final class WeightedPoolCore { coverage: SparseCoverage?, features: [UInt64]? = nil, inputSize: Int? = nil, + boundaryDistances: [UInt64: UInt64]? = nil, poolSource: PoolIterationSource ) -> Int? { let outcome = PoolIterationOutcome( - source: poolSource, newCoverage: coverage, features: features, inputSize: inputSize) + source: poolSource, newCoverage: coverage, features: features, + inputSize: inputSize, boundaryDistances: boundaryDistances) notifyAndApply(.iteration(outcome)) guard let coverage else { return nil } // The pool accounts ownership in the strategy's vocabulary when it // publishes one, and widened covered edges otherwise — `resolvedFeatures` - // is the single definition of that fallback. + // is the single definition of that fallback. The admission judge reads + // the whole outcome (so `featureOwnership` can see distances). let resolved = outcome.resolvedFeatures - let verdict = judge(resolved, inputSize ?? coverage.count) + let verdict = judge(outcome) guard verdict.admit else { return nil } // The admission's own displacements (REDUCE losers) go through the @@ -199,10 +228,20 @@ final class WeightedPoolCore { livePos[id] = live.count live.append(id) liveWeightTotal += 1.0 - notifyAndApply(.inserted(id: id, coverage: coverage, features: resolved)) + let parent: Int? + if case let .pool(p) = poolSource { parent = p } else { parent = nil } + notifyAndApply(.inserted(id: id, coverage: coverage, features: resolved, + parent: parent, claimed: verdict.claimed)) return id } + /// How many times `next()` chains the mutator for a `.mutate(id)` directive + /// on this entry. Defaults to 1 (single-step) until a policy raises it via + /// `.setMutationDepth`. + func mutationDepth(for id: Int) -> Int { + entryDepth[id] ?? 1 + } + /// The resident a capacity overflow removes: lowest weight, then the /// LARGEST measured input, then the NEWEST. Real sizes target the drift /// disease directly — the mutation random walk grows inputs, and the @@ -268,6 +307,7 @@ final class WeightedPoolCore { live[pos] = lastID live.removeLast() if lastID != id { livePos[lastID] = pos } + entryDepth[id] = nil // Re-broadcast so every policy stays consistent with // membership it didn't change itself. Terminates: each ID can // be removed at most once (the guard above). @@ -280,6 +320,9 @@ final class WeightedPoolCore { if livePos[id] != nil { liveWeightTotal += clamped - weights[id] } weights[id] = clamped } + + case let .setMutationDepth(id, depth): + entryDepth[id] = max(1, depth) } } } @@ -307,6 +350,9 @@ struct WeightedPoolFactory: SchedulerFactory { let makePolicies: @Sendable () -> [any PoolPlugin] let generationRatio: Double let capacity: Int? + /// The edge-coverage signal this pool culls over. The scheduler vends its own + /// provider (coverage is no longer hardcoded by the batteries). + let coverageStrategy: CoverageStrategy func makeScheduler( mutators: repeat Mutator @@ -325,7 +371,16 @@ struct WeightedPoolFactory: SchedulerFactory { return FuzzCore.AnyScheduler( requiredProbes: [CoverageProbeKey.self], next: { core.next() }, - observe: { input, context, source in core.observe(input, context, source: source) } + observe: { input, context, source in core.observe(input, context, source: source) }, + snapshot: { core.snapshot() } ) } } + +extension WeightedPoolFactory { + /// Vend the edge-coverage provider this pool's signal needs, fresh per engine. + func makeProviders() -> [any InstrumentationProvider] { + @Dependency(\.coverageCounters) var client + return [CoverageProvider(evaluator: coverageStrategy.makeEvaluator(), client: client)] + } +} diff --git a/Sources/PropertyTestingKit/Fuzzing/UncheckedBox.swift b/Sources/PropertyTestingKit/Fuzzing/UncheckedBox.swift new file mode 100644 index 00000000..d2a30e60 --- /dev/null +++ b/Sources/PropertyTestingKit/Fuzzing/UncheckedBox.swift @@ -0,0 +1,45 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A lock-free mutable holder with SyncBox's ergonomics minus the lock. +// + +/// A `@Sendable`-closure-capturable mutable holder that takes NO lock. +/// +/// Use ONLY for state confined to a single serial context — e.g. a coverage +/// engine's `decide`/`features` half, which the fuzz loop calls one-at-a-time per +/// engine (each engine owns its own instance, and the observer callbacks never +/// touch this state). It exists because a `@Sendable` closure cannot capture a +/// bare mutable `var`; the previous answer was `SyncBox` (an `NSLock`), but that +/// is a test utility never meant for the fuzz path (Finding 42). For state +/// genuinely shared across threads (per-dispatch observer accumulation), use a +/// lock-free structure (HitCountAccumulator / AtomicFeatureSet / EdgeUnionBitmap) +/// instead — this box gives no cross-thread safety. +/// +/// `@unchecked Sendable` because the contract (single serial writer) is enforced +/// by the caller, not the type. +final class UncheckedBox: @unchecked Sendable { + var value: T + + init(_ value: T) { + self.value = value + } + + /// Mutate the value in place. Mirrors `SyncBox.update` so call sites migrate + /// by changing only the type name. + @discardableResult + func update(_ transform: (inout T) throws -> Result) rethrows -> Result { + try transform(&value) + } +} diff --git a/Sources/SanCovHooks/SanCovHooks.c b/Sources/SanCovHooks/SanCovHooks.c index 38351df3..35892db2 100644 --- a/Sources/SanCovHooks/SanCovHooks.c +++ b/Sources/SanCovHooks/SanCovHooks.c @@ -67,11 +67,58 @@ static uint32_t *g_guards_start = NULL; static uint32_t *g_guards_end = NULL; static size_t g_guard_count = 0; -// Thread-local target context for schedule-aware coverage. -// Set per-thread so parallel sessions don't corrupt each other. -// Defined here (before first use in get_current_coverage_map) and -// set/cleared in sancov_set_target_context below. -static _Thread_local SanCovMeasurementContext* g_target_context = NULL; +// MARK: - Coalesced thread-local state (one TLS block per thread) +// +// PERFORMANCE: every distinct `_Thread_local` variable accessed in a dylib +// lowers to a `tlv_get_addr` *function call* on macOS. The per-comparison hot +// path (sancov_dispatch_cmp → get_current_coverage_map) touched ~6 separate +// thread-locals, so it paid ~6 tlv_get_addr calls per instrumented comparison — +// profiled at ~30% of the cmp-dispatch subtree (Finding 41c). Coalescing them +// into ONE struct lets the hot path fetch the block address ONCE (a single +// tlv_get_addr) and read/write every field as a struct offset. The address is +// resolved at each hot entry point and threaded down through `ts` so no callee +// re-fetches it. +// +// Field-by-field provenance (was N separate `_Thread_local` globals): +typedef struct SanCovTLS { + // Schedule-aware target context (ScheduleControl). When non-NULL ALL edges + // route here regardless of task/thread. Set in sancov_set_target_context. + SanCovMeasurementContext* target_context; + // Non-async fallback coverage map (lazily calloc'd by ensure_tls_coverage_map). + uint8_t* coverage_map; + // Pseudo-task id for synchronous code outside any async context. + void* sync_pseudo_task; + // Hot-path cache: last resolved (task → map) pair and the liveness epoch it + // was resolved under. See get_current_coverage_map's fast path. + void* cached_task; + uint8_t* cached_task_map; + SanCovMeasurementContext* cached_measurement_context; // refcounted (see set_tls_measurement_context) + uint8_t* cached_coverage_map; + uint64_t cached_generation; + // Re-entry guard: set while inside a cmp recorder / reset hook so a + // comparison fired by the recorder cannot re-dispatch and recurse + // (CmpRecorderTests stack-overflow; the cmp twin of in_edge_observer). + bool in_cmp_recorder; + // Re-entry guard: set while inside an edge observer callback so edges fired + // BY the callback never re-enter it (non-reentrant-lock deadlock). + bool in_edge_observer; + // Generation guard: set by the fuzz loop around input generation/mutation, + // which executes instrumented SUT code (e.g. a type-directed generator + // calling getTyp) whose edges/comparisons are NOT the property under test — + // they are reset away before the test runs, so dispatching+recording them is + // pure waste (~25% of the process; scheduler-lab Finding 41p). When set, the + // dispatch hooks early-return on this thread. Per-thread so concurrent engines + // (one mutating, one testing) don't suppress each other. + bool suppressed; +} SanCovTLS; + +static _Thread_local SanCovTLS g_tls = {0}; + +// One tlv_get_addr; callees take the returned pointer as `ts` and never re-fetch. +// always_inline so the hot dispatch paths fold the TLS fetch in-line (no call +// frame, and the compiler keeps the resolved base in a register). +__attribute__((always_inline)) +static inline SanCovTLS* sancov_tls(void) { return &g_tls; } // Key pointer for coverage inheritance task local. When set, child tasks // inherit their parent's measurement context via Swift task locals. Atomic @@ -338,30 +385,25 @@ static SanCovMeasurementContext* retain_inherited_if_valid(uint64_t handle); // Defined further below (after the refcount helpers); forward-declared so the // testing seams above can reset the calling thread's cached measurement context. -static void set_tls_measurement_context(SanCovMeasurementContext* new_ctx); +static void set_tls_measurement_context(SanCovTLS* ts, SanCovMeasurementContext* new_ctx); -// Thread-local fallback for non-async contexts -static _Thread_local uint8_t *tls_coverage_map = NULL; +// (Thread-local fallback map now lives in SanCovTLS.coverage_map.) // Measurement registry: task_id -> measurement_context static ck_ht_t g_measurement_ht; static pthread_once_t g_measurement_ht_once = PTHREAD_ONCE_INIT; static pthread_rwlock_t g_measurement_ht_lock = PTHREAD_RWLOCK_INITIALIZER; -// Thread-local pseudo-task ID for synchronous code outside async contexts -static _Thread_local void* tls_sync_pseudo_task = NULL; +// (Pseudo-task id now lives in SanCovTLS.sync_pseudo_task.) // Global generation counter - incremented when any measurement context ends. // Used to invalidate stale TLS caches across all threads. static _Atomic uint64_t g_measurement_generation = 0; -// Thread-local cache for coverage map lookup (avoids rwlock acquisition in hot path) -// The cache is invalidated when task changes or measurement context ends -static _Thread_local void* tls_cached_task = NULL; -static _Thread_local uint8_t* tls_cached_task_map = NULL; -static _Thread_local SanCovMeasurementContext* tls_cached_measurement_context = NULL; -static _Thread_local uint8_t* tls_cached_coverage_map = NULL; -static _Thread_local uint64_t tls_cached_generation = 0; +// (Hot-path cache fields — cached_task / cached_task_map / +// cached_measurement_context / cached_coverage_map / cached_generation — now +// live in SanCovTLS. The cache is invalidated when task changes or a +// measurement context ends.) // Silent diagnostic counters tracking which path resolved get_current_coverage_map. // Enabled per-test by tests that want to verify routing behavior. No fprintf, @@ -388,24 +430,58 @@ static _Atomic uint64_t g_route_tlsfb_sync_pseudo_task = 0; static _Atomic uint64_t g_route_tlsfb_real_task_no_head = 0; static _Atomic uint64_t g_route_tlsfb_real_task_no_match = 0; +// Dispatch counters (env-gated PTK_DISPATCH_COUNT): count the edge vs cmp +// dispatches that actually pay the per-thread TLS fetch (post-filter, post- +// suppress) so we can see which channel dominates tlv_get_addr. Relaxed atomics +// — only the RATIO matters, so the cross-core contention they add to the +// counting run is irrelevant. `cmp_recorded` counts the kept cmp dispatches that +// reached an attached recorder; (cmp - cmp_recorded) is TLS paid with no +// consumer (edge-only strategies still fire the trace-cmp hooks). Dumped to +// stderr at exit. Off ⇒ one predicted-not-taken load on the hot path. +static bool g_dispatch_count_on = false; +static _Atomic uint64_t g_dispatch_edge_count = 0; +static _Atomic uint64_t g_dispatch_cmp_count = 0; +static _Atomic uint64_t g_dispatch_cmp_recorded = 0; + +// Process-global count of measurement contexts with a cmp recorder attached. +// sancov_dispatch_cmp early-returns before the TLS fetch when this is 0, so +// edge-only strategies (no cmp consumer) don't pay cmp-routing for comparisons +// nobody reads (Finding 42: ~33M unconsumed cmp TLS fetches / 6s). Adjusted only +// at recorder attach/detach (measurement setup/teardown), never on the hot path. +static _Atomic int g_cmp_recorder_count = 0; + +// Apply a cmp_recorder_bits transition to the global count: 0→nonzero attaches +// (+1), nonzero→0 detaches (-1), nonzero→nonzero (re-attach) is a no-op. +static inline void cmp_recorder_count_adjust(uintptr_t old_bits, uintptr_t new_bits) { + if (old_bits == 0 && new_bits != 0) { + atomic_fetch_add_explicit(&g_cmp_recorder_count, 1, memory_order_acq_rel); + } else if (old_bits != 0 && new_bits == 0) { + atomic_fetch_sub_explicit(&g_cmp_recorder_count, 1, memory_order_acq_rel); + } +} + +int sancov_cmp_recorder_count_for_testing(void) { + return atomic_load_explicit(&g_cmp_recorder_count, memory_order_acquire); +} + // Get or create a pseudo-task ID for synchronous code -static void* get_sync_pseudo_task(void) { - if (tls_sync_pseudo_task == NULL) { +static void* get_sync_pseudo_task(SanCovTLS* ts) { + if (ts->sync_pseudo_task == NULL) { // Use a unique heap address as pseudo-task ID - tls_sync_pseudo_task = xmalloc(1); + ts->sync_pseudo_task = xmalloc(1); } - return tls_sync_pseudo_task; + return ts->sync_pseudo_task; } // Get the current task (Swift task or sync pseudo-task) -static void* get_current_task_for_measurement(void) { +static void* get_current_task_for_measurement(SanCovTLS* ts) { if (swift_task_getCurrent != NULL) { void* task = swift_task_getCurrent(); if (task != NULL) { return task; } - } - return get_sync_pseudo_task(); + } + return get_sync_pseudo_task(ts); } // MARK: - ck_ht-based Lock-Free Hash Table Operations @@ -555,7 +631,7 @@ static void remove_measurement_context_for_task(void* task_id) { // same task that began the measurement (matches end_measurement's contract). void sancov_unregister_inheritance_for_testing(SanCovMeasurementContext* context) { unregister_active_inheritance_context(context); - remove_measurement_context_for_task(get_current_task_for_measurement()); + remove_measurement_context_for_task(get_current_task_for_measurement(sancov_tls())); } // TESTING ONLY (see header): drop just the current task's measurement-registry @@ -564,16 +640,17 @@ void sancov_unregister_inheritance_for_testing(SanCovMeasurementContext* context // otherwise the owning thread's cached map pointer would keep routing the // owning task's edges into the context after the registry entry is gone. void sancov_remove_task_measurement_for_testing(void) { - remove_measurement_context_for_task(get_current_task_for_measurement()); + SanCovTLS* ts = sancov_tls(); + remove_measurement_context_for_task(get_current_task_for_measurement(ts)); // Clear this thread's hot-path cache so a stale cached map pointer can't keep // routing the owning task's edges into the (now-deregistered) context. The // epoch bump covers inheritance-active readers; clearing the TLS cache also // covers the !inheritance_active fast-path short-circuit (which ignores the // epoch). Mirrors the cache teardown in sancov_end_measurement. - set_tls_measurement_context(NULL); - tls_cached_task = NULL; - tls_cached_task_map = NULL; - tls_cached_coverage_map = NULL; + set_tls_measurement_context(ts, NULL); + ts->cached_task = NULL; + ts->cached_task_map = NULL; + ts->cached_coverage_map = NULL; atomic_fetch_add_explicit(&g_active_ctx_epoch, 1, memory_order_release); } @@ -658,6 +735,7 @@ static void ctx_retain(SanCovMeasurementContext* ctx) { // Defined below with the recorder API; ONE release path so a future fix to // the data-release semantics cannot land in one copy and miss the other. static void release_recorder_data(SanCovMeasurementContext* context); +static void release_cmp_recorder_data(SanCovMeasurementContext* context); // Release a measurement context (decrement refcount, free if zero) static void ctx_release(SanCovMeasurementContext* ctx) { @@ -669,6 +747,7 @@ static void ctx_release(SanCovMeasurementContext* ctx) { // with the last reference gone no thread can still dispatch into // this context, so releasing the data here can race nothing. release_recorder_data(ctx); + release_cmp_recorder_data(ctx); cleanup_task_map(ctx); free(ctx->covered_indices); free(ctx); @@ -677,11 +756,11 @@ static void ctx_release(SanCovMeasurementContext* ctx) { } // Helper to update TLS cached measurement context with proper refcounting -static void set_tls_measurement_context(SanCovMeasurementContext* new_ctx) { - SanCovMeasurementContext* old_ctx = tls_cached_measurement_context; +static void set_tls_measurement_context(SanCovTLS* ts, SanCovMeasurementContext* new_ctx) { + SanCovMeasurementContext* old_ctx = ts->cached_measurement_context; if (old_ctx != new_ctx) { ctx_retain(new_ctx); // Retain new (NULL is safe) - tls_cached_measurement_context = new_ctx; + ts->cached_measurement_context = new_ctx; ctx_release(old_ctx); // Release old (NULL is safe) } } @@ -740,6 +819,10 @@ static void init_recorder_fields(SanCovMeasurementContext* ctx) { ctx->recorder_data = NULL; ctx->recorder_reset_bits = 0; ctx->recorder_release_bits = 0; + ctx->cmp_recorder_bits = 0; + ctx->cmp_recorder_data = NULL; + ctx->cmp_recorder_reset_bits = 0; + ctx->cmp_recorder_release_bits = 0; } SanCovMeasurementContext* sancov_begin_measurement(void) { @@ -760,7 +843,8 @@ SanCovMeasurementContext* sancov_begin_measurement(void) { atomic_init(&ctx->refcount, 1); // Start with refcount of 1 (owner reference) // Associate this measurement context with the current task - void* task = get_current_task_for_measurement(); + SanCovTLS* ts = sancov_tls(); + void* task = get_current_task_for_measurement(ts); if (!set_measurement_context_for_task(task, ctx)) { fprintf(stderr, "FATAL: failed to register measurement context for task %p\n", task); abort(); @@ -773,10 +857,10 @@ SanCovMeasurementContext* sancov_begin_measurement(void) { ctx->coverage_map = map; // Populate TLS caches for the current thread (may help if no hop occurs) - set_tls_measurement_context(ctx); - tls_cached_coverage_map = map; - tls_cached_task = task; - tls_cached_task_map = map; + set_tls_measurement_context(ts, ctx); + ts->cached_coverage_map = map; + ts->cached_task = task; + ts->cached_task_map = map; } } @@ -800,10 +884,21 @@ SanCovMeasurementContext* sancov_create_dummy_context(void) { return ctx; } +// (The in-cmp-recorder re-entry guard now lives in SanCovTLS.in_cmp_recorder.) +// A cmp recorder's OWN body — and any reset hook — contains instrumented +// comparisons whenever it is compiled into a trace-cmp module; each such +// comparison fires __sanitizer_cov_trace_cmp* -> sancov_dispatch_cmp, which +// would re-enter the recorder and recurse without bound (observed as a 500-deep +// stack overflow / SIGBUS in CmpRecorderTests, whose recorders live in the +// trace-cmp-instrumented test target). This is the cmp twin of +// in_edge_observer: while set, sancov_dispatch_cmp is a no-op so a recorder can +// never re-dispatch into itself. + /// Reset coverage for a measurement context (cheap memset, O(1) for covered_count). /// Used between iterations in the fuzz loop to avoid hash table insert/remove overhead. void sancov_reset_coverage(SanCovMeasurementContext* ctx) { if (ctx == NULL) return; + SanCovTLS* ts = sancov_tls(); if (ctx->coverage_map != NULL && g_guard_count > 0) { memset(ctx->coverage_map, 0, g_guard_count); @@ -813,8 +908,8 @@ void sancov_reset_coverage(SanCovMeasurementContext* ctx) { // Clear the calling thread's TLS-cached coverage map pointer so the next // edge that fires on this thread re-routes through get_current_coverage_map. - tls_cached_coverage_map = NULL; - // We deliberately do NOT memset whatever bitmap `tls_cached_task_map` points + ts->cached_coverage_map = NULL; + // We deliberately do NOT memset whatever bitmap `cached_task_map` points // at. Under parallel test execution that pointer can target another active // test's coverage_map (a worker thread previously executed a child task // whose routing populated the cache, then was reassigned to this iteration @@ -830,6 +925,19 @@ void sancov_reset_coverage(SanCovMeasurementContext* ctx) { reset(__atomic_load_n(&ctx->recorder_data, __ATOMIC_ACQUIRE)); } + // Same per-iteration reset for the independent cmp recorder (e.g. clear the + // value-profile feature set so each iteration starts from a clean slate). + // Guard with in_cmp_recorder: a trace-cmp-instrumented reset hook fires + // comparisons of its own, which must not re-dispatch into the (still + // attached) cmp recorder and recurse. + SanCovRecorderDataFn cmp_reset = + (SanCovRecorderDataFn)__atomic_load_n(&ctx->cmp_recorder_reset_bits, __ATOMIC_ACQUIRE); + if (cmp_reset) { + ts->in_cmp_recorder = true; + cmp_reset(__atomic_load_n(&ctx->cmp_recorder_data, __ATOMIC_ACQUIRE)); + ts->in_cmp_recorder = false; + } + } // Release the context's current recorder data through its release hook (if @@ -850,6 +958,17 @@ static void release_recorder_data(SanCovMeasurementContext* context) { } } +// The cmp-recorder twin of release_recorder_data — same exchange-not-load +// reasoning (a race degrades to a leak, never a double release). +static void release_cmp_recorder_data(SanCovMeasurementContext* context) { + SanCovRecorderDataFn release = + (SanCovRecorderDataFn)__atomic_exchange_n(&context->cmp_recorder_release_bits, 0, __ATOMIC_ACQ_REL); + void* data = __atomic_exchange_n(&context->cmp_recorder_data, NULL, __ATOMIC_ACQ_REL); + if (release && data) { + release(data); + } +} + void sancov_context_set_recorder( SanCovMeasurementContext* context, SanCovEdgeRecorder recorder, @@ -879,6 +998,34 @@ void sancov_context_set_recorder( } } +// The cmp-recorder twin of sancov_context_set_recorder — same ordering and +// ownership contract, applied to the independent cmp slot. +void sancov_context_set_cmp_recorder( + SanCovMeasurementContext* context, + SanCovCmpRecorder recorder, + void* data, + SanCovRecorderDataFn reset, + SanCovRecorderDataFn release) { + if (context == NULL) return; + + // Capture the prior recorder so the global cmp-recorder count tracks the + // 0↔nonzero transition (gates sancov_dispatch_cmp — see g_cmp_recorder_count). + uintptr_t old_cmp = __atomic_exchange_n(&context->cmp_recorder_bits, 0, __ATOMIC_RELEASE); + __atomic_store_n(&context->cmp_recorder_reset_bits, 0, __ATOMIC_RELEASE); + release_cmp_recorder_data(context); + + if (recorder) { + __atomic_store_n(&context->cmp_recorder_release_bits, (uintptr_t)release, __ATOMIC_RELEASE); + __atomic_store_n(&context->cmp_recorder_data, data, __ATOMIC_RELEASE); + __atomic_store_n(&context->cmp_recorder_reset_bits, (uintptr_t)reset, __ATOMIC_RELEASE); + __atomic_store_n(&context->cmp_recorder_bits, (uintptr_t)recorder, __ATOMIC_RELEASE); + } else if (release && data) { + // Clear-with-payload: ownership still transferred, release once. + release(data); + } + cmp_recorder_count_adjust(old_cmp, recorder ? (uintptr_t)recorder : 0); +} + // sancov_context_get_recorder_data lives in the header as static inline (hot path). // TESTING ONLY seams (see SanCovHooks.h). @@ -887,6 +1034,11 @@ void* sancov_context_get_recorder_for_testing(SanCovMeasurementContext* context) return (void*)__atomic_load_n(&context->edge_recorder_bits, __ATOMIC_ACQUIRE); } +void* sancov_context_get_cmp_recorder_for_testing(SanCovMeasurementContext* context) { + if (context == NULL) return NULL; + return (void*)__atomic_load_n(&context->cmp_recorder_bits, __ATOMIC_ACQUIRE); +} + void sancov_release_for_testing(SanCovMeasurementContext* context) { ctx_release(context); } @@ -909,24 +1061,29 @@ void sancov_end_measurement(SanCovMeasurementContext* ctx) { // data alive" contract merely documented. __atomic_store_n(&ctx->edge_recorder_bits, 0, __ATOMIC_RELEASE); __atomic_store_n(&ctx->recorder_reset_bits, 0, __ATOMIC_RELEASE); + // Sever the cmp recorder on the same terms (data survives for stragglers). + uintptr_t old_cmp = __atomic_exchange_n(&ctx->cmp_recorder_bits, 0, __ATOMIC_RELEASE); + cmp_recorder_count_adjust(old_cmp, 0); + __atomic_store_n(&ctx->cmp_recorder_reset_bits, 0, __ATOMIC_RELEASE); // Drop the inheritance registration first so concurrent routing decisions // stop matching this context by value pointer before we tear it down. unregister_active_inheritance_context(ctx); // Remove the measurement context from the current task - void* task = get_current_task_for_measurement(); + SanCovTLS* ts = sancov_tls(); + void* task = get_current_task_for_measurement(ts); remove_measurement_context_for_task(task); // Invalidate this thread's TLS cache if it matches // Note: Other threads may still hold TLS references - that's OK because // the refcount will keep the context alive until they release it. - if (tls_cached_measurement_context == ctx) { - set_tls_measurement_context(NULL); // Releases our TLS reference - tls_cached_coverage_map = NULL; + if (ts->cached_measurement_context == ctx) { + set_tls_measurement_context(ts, NULL); // Releases our TLS reference + ts->cached_coverage_map = NULL; } - tls_cached_task = NULL; - tls_cached_task_map = NULL; + ts->cached_task = NULL; + ts->cached_task_map = NULL; // Release the owner reference (context allocated with refcount=1) // The context will be freed when all TLS references are also released @@ -1059,9 +1216,9 @@ uint32_t* sancov_snapshot_covered_indices_with_context(SanCovMeasurementContext* } // Ensure thread-local fallback map is allocated -static void ensure_tls_coverage_map(void) { - if (tls_coverage_map == NULL && g_guard_count > 0) { - tls_coverage_map = (uint8_t*)calloc(g_guard_count, 1); +static void ensure_tls_coverage_map(SanCovTLS* ts) { + if (ts->coverage_map == NULL && g_guard_count > 0) { + ts->coverage_map = (uint8_t*)calloc(g_guard_count, 1); } } @@ -1086,22 +1243,26 @@ static void ensure_tls_coverage_map(void) { #define SANCOV_DISABLE_TLS_CACHE 0 #endif -static uint8_t* get_current_coverage_map(void) { +// Resolves routing using the caller's already-fetched TLS block (`ts`), so the +// per-edge / per-comparison hot path pays a single tlv_get_addr at its entry and +// every field touch here is a struct offset. Behaviour is identical to the old +// per-variable form; only the storage was coalesced (Finding 41c). +static uint8_t* get_current_coverage_map(SanCovTLS* ts) { // HIGHEST PRIORITY: schedule-aware target context. // When schedule fuzzing is active, ALL edge hits go to the engine's context // regardless of which task/thread they fire on. - if (g_target_context != NULL) { + if (ts->target_context != NULL) { atomic_fetch_add_explicit(&g_route_target_ctx, 1, memory_order_relaxed); - // Route all edges to the target context. Set tls_cached_measurement_context + // Route all edges to the target context. Set cached_measurement_context // so the attached recorder/observer and covered_indices are maintained // (observer state guards its own concurrent access from pool threads). - set_tls_measurement_context(g_target_context); - tls_cached_coverage_map = g_target_context->coverage_map; - return g_target_context->coverage_map; + set_tls_measurement_context(ts, ts->target_context); + ts->cached_coverage_map = ts->target_context->coverage_map; + return ts->target_context->coverage_map; } // Get the current task (Swift task or sync pseudo-task) - void* task = get_current_task_for_measurement(); + void* task = get_current_task_for_measurement(ts); bool inheritance_active = (g_coverage_inheritance_key != NULL); // Snapshot the liveness epoch up front. The cached resolution below is only @@ -1117,12 +1278,12 @@ static uint8_t* get_current_coverage_map(void) { // unchanged. An unchanged epoch means no measurement began or ended since we // resolved this task, so (a) the cached context is still the correct routing // target and (b) it is still alive (held by this thread's - // tls_cached_measurement_context reference) — so returning its map needs no + // cached_measurement_context reference) — so returning its map needs no // re-validation and no reference dance. Any begin/end bumps the epoch and // forces the full, lock-protected re-resolve below (which closes TOCTOU/ABA). - if (task == tls_cached_task && tls_cached_task_map != NULL) { - if (!inheritance_active || resolve_epoch == tls_cached_generation) { - return tls_cached_task_map; + if (task == ts->cached_task && ts->cached_task_map != NULL) { + if (!inheritance_active || resolve_epoch == ts->cached_generation) { + return ts->cached_task_map; } // Epoch changed → a begin/end occurred; re-resolve. atomic_fetch_add_explicit(&g_route_tls_cache_inheritance_active, 1, memory_order_relaxed); @@ -1198,11 +1359,11 @@ static uint8_t* get_current_coverage_map(void) { atomic_fetch_add_explicit(&g_route_inherited_manualwalk, 1, memory_order_relaxed); } uint8_t* map = inherited->coverage_map; - tls_cached_task = task; - tls_cached_task_map = map; - tls_cached_generation = resolve_epoch; - set_tls_measurement_context(inherited); // takes its own reference - ctx_release(inherited); // drop our temporary reference + ts->cached_task = task; + ts->cached_task_map = map; + ts->cached_generation = resolve_epoch; + set_tls_measurement_context(ts, inherited); // takes its own reference + ctx_release(inherited); // drop our temporary reference return map; } // Live but no coverage_map yet: drop the temporary reference and fall @@ -1217,22 +1378,22 @@ static uint8_t* get_current_coverage_map(void) { atomic_fetch_add_explicit(&g_route_per_task_registry, 1, memory_order_relaxed); #if !SANCOV_DISABLE_TLS_CACHE // Check measurement context cache - if (measurement_ctx == tls_cached_measurement_context && tls_cached_coverage_map != NULL) { + if (measurement_ctx == ts->cached_measurement_context && ts->cached_coverage_map != NULL) { // Update task cache to point to measurement map - tls_cached_task = task; - tls_cached_task_map = tls_cached_coverage_map; - tls_cached_generation = resolve_epoch; - return tls_cached_coverage_map; + ts->cached_task = task; + ts->cached_task_map = ts->cached_coverage_map; + ts->cached_generation = resolve_epoch; + return ts->cached_coverage_map; } #endif // Slow path: lookup or create, then cache uint8_t* map = find_or_create_task_map(measurement_ctx); if (map != NULL) { - set_tls_measurement_context(measurement_ctx); // Properly retain/release - tls_cached_coverage_map = map; - tls_cached_task = task; - tls_cached_task_map = map; - tls_cached_generation = resolve_epoch; + set_tls_measurement_context(ts, measurement_ctx); // Properly retain/release + ts->cached_coverage_map = map; + ts->cached_task = task; + ts->cached_task_map = map; + ts->cached_generation = resolve_epoch; return map; } } @@ -1264,14 +1425,14 @@ static uint8_t* get_current_coverage_map(void) { } else { atomic_fetch_add_explicit(&g_route_tls_fallback_no_inheritance, 1, memory_order_relaxed); } - ensure_tls_coverage_map(); - tls_cached_task = task; - tls_cached_task_map = tls_coverage_map; - tls_cached_generation = resolve_epoch; + ensure_tls_coverage_map(ts); + ts->cached_task = task; + ts->cached_task_map = ts->coverage_map; + ts->cached_generation = resolve_epoch; // Clear stale measurement context so dispatched edges don't append // edges from this task into another test's measurement context. - set_tls_measurement_context(NULL); - return tls_coverage_map; + set_tls_measurement_context(ts, NULL); + return ts->coverage_map; } // Diagnostic: read routing path counters. Tests can use this to verify that @@ -1360,34 +1521,36 @@ SanCovEdgeRecording sancov_record_edge_first_hit(uint32_t* guard, uint8_t* map, return record_first_hit(*guard, map, ctx) ? SANCOV_EDGE_FIRST_HIT : SANCOV_EDGE_REPEAT; } -// Set while the calling thread is inside an observer callback, so edges fired -// BY the callback never re-enter it (see header: re-entry deadlocks any -// non-reentrant lock the callback holds). -static _Thread_local bool tls_in_edge_observer = false; +// (The in-edge-observer re-entry guard now lives in SanCovTLS.in_edge_observer: +// set while the calling thread is inside an observer callback, so edges fired BY +// the callback never re-enter it — re-entry deadlocks any non-reentrant lock the +// callback holds.) bool sancov_observer_enter(void) { - if (tls_in_edge_observer) return false; - tls_in_edge_observer = true; + SanCovTLS* ts = sancov_tls(); + if (ts->in_edge_observer) return false; + ts->in_edge_observer = true; return true; } void sancov_observer_exit(void) { - tls_in_edge_observer = false; + sancov_tls()->in_edge_observer = false; } // MARK: - Schedule-Aware Target Context void sancov_set_target_context(SanCovMeasurementContext* context) { - g_target_context = context; - // The target interlude rebinds tls_cached_measurement_context to the - // target while leaving the per-task fast path's (task, map) pairing - // intact. A post-interlude dispatch would then take the fast path and - // pair the task's own map with the target's still-cached context — - // silently appending covered indices (and firing the recorder/observer) - // on the wrong engine. Dropping the task cache forces the next dispatch - // through the full resolve, which re-pairs map and context together. - tls_cached_task = NULL; - tls_cached_task_map = NULL; + SanCovTLS* ts = sancov_tls(); + ts->target_context = context; + // The target interlude rebinds cached_measurement_context to the target + // while leaving the per-task fast path's (task, map) pairing intact. A + // post-interlude dispatch would then take the fast path and pair the task's + // own map with the target's still-cached context — silently appending + // covered indices (and firing the recorder/observer) on the wrong engine. + // Dropping the task cache forces the next dispatch through the full resolve, + // which re-pairs map and context together. + ts->cached_task = NULL; + ts->cached_task_map = NULL; } // MARK: - Coverage Inheritance (Task-Local Propagation) @@ -1456,9 +1619,29 @@ void sancov_rebuild_covered_indices_from_map(SanCovMeasurementContext* ctx) { // with the __atomic builtins (a fn-ptr _Atomic is rejected; cast on load — // fn-ptr ↔ uintptr_t round-trips losslessly on every supported target, the // same assumption dlsym relies on). +// Process-global "ever-covered" edge bitmap (diagnostic). See the API block +// near the bottom of this file. Default NULL ⇒ recording disabled ⇒ the hot +// path below pays one predicted-not-taken atomic load. Once enabled, every +// allowed edge fire sets a byte to 1; nothing in the fuzz loop ever clears it +// (only sancov_reset_global_ever_covered). Writes are idempotent stores of the +// constant 1 — concurrent engines writing the same value to the same byte is a +// benign race (the only transition is 0→1, no torn value for a single byte). +static _Atomic(uint8_t*) g_ever_covered = NULL; + void sancov_dispatch_edge(uint32_t *guard) { - uint8_t* map = get_current_coverage_map(); - SanCovMeasurementContext* ctx = tls_cached_measurement_context; + uint8_t* ever = atomic_load_explicit(&g_ever_covered, memory_order_acquire); + if (__builtin_expect(ever != NULL, 0)) { + uint32_t ge = *guard; + if (ge < g_guard_count) ever[ge] = 1; // idempotent; see note above + } + SanCovTLS* ts = sancov_tls(); // one tlv_get_addr for the whole dispatch + // Generation guard: skip routing+recording for edges fired by input + // generation/mutation (not the property under test). See SanCovTLS.suppressed. + if (ts->suppressed) return; + if (__builtin_expect(g_dispatch_count_on, 0)) + atomic_fetch_add_explicit(&g_dispatch_edge_count, 1, memory_order_relaxed); + uint8_t* map = get_current_coverage_map(ts); + SanCovMeasurementContext* ctx = ts->cached_measurement_context; if (ctx) { SanCovEdgeRecorder r = (SanCovEdgeRecorder)__atomic_load_n(&ctx->edge_recorder_bits, __ATOMIC_ACQUIRE); if (r) { @@ -1469,39 +1652,259 @@ void sancov_dispatch_edge(uint32_t *guard) { sancov_recorder_default(guard, map, ctx); } -// Forward declarations for lazy edge filter (defined later in file alongside -// the upfront filter helpers). State pointer and state byte values are -// declared here so the hot path can reference them. -#define EDGE_STATE_UNCHECKED 0 -#define EDGE_STATE_ALLOWED 1 -#define EDGE_STATE_SKIP 2 -extern uint8_t* g_edge_state; -static void check_and_cache_edge_lazy(uint32_t* guard, uint32_t g); - void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { - // Fast-path: out-of-range or upfront-cached-SKIP guards (set to - // SANCOV_GUARD_SKIP once, under pthread_once, before any edge fires) skip. - // `*guard` is never written after that init barrier, so this read is + // Out-of-range guards (uninitialized, or from a module sized differently + // than g_guard_count) skip. Compiler-generated edges are NOT filtered here + // anymore: the TagCompilerGenerated LLVM pass plugin tags those functions + // NoSanitizeCoverage at compile time, so SanCov never emits guards for them + // (this also keeps async resume/yield edges out, preserving pathTrie + // determinism). `*guard` is never written after init, so this read is // race-free under parallel fuzzing. uint32_t g = *guard; if (g >= g_guard_count) return; + sancov_dispatch_edge(guard); +} - // Lazy filter: classify on first fire of each edge, then cache the verdict - // in g_edge_state (atomic). The classification — NOT a `*guard` stamp — is - // the single source of truth, so concurrent engines firing the same edge do - // not race on the shared guard global (TSan-confirmed fix). - if (__builtin_expect(g_edge_state != NULL, 1)) { - uint8_t state = __atomic_load_n(&g_edge_state[g], __ATOMIC_ACQUIRE); - if (__builtin_expect(state == EDGE_STATE_UNCHECKED, 0)) { - check_and_cache_edge_lazy(guard, g); - // Re-read the cached verdict: SKIP → suppress. - if (__atomic_load_n(&g_edge_state[g], __ATOMIC_ACQUIRE) == EDGE_STATE_SKIP) return; - } else if (state == EDGE_STATE_SKIP) { +// MARK: - Comparison Census (diagnostic, env-gated: PTK_CMP_CENSUS=) +// +// Answers "is comparison VOLUME concentrated in a few sites, and do the hot +// sites approach the boundary?" — i.e. is there a filterable population, or is +// the volume the relevant SUT-logic comparisons themselves (scheduler-lab +// Finding 41f follow-up). Records per comparison-site PC: fire count and the +// minimum |arg1-arg2| ever seen. Symbol resolution (dladdr) is deferred to the +// atexit dump, so the per-comparison cost is one CAS-claim + two relaxed RMWs on +// a fixed open-addressing table — and ZERO when disabled (one predicted-not-taken +// atomic load of g_cmp_census, same pattern as g_ever_covered). Enabled once at +// load via the constructor below; never touches production unless the env is set. +typedef struct { + _Atomic uint64_t pc; // 0 = empty slot + _Atomic uint64_t count; // fire volume + _Atomic uint64_t min_dist; // min |arg1-arg2|, starts UINT64_MAX +} CmpCensusEntry; + +typedef struct { + CmpCensusEntry* slots; + size_t capacity; // power of two + const char* path; +} CmpCensus; + +static CmpCensus* _Atomic g_cmp_census = NULL; + +static inline uint64_t cmp_census_hash(uint64_t x) { + x += 0x9E3779B97F4A7C15ULL; + x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL; + x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL; + return x ^ (x >> 31); +} + +static void cmp_census_record(uint64_t pc, uint64_t arg1, uint64_t arg2) { + CmpCensus* c = atomic_load_explicit(&g_cmp_census, memory_order_acquire); + if (__builtin_expect(c == NULL, 1)) return; + uint64_t dist = arg1 > arg2 ? arg1 - arg2 : arg2 - arg1; + size_t m = c->capacity - 1; + size_t i = (size_t)(cmp_census_hash(pc) & (uint64_t)m); + for (size_t probes = 0; probes <= m; probes++) { + CmpCensusEntry* e = &c->slots[i]; + uint64_t k = atomic_load_explicit(&e->pc, memory_order_relaxed); + if (k == 0) { + uint64_t expected = 0; + if (!atomic_compare_exchange_strong_explicit( + &e->pc, &expected, pc, memory_order_acq_rel, memory_order_relaxed) + && expected != pc) { + i = (i + 1) & m; // lost claim to a different pc; keep probing + continue; + } + // won the claim, or another thread claimed it for THIS pc — fall through + k = pc; + } + if (k == pc) { + atomic_fetch_add_explicit(&e->count, 1, memory_order_relaxed); + uint64_t cur = atomic_load_explicit(&e->min_dist, memory_order_relaxed); + while (dist < cur) { + if (atomic_compare_exchange_weak_explicit( + &e->min_dist, &cur, dist, memory_order_relaxed, memory_order_relaxed)) + break; + } return; } + i = (i + 1) & m; + } + // table full: drop (census is best-effort) +} + +static void cmp_census_dump(void) { + CmpCensus* c = atomic_load_explicit(&g_cmp_census, memory_order_acquire); + if (c == NULL) return; + FILE* f = fopen(c->path, "w"); + if (f == NULL) return; + fprintf(f, "# count\tmin_dist\tpc\tsymbol\n"); + for (size_t i = 0; i < c->capacity; i++) { + uint64_t pc = atomic_load_explicit(&c->slots[i].pc, memory_order_relaxed); + if (pc == 0) continue; + uint64_t count = atomic_load_explicit(&c->slots[i].count, memory_order_relaxed); + uint64_t md = atomic_load_explicit(&c->slots[i].min_dist, memory_order_relaxed); + const char* sym = "?"; + Dl_info info; + if (dladdr((void*)(uintptr_t)pc, &info) && info.dli_sname) sym = info.dli_sname; + fprintf(f, "%llu\t%llu\t0x%llx\t%s\n", + (unsigned long long)count, + (unsigned long long)(md == UINT64_MAX ? 0 : md), + (unsigned long long)pc, sym); + } + fclose(f); +} + +__attribute__((constructor)) +static void cmp_census_init(void) { + const char* path = getenv("PTK_CMP_CENSUS"); + if (path == NULL || path[0] == '\0') return; + CmpCensus* c = (CmpCensus*)xmalloc(sizeof(CmpCensus)); + c->capacity = 16384; // power of two; ≫ any workload's distinct cmp-site count + c->slots = (CmpCensusEntry*)calloc(c->capacity, sizeof(CmpCensusEntry)); + if (c->slots == NULL) { free(c); return; } + for (size_t i = 0; i < c->capacity; i++) { + atomic_init(&c->slots[i].min_dist, UINT64_MAX); + } + c->path = path; + atomic_store_explicit(&g_cmp_census, c, memory_order_release); + atexit(cmp_census_dump); +} + +static void dispatch_count_dump(void) { + if (!g_dispatch_count_on) return; + unsigned long long e = atomic_load_explicit(&g_dispatch_edge_count, memory_order_relaxed); + unsigned long long c = atomic_load_explicit(&g_dispatch_cmp_count, memory_order_relaxed); + unsigned long long cr = atomic_load_explicit(&g_dispatch_cmp_recorded, memory_order_relaxed); + fprintf(stderr, + "=== PTK_DISPATCH_COUNT ===\n" + "edge_dispatches %llu\n" + "cmp_dispatches %llu\n" + "cmp_recorded %llu (cmp - recorded = %llu paid TLS with no consumer)\n", + e, c, cr, (c >= cr ? c - cr : 0)); +} + +__attribute__((constructor)) +static void dispatch_count_init(void) { + const char* v = getenv("PTK_DISPATCH_COUNT"); + if (v == NULL || v[0] == '\0' || v[0] == '0') return; + g_dispatch_count_on = true; + atexit(dispatch_count_dump); +} + +// MARK: - Comparison Dispatch (trace-cmp / value profile) + +// Per-comparison dispatch: resolve routing once (same current-context lookup as +// sancov_dispatch_edge — get_current_coverage_map populates the TLS context as +// a side effect), then run the context's cmp recorder if one is attached. No +// edge map is touched; cmp recording is a parallel channel. No-op when no cmp +// recorder is attached or no measurement is active. +// Generation guard control (see SanCovTLS.suppressed). Set true around input +// generation/mutation so this thread's instrumented SUT calls aren't dispatched +// or recorded; set false before the property runs. Per-thread; cheap (the bool +// lives in the already-fetched TLS struct). +void sancov_set_dispatch_suppressed(bool suppressed) { + sancov_tls()->suppressed = suppressed; +} + +bool sancov_dispatch_is_suppressed(void) { + return sancov_tls()->suppressed; +} + +void sancov_dispatch_cmp(uintptr_t pc, uint64_t arg1, uint64_t arg2, uint32_t size_bytes) { + // No consumer anywhere → skip EVERYTHING (TLS fetch + routing). Edge-only + // strategies (newEdge / hitCountBuckets / pathTrie / signatureMatch) attach no + // cmp recorder, so every comparison would otherwise pay sancov_tls() + + // get_current_coverage_map() (~33M/6s — Finding 42) for nothing. Checked FIRST: + // both are plain global loads (no TLS), so the gate is the cheapest possible + // early-out. The census exemption keeps PTK_CMP_CENSUS working when enabled + // without a recorder. A MIXED run (some engine has a recorder) keeps the count + // >0, so a real consumer is never suppressed. + // + // Synthesized/stdlib comparison sites (bounds/overflow/precondition trap + // guards) are no longer dropped here: the EmitCmpTrace LLVM pass plugin omits + // their trace_cmp callbacks at compile time, so they never reach this dispatch. + if (atomic_load_explicit(&g_cmp_recorder_count, memory_order_acquire) == 0 && + atomic_load_explicit(&g_cmp_census, memory_order_acquire) == NULL) return; + // Fetch this thread's TLS block ONCE (single tlv_get_addr) for the kept sites. + SanCovTLS* ts = sancov_tls(); + // Re-entry guard (see SanCovTLS.in_cmp_recorder): a comparison fired by the + // recorder itself (or by a reset hook we are invoking) must NOT re-dispatch, + // or the recorder recurses into itself and overflows the stack. + if (ts->in_cmp_recorder) return; + // Generation guard: skip census + routing + recording for comparisons fired + // by input generation/mutation (not the property under test). Kept comparisons + // (SUT funcs like getTyp the mutator calls to validate mutants) reach here; + // dropped ones already returned at the drop check above. See SanCovTLS.suppressed. + if (ts->suppressed) return; + if (__builtin_expect(g_dispatch_count_on, 0)) + atomic_fetch_add_explicit(&g_dispatch_cmp_count, 1, memory_order_relaxed); + // Diagnostic census (env-gated; one predicted-not-taken load when disabled). + // Placed after the re-entry guard so it counts only genuine SUT comparisons, + // not the recorder's own internal ones. + cmp_census_record(pc, arg1, arg2); + // Resolve the calling thread's current measurement context. We don't need + // the returned map, but the call refreshes cached_measurement_context. + (void)get_current_coverage_map(ts); + SanCovMeasurementContext* ctx = ts->cached_measurement_context; + if (!ctx) return; + SanCovCmpRecorder r = (SanCovCmpRecorder)__atomic_load_n(&ctx->cmp_recorder_bits, __ATOMIC_ACQUIRE); + if (r) { + if (__builtin_expect(g_dispatch_count_on, 0)) + atomic_fetch_add_explicit(&g_dispatch_cmp_recorded, 1, memory_order_relaxed); + ts->in_cmp_recorder = true; + r(pc, arg1, arg2, size_bytes, ctx); + ts->in_cmp_recorder = false; } +} - sancov_dispatch_edge(guard); +// SanitizerCoverage comparison hooks. The compiler emits a call to one of these +// before each instrumented integer comparison / switch, passing the operands. +// We capture the call site via __builtin_return_address(0) as the comparison's +// PC (stable per comparison site) and forward to sancov_dispatch_cmp. const_cmp +// variants (one operand a compile-time constant) route identically — the +// recorder decides whether to treat constants specially. +// +// These run on EVERY comparison in instrumented code (including Swift runtime +// internals: refcounts, bounds checks, address compares), so the recorder MUST +// key by PC to isolate the comparisons it cares about from runtime chatter. +void __sanitizer_cov_trace_cmp1(uint8_t arg1, uint8_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 1); +} +void __sanitizer_cov_trace_cmp2(uint16_t arg1, uint16_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 2); +} +void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 4); +} +void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 8); +} +void __sanitizer_cov_trace_const_cmp1(uint8_t arg1, uint8_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 1); +} +void __sanitizer_cov_trace_const_cmp2(uint16_t arg1, uint16_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 2); +} +void __sanitizer_cov_trace_const_cmp4(uint32_t arg1, uint32_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 4); +} +void __sanitizer_cov_trace_const_cmp8(uint64_t arg1, uint64_t arg2) { + sancov_dispatch_cmp((uintptr_t)__builtin_return_address(0), arg1, arg2, 8); +} + +// switch: cases[0] = number of case constants, cases[1] = value bit width, +// cases[2..] = the case constants (ascending). Emit one comparison per case +// (val vs constant) so the value profile sees how close val came to each arm — +// the switch analog of the cmp gradient. +void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases) { + if (cases == NULL) return; + uint64_t n = cases[0]; + uint32_t size_bytes = (uint32_t)(cases[1] / 8); + if (size_bytes == 0) size_bytes = 8; + uintptr_t pc = (uintptr_t)__builtin_return_address(0); + for (uint64_t i = 0; i < n; i++) { + sancov_dispatch_cmp(pc, val, cases[2 + i], size_bytes); + } } // MARK: - PC Storage for Source Mapping @@ -1529,6 +1932,66 @@ size_t sancov_get_counter_count(void) { return g_guard_count; } +// MARK: - Process-global "ever-covered" edge bitmap (diagnostic) +// +// (Storage `g_ever_covered` and the hot-path write live with +// sancov_dispatch_edge above.) This accumulator is the answer to "did a fuzz +// run reach full SUT coverage?" without the confounds that make the per-task +// context and the corpus unsuitable: the context is reset every iteration and +// the corpus only banks ADMITTED inputs, so neither holds the true union of +// edges executed across a whole run. The global bitmap does — it is set on +// every allowed edge fire and only cleared by sancov_reset_global_ever_covered. +// +// All four entry points are intended for a single-threaded diagnostic harness +// between runs; the per-edge write is the only thing that runs under the +// parallel fuzz loop. + +void sancov_enable_global_ever_covered(void) { + if (g_guard_count == 0) return; + if (atomic_load_explicit(&g_ever_covered, memory_order_acquire) != NULL) return; + uint8_t* buf = (uint8_t*)calloc(g_guard_count, 1); + if (!buf) return; + uint8_t* expected = NULL; + // CAS so a racing second enable doesn't leak a buffer; first writer wins. + if (!atomic_compare_exchange_strong_explicit(&g_ever_covered, &expected, buf, + memory_order_acq_rel, memory_order_acquire)) { + free(buf); + } +} + +void sancov_reset_global_ever_covered(void) { + uint8_t* buf = atomic_load_explicit(&g_ever_covered, memory_order_acquire); + if (buf && g_guard_count > 0) memset(buf, 0, g_guard_count); +} + +size_t sancov_global_ever_covered_count(void) { + uint8_t* buf = atomic_load_explicit(&g_ever_covered, memory_order_acquire); + if (!buf) return 0; + size_t n = 0; + for (size_t i = 0; i < g_guard_count; i++) { + if (buf[i]) n++; + } + return n; +} + +uint32_t* sancov_snapshot_global_ever_covered(size_t* out_count) { + if (out_count) *out_count = 0; + uint8_t* buf = atomic_load_explicit(&g_ever_covered, memory_order_acquire); + if (!buf || g_guard_count == 0) return NULL; + size_t n = 0; + for (size_t i = 0; i < g_guard_count; i++) { + if (buf[i]) n++; + } + if (n == 0) return NULL; + uint32_t* out = (uint32_t*)xmalloc(n * sizeof(uint32_t)); + size_t k = 0; + for (size_t i = 0; i < g_guard_count && k < n; i++) { + if (buf[i]) out[k++] = (uint32_t)i; + } + if (out_count) *out_count = k; + return out; +} + // MARK: - PC-to-Source Mapping Implementation bool sancov_pcs_available(void) { @@ -1564,254 +2027,3 @@ bool sancov_get_source_location(size_t edge_index, SanCovSourceLocation* locatio return true; } - -// MARK: - Edge Filter - -static size_t g_filtered_count = 0; -static bool g_filter_applied = false; - -// MARK: - Lazy Edge Filter + Disk Cache -// -// Replaces the upfront `dladdr` scan with a per-edge first-fire check, results -// of which are persisted to disk and re-applied on subsequent process runs of -// the same binary. After warm-up, both first-fire and subsequent fires of any -// known edge cost ~1 byte load + 1 branch. -// -// Edge state values defined above next to the hot path (forward decls). - -uint8_t* g_edge_state = NULL; // size = g_guard_count when allocated -static size_t g_lazy_filtered_count = 0; -static size_t g_lazy_allowed_count = 0; -static int g_edge_state_dirty = 0; // atomic flag: persist on exit -static pthread_once_t g_filter_init_once = PTHREAD_ONCE_INIT; - -#define SANCOV_FILTER_CACHE_MAGIC ((uint64_t)0x5345434f56523031ULL) // "SECOVR01" - -static void compute_cache_path(char* out, size_t out_size) { - out[0] = '\0'; - if (!g_guards_start) return; - Dl_info info; - if (!dladdr((void*)g_guards_start, &info) || !info.dli_fname) return; - struct stat st; - if (stat(info.dli_fname, &st) != 0) return; - - const char* tmp = getenv("TMPDIR"); - if (!tmp || tmp[0] == '\0') tmp = "/tmp"; - - // Stable per-binary key: inode + mtime. Survives rebuilds via mtime. - // Path: $TMPDIR/sancov-filter--.bin - snprintf(out, out_size, "%ssancov-filter-%llu-%lld.bin", - tmp, (unsigned long long)st.st_ino, - (long long)st.st_mtimespec.tv_sec); -} - -static void load_filter_cache(void) { - char path[1024]; - compute_cache_path(path, sizeof(path)); - if (path[0] == '\0') return; - - int fd = open(path, O_RDONLY); - if (fd < 0) return; - - uint64_t header[2]; - ssize_t n = read(fd, header, sizeof(header)); - if (n != (ssize_t)sizeof(header) || - header[0] != SANCOV_FILTER_CACHE_MAGIC || - header[1] != (uint64_t)g_guard_count) { - close(fd); - return; - } - n = read(fd, g_edge_state, g_guard_count); - close(fd); - if (n != (ssize_t)g_guard_count) { - // Partial read: best-effort, treat unread bytes as UNCHECKED. - memset(g_edge_state + (n > 0 ? n : 0), EDGE_STATE_UNCHECKED, - g_guard_count - (n > 0 ? n : 0)); - return; - } - - // Apply cached SKIP markers to guards eagerly so the existing - // `*guard < g_guard_count` hot-path gate short-circuits without reading - // g_edge_state at all. - size_t loaded_skip = 0, loaded_allowed = 0; - for (size_t i = 0; i < g_guard_count; i++) { - if (g_edge_state[i] == EDGE_STATE_SKIP) { - g_guards_start[i] = SANCOV_GUARD_SKIP; - loaded_skip++; - } else if (g_edge_state[i] == EDGE_STATE_ALLOWED) { - loaded_allowed++; - } - } - g_lazy_filtered_count = loaded_skip; - g_lazy_allowed_count = loaded_allowed; -} - -static void save_filter_cache(void) { - if (!__atomic_load_n(&g_edge_state_dirty, __ATOMIC_ACQUIRE)) return; - if (!g_edge_state || g_guard_count == 0) return; - - char path[1024]; - compute_cache_path(path, sizeof(path)); - if (path[0] == '\0') return; - - char tmp_path[1100]; - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.%d", path, (int)getpid()); - - int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd < 0) return; - - uint64_t header[2] = { SANCOV_FILTER_CACHE_MAGIC, (uint64_t)g_guard_count }; - if (write(fd, header, sizeof(header)) != (ssize_t)sizeof(header)) { - close(fd); unlink(tmp_path); return; - } - if (write(fd, g_edge_state, g_guard_count) != (ssize_t)g_guard_count) { - close(fd); unlink(tmp_path); return; - } - close(fd); - rename(tmp_path, path); // atomic on POSIX -} - -static void filter_init_impl(void) { - if (g_guard_count == 0) return; - g_edge_state = (uint8_t*)calloc(g_guard_count, 1); - if (!g_edge_state) return; - load_filter_cache(); - atexit(save_filter_cache); -} - -static inline void ensure_filter_init(void) { - pthread_once(&g_filter_init_once, filter_init_impl); -} - -// Slow path: classify a single edge on its first fire and update state. -// Called rarely (once per edge, ever). Sets either: -// - state[g] = SKIP (compiler-generated noise; never stamps *guard) -// - state[g] = ALLOWED (real instrumented code) -// Forward-declared up near the hot path. -static void check_and_cache_edge_lazy_impl(uint32_t* guard, uint32_t g); -static void check_and_cache_edge_lazy(uint32_t* guard, uint32_t g) { - check_and_cache_edge_lazy_impl(guard, g); -} -static void check_and_cache_edge_lazy_impl(uint32_t* guard, uint32_t g) { - if (!g_edge_state) return; - - bool is_noise = false; - // Need PCs to dladdr. If pcs aren't available (e.g., multi-module without - // the pcs_init fix), default to ALLOWED — graceful degradation. - if (g_pcs_start && g < g_pcs_count) { - uintptr_t pc = g_pcs_start[(size_t)g * 2]; - if (pc != 0) { - Dl_info info; - if (dladdr((void*)pc, &info) && info.dli_sname) { - is_noise = sancov_is_compiler_generated(info.dli_sname); - } - } - } - - if (is_noise) { - // Record the verdict in g_edge_state only. Do NOT stamp `*guard` — that - // shared global is read lock-free on the hot path by every concurrent - // engine, so writing it here is a data race (TSan-confirmed). The atomic - // g_edge_state verdict already suppresses future fires. - __atomic_store_n(&g_edge_state[g], (uint8_t)EDGE_STATE_SKIP, __ATOMIC_RELEASE); - __atomic_fetch_add(&g_lazy_filtered_count, 1, __ATOMIC_RELAXED); - } else { - __atomic_store_n(&g_edge_state[g], (uint8_t)EDGE_STATE_ALLOWED, __ATOMIC_RELEASE); - __atomic_fetch_add(&g_lazy_allowed_count, 1, __ATOMIC_RELAXED); - } - __atomic_store_n(&g_edge_state_dirty, 1, __ATOMIC_RELEASE); -} - -/// Check if a mangled symbol name matches a compiler-generated pattern. -/// Returns true if the symbol should be filtered out. -bool sancov_is_compiler_generated(const char* sname) { - if (!sname) return false; - - // Prefix checks: runtime internals - if (strncmp(sname, "__swift_", 8) == 0) return true; - if (strncmp(sname, "_swift_", 7) == 0) return true; - - size_t len = strlen(sname); - if (len < 3) return false; - - // Suffix checks on mangled Swift names. - // Two-character suffixes: - const char* last2 = sname + len - 2; - if (strcmp(last2, "Wl") == 0) return true; // lazy protocol witness table accessor - if (strcmp(last2, "WL") == 0) return true; // lazy metadata accessor - if (strcmp(last2, "Ma") == 0) return true; // type metadata accessor (generic) - - // Three-character suffixes (WO + specifier): - if (len >= 3) { - const char* last3 = sname + len - 3; - if (strncmp(last3, "WO", 2) == 0) return true; // all outlined operations (WOh/c/d/r/b/e/...) - } - - // Two-character suffixes for other compiler-generated patterns: - if (strcmp(last2, "TA") == 0) return true; // partial apply forwarder - if (strcmp(last2, "TR") == 0) return true; // reabstraction thunk - if (strcmp(last2, "TK") == 0) return true; // key path getter - if (strcmp(last2, "Mr") == 0) return true; // type metadata completion - - // Async resume/suspend of compiler-generated thunks: - // e.g. ...TRTATQ0_ (resume of partial apply of reabstraction thunk) - if (strstr(sname, "TATQ") != NULL) return true; - if (strstr(sname, "TATY") != NULL) return true; - if (strstr(sname, "TRTQ") != NULL) return true; - if (strstr(sname, "TRTY") != NULL) return true; - - // Global/static variable addressors: ends with "vau" (unsigned addressor) - // These have init-once semantics with different branches for first vs cached access. - if (len >= 3) { - const char* last3 = sname + len - 3; - if (last3[0] == 'v' && last3[1] == 'a' && last3[2] == 'u') return true; - } - - // Bare async resume/yield points: ends with TQ_ or TY_ - // e.g. ...FTQ3_, ...FTY4_, ...cfU_TQ0_, ...cfU_TY1_ - // These continuation edges are scheduling-dependent — even under - // ScheduleController.run (deterministic task ordering), the "which resume - // point fires first" order can vary because two continuations may be - // enqueued in whichever order the dependency-resolution happened to pick. - // Filtering them is required for pathTrie-based determinism. - if (len >= 4) { - const char* p = sname + len - 1; - if (*p == '_') { - p--; - // Skip digits - while (p > sname && *p >= '0' && *p <= '9') p--; - // Check for TQ or TY - if (p >= sname + 1 && *p == 'Q' && *(p-1) == 'T') return true; - if (p >= sname + 1 && *p == 'Y' && *(p-1) == 'T') return true; - } - } - - // Default argument: ends with fA_ (e.g. fA_, fA0_, fA1_) - if (len >= 3) { - // Check fA_ (no digit) - const char* last3 = sname + len - 3; - if (last3[0] == 'f' && last3[1] == 'A' && last3[2] == '_') return true; - // Check fA_ (4-char pattern) - if (len >= 4) { - const char* last4 = sname + len - 4; - if (last4[0] == 'f' && last4[1] == 'A' && last4[3] == '_') return true; - } - } - - return false; -} - -void sancov_apply_edge_filter(void) { - // Filtering is now lazy + cached. Allocate the state array, load the - // on-disk cache (if present), and apply any cached SKIP markers eagerly. - // After this, individual edges are classified at their first fire. - ensure_filter_init(); - g_filter_applied = true; -} - -size_t sancov_get_filtered_count(void) { - // Backwards-compatible: report the running tally from the lazy filter, - // plus any leftover from old upfront passes (now zero in practice). - size_t lazy = __atomic_load_n(&g_lazy_filtered_count, __ATOMIC_RELAXED); - return lazy + g_filtered_count; -} diff --git a/Sources/SanCovHooks/include/SanCovHooks.h b/Sources/SanCovHooks/include/SanCovHooks.h index 5b2d7934..c5fb5145 100644 --- a/Sources/SanCovHooks/include/SanCovHooks.h +++ b/Sources/SanCovHooks/include/SanCovHooks.h @@ -115,6 +115,18 @@ typedef struct { /// with recorder_data when the context is finally freed, or immediately /// when the recorder is replaced/cleared via sancov_context_set_recorder. uintptr_t recorder_release_bits; + /// Optional per-context COMPARISON recorder (the trace-cmp half), with its + /// own data + reset/release hooks. Fully independent of the edge recorder + /// above: the comparisonCoverage strategy attaches BOTH (edge union + value + /// profile). Stored as pointer bits like edge_recorder_bits; 0 → none + /// attached (sancov_dispatch_cmp is then a no-op). Set via + /// sancov_context_set_cmp_recorder; read per comparison by + /// sancov_dispatch_cmp after routing resolves this context. Same + /// release/acquire ordering and co-ownership contract as the edge slot. + uintptr_t cmp_recorder_bits; + void* cmp_recorder_data; + uintptr_t cmp_recorder_reset_bits; + uintptr_t cmp_recorder_release_bits; } SanCovMeasurementContext; /// Begin a measurement context for coverage isolation. @@ -174,6 +186,15 @@ static inline void* sancov_context_get_recorder_data(SanCovMeasurementContext* c return __atomic_load_n(&context->recorder_data, __ATOMIC_ACQUIRE); } +/// Read the context's opaque CMP recorder data (acquire). Used by Swift +/// comparison-observer recorders once per comparison to reach their box; NULL +/// when nothing is attached. static inline for the hot path (single acquire +/// load), mirroring sancov_context_get_recorder_data. +static inline void* sancov_context_get_cmp_recorder_data(SanCovMeasurementContext* context) { + if (context == NULL) return NULL; + return __atomic_load_n(&context->cmp_recorder_data, __ATOMIC_ACQUIRE); +} + /// The coverage-inheritance handle for a measurement context: a 64-bit value /// that packs the context's generation tag (high 16 bits) with its pointer /// (low 48 bits). Store THIS in the `CoverageInheritance.context` task-local @@ -278,12 +299,75 @@ void sancov_observer_exit(void); /// tests can drive the real dispatch path with synthetic guards. void sancov_dispatch_edge(uint32_t* guard); +// MARK: - Comparison Recorders (trace-cmp / value profile) +// +// The trace-cmp half of the substrate. SanitizerCoverage's +// __sanitizer_cov_trace_cmp{1,2,4,8} / const_cmp / switch hooks deliver the +// OPERANDS of each instrumented comparison (plus the comparison's PC). That +// gives a gradient — e.g. popcount(arg1 ^ arg2) shrinking as an input nears a +// boundary `i < c` — that pure edge coverage is blind to (every near-miss +// traces the same edge). A comparison recorder is the cmp analog of an edge +// recorder: it lives on the measurement context, and sancov_dispatch_cmp +// routes each comparison to it. Independent of the edge recorder slot. + +/// A comparison recorder. Receives the comparison site's PC, both operands +/// (zero-extended to 64 bits), the operand width in bytes (1/2/4/8), and the +/// already-resolved measurement context (so recorders never re-run routing). +typedef void (*SanCovCmpRecorder)(uintptr_t pc, uint64_t arg1, uint64_t arg2, + uint32_t size_bytes, SanCovMeasurementContext* context); + +/// Set (or with NULL clear) the context's COMPARISON recorder, its opaque +/// state, and the state's lifecycle hooks. Same ownership/ordering contract as +/// sancov_context_set_recorder (the edge slot), applied to the independent cmp +/// slot: data/hooks stored before the fn (release ordering); `release` (when +/// non-NULL) transfers ownership of `data` to the context and is called exactly +/// once — at the context's last reference drop, on replacement, or immediately +/// on a clear-with-payload; `reset` (when non-NULL) is called by +/// sancov_reset_coverage with `data`. +void sancov_context_set_cmp_recorder( + SanCovMeasurementContext* context, + SanCovCmpRecorder recorder, + void* data, + SanCovRecorderDataFn reset, + SanCovRecorderDataFn release); + +/// TESTING ONLY: read the context's cmp recorder as raw pointer bits (NULL when +/// none attached). +void* sancov_context_get_cmp_recorder_for_testing(SanCovMeasurementContext* context); + +/// TESTING ONLY: the process-global count of measurement contexts that currently +/// have a comparison recorder attached. sancov_dispatch_cmp early-returns (before +/// the per-thread TLS fetch) when this is 0 — so edge-only strategies don't pay +/// the cmp-routing cost for comparisons no one consumes. +int sancov_cmp_recorder_count_for_testing(void); + /// TESTING ONLY: drive the manual task-local inheritance chain walk directly. /// Lets a test feed a fake task whose chain head is an unmapped (freed/poisoned) /// pointer — the task #49 teardown shape — and assert it returns 0 rather than /// dereferencing the bad pointer and crashing. uint64_t sancov_manual_walk_for_inherited_context_for_testing(const void* task); +/// Generation guard: when set true on a thread, sancov_dispatch_edge and +/// sancov_dispatch_cmp early-return on that thread (after the drop filter / TLS +/// fetch). The fuzz loop sets it around input generation/mutation — which runs +/// instrumented SUT code (e.g. a type-directed generator calling getTyp) whose +/// coverage is NOT the property under test and is reset away before the test — +/// so dispatching+recording it is pure waste. Per-thread, so concurrent engines +/// (one mutating, one testing) never suppress each other. Cheap: the flag lives +/// in the TLS block the dispatch already fetches. Must be cleared before the +/// property runs or its coverage is lost. +void sancov_set_dispatch_suppressed(bool suppressed); + +/// Read the current thread's generation-guard flag (testing/diagnostic). +bool sancov_dispatch_is_suppressed(void); + +/// Resolve routing for the current task/thread and run the context's cmp +/// recorder with the given comparison operands. No-op when no cmp recorder is +/// attached or no measurement is active. Called by the __sanitizer_cov_trace_cmp* +/// hooks for every instrumented comparison; public so tests can drive the real +/// dispatch path with synthetic operands. +void sancov_dispatch_cmp(uintptr_t pc, uint64_t arg1, uint64_t arg2, uint32_t size_bytes); + // MARK: - Schedule-Aware Coverage // // When schedule fuzzing is active, test code runs in a different Swift task @@ -312,40 +396,6 @@ const void* sancov_capture_key_by_value(const void* task, uintptr_t expected_val /// strategies using covered_indices see the correct data. void sancov_rebuild_covered_indices_from_map(SanCovMeasurementContext* context); -// MARK: - Edge Filter -// -// Filters compiler-generated edges (outlined destroyers, lazy witness table -// accessors, lazy metadata accessors) by setting their guard value to -// SANCOV_GUARD_SKIP. Because the hot-path check is `*guard < g_guard_count`, -// guards set to UINT32_MAX will always fail that check — zero overhead. - -/// Sentinel value that disables a guard. Any guard set to this value will be -/// skipped by the edge recording hooks (since UINT32_MAX >= g_guard_count). -#define SANCOV_GUARD_SKIP UINT32_MAX - -/// Scan all guard PCs and disable compiler-generated edges. -/// Call once before fuzzing begins — both __sanitizer_cov_trace_pc_guard_init -/// and __sanitizer_cov_pcs_init will have completed by then. -/// -/// Filtered symbol patterns (matched on raw mangled dli_sname): -/// - WOh suffix — outlined destroy -/// - WOc suffix — outlined copy -/// - WOd suffix — outlined consume -/// - WOr suffix — outlined release -/// - Wl suffix — lazy protocol witness table accessor -/// - WL suffix — lazy metadata accessor -/// - Ma suffix — type metadata accessor (generic) -/// - __swift_ prefix — runtime internals -/// - _swift_ prefix — runtime internals -void sancov_apply_edge_filter(void); - -/// Return the number of edges disabled by sancov_apply_edge_filter(). -size_t sancov_get_filtered_count(void); - -/// Check if a symbol name matches compiler-generated patterns. -/// Exposed for testing the filter logic. -bool sancov_is_compiler_generated(const char* sname); - /// Diagnostic: per-routing-path counters maintained inside get_current_coverage_map. /// Pure atomic loads — safe to call from anywhere; concurrent reads are consistent /// even if increments are interleaved. @@ -373,6 +423,29 @@ typedef struct { /// Read the current routing-path counters into `out`. Safe to call concurrently. void sancov_read_route_counters(SanCovRouteCounters* out); +// MARK: - Process-global "ever-covered" edge bitmap (diagnostic) +// +// An accumulator that records EVERY allowed edge fire, independent of the +// per-task measurement context (which the fuzz loop resets each iteration) and +// the corpus (which only banks admitted inputs). It answers "what is the true +// union of edges executed across an entire run?". Disabled by default (zero +// hot-path cost beyond one predicted-not-taken load); enable once for a +// diagnostic run, reset between runs, then read the count/indices. + +/// Allocate the bitmap and start recording. Idempotent; safe under races. +void sancov_enable_global_ever_covered(void); + +/// Clear all recorded bits (keeps recording enabled). For use between runs in a +/// single-threaded diagnostic harness. +void sancov_reset_global_ever_covered(void); + +/// Number of distinct edges ever fired since the last reset (0 if disabled). +size_t sancov_global_ever_covered_count(void); + +/// Allocate and return the sorted indices of every edge ever fired since the +/// last reset; sets `*out_count`. Caller must free() the result. NULL if none. +uint32_t* sancov_snapshot_global_ever_covered(size_t* out_count); + #ifdef __cplusplus } #endif diff --git a/Tests/GenericTimerPollerTests/FlattenedScheduleTests.swift b/Tests/GenericTimerPollerTests/FlattenedScheduleTests.swift index d5c80004..1e63de82 100644 --- a/Tests/GenericTimerPollerTests/FlattenedScheduleTests.swift +++ b/Tests/GenericTimerPollerTests/FlattenedScheduleTests.swift @@ -38,10 +38,7 @@ struct FlattenedScheduleTests { func peelMovesElementZeroToScheduleBytes() throws { let entry = CorpusEntry<[UInt8], Int, String>( input: [9, 8, 7], 42, "hi", - scheduleBytes: nil, - sparseCoverage: SparseCoverage(indices: [1, 2]), - entryType: .coverage, - failure: nil + scheduleBytes: nil ) let extended = FuzzResult<[UInt8], Int, String>( corpus: CorpusSnapshot(entries: [entry]), @@ -59,7 +56,6 @@ struct FlattenedScheduleTests { #expect(ei == 42) #expect(es == "hi") #expect(e.scheduleBytes == [9, 8, 7]) - #expect(e.sparseCoverage.indices == [1, 2]) // Failure: input peeled to (Int, String); the schedule that triggered it is // lifted from element 0 onto the `scheduleBytes` slot so it can be reproduced. @@ -87,10 +83,7 @@ struct FlattenedScheduleTests { let entry = CorpusEntry<[UInt8], Int>( input: schedule, 42, // The engine sets this from the element-0 extractor during a scheduled run. - scheduleBytes: schedule, - sparseCoverage: SparseCoverage(indices: [1, 2]), - entryType: .coverage, - failure: nil + scheduleBytes: schedule ) let snapshot = CorpusSnapshot<[UInt8], Int>(entries: [entry]) @@ -179,8 +172,7 @@ struct FlattenedScheduleTests { using: Mutator(seeds: [1, 2, 3], mutate: { v, _ in v &+ 1 }), duration: .milliseconds(200), persistence: .ephemeral, - coverageStrategy: custom, - scheduler: MutationScheduler.weightedPool(admission: .everyDiscovery), + scheduler: MutationScheduler.weightedPool(admission: .everyDiscovery, coverageStrategy: custom), scheduleFuzzing: true ) { (_: Int) in } } diff --git a/Tests/GenericTimerPollerTests/GenericTimerPollerFuzzTests.swift b/Tests/GenericTimerPollerTests/GenericTimerPollerFuzzTests.swift index 41dcab59..1335387d 100644 --- a/Tests/GenericTimerPollerTests/GenericTimerPollerFuzzTests.swift +++ b/Tests/GenericTimerPollerTests/GenericTimerPollerFuzzTests.swift @@ -252,8 +252,7 @@ struct GenericTimerPollerFuzzTests { // Poller deinits here — deinit cancels task and finishes continuation } for (i, entry) in result.corpus.entries.enumerated() { - let edges = entry.sparseCoverage.indices.sorted() - print("Entry \(i): \(edges.count) edges, input=\(entry.input)") + print("Entry \(i): input=\(entry.input)") } } } @@ -287,8 +286,7 @@ struct GenericTimerPollerFuzzTests { } } } - let allEdges = result.corpus.entries.reduce(into: Set()) { $0.formUnion($1.sparseCoverage.indices) } - print("Schedule fuzz: \(result.stats.totalInputs) iterations, \(result.corpus.entries.count) corpus entries, \(String(format: "%.1f", result.stats.inputsPerSecond)) iter/s, \(allEdges.count) unique edges total") + print("Schedule fuzz: \(result.stats.totalInputs) iterations, \(result.corpus.entries.count) corpus entries, \(String(format: "%.1f", result.stats.inputsPerSecond)) iter/s") } } @@ -317,13 +315,6 @@ struct GenericTimerPollerFuzzTests { let corpusCount = result.corpus.entries.count print("Fixed input: \(result.stats.totalInputs) iterations, \(corpusCount) corpus entries") - // Dump edge -> PC mapping for all edges seen - let allEdges = result.corpus.entries.reduce(into: Set()) { $0.formUnion($1.sparseCoverage.indices) } - for edge in allEdges.sorted() { - let pc = SanCovCounters.getPC(for: Int(edge)) - print("[EDGE_PC] \(edge)|\(pc)") - } - #expect( corpusCount <= 10, "Expected at most ~5-10 unique paths for a fixed input, got \(corpusCount)" @@ -346,7 +337,7 @@ struct GenericTimerPollerFuzzTests { let result = try await fuzz( duration: .seconds(3), persistence: .ephemeral, - coverageStrategy: .pathTrie + scheduler: MutationScheduler.weightedPool(coverageStrategy: .pathTrie) ) { (input: ConstantPollerInput) in let poller = GenericTimerPoller(defaultInterval: .microseconds(100)) await withTaskGroup(of: Void.self) { group in @@ -383,7 +374,7 @@ struct GenericTimerPollerFuzzTests { let result = try await fuzz( duration: .seconds(2), persistence: .ephemeral, - coverageStrategy: .pathTrie, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .pathTrie), scheduleFuzzing: true ) { (input: ConstantPollerInput) in let poller = GenericTimerPoller(defaultInterval: .microseconds(100)) diff --git a/Tests/PropertyTestingKitTests/Coverage/CmpRecorderTests.swift b/Tests/PropertyTestingKitTests/Coverage/CmpRecorderTests.swift new file mode 100644 index 00000000..c010afa0 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Coverage/CmpRecorderTests.swift @@ -0,0 +1,200 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for per-context COMPARISON recorders: the trace-cmp half of the +// coverage substrate. A comparison recorder lives on the measurement context +// (cmp_recorder_bits), `sancov_dispatch_cmp` routes each instrumented +// comparison's operands (pc, arg1, arg2, size) to it, and the slot has the +// same attach/release/reset lifecycle as the edge recorder. Mirrors +// ContextRecorderTests for the cmp path. +// + +import Testing +import Foundation +import SanCovHooks +@testable import PropertyTestingKit + +/// C-ABI capture struct: a `@convention(c)` recorder can't capture, so it +/// reaches its state through the context's cmp recorder data. +private struct CmpCapture { + var count: Int = 0 + var lastPC: UInt = 0 + var lastArg1: UInt64 = 0 + var lastArg2: UInt64 = 0 + var lastSize: UInt32 = 0 +} + +/// A recorder that records the last operands it saw into its `CmpCapture` data. +private let captureRecorder: SanCovCmpRecorder = { pc, arg1, arg2, size, ctx in + guard let ctx, let data = sancov_context_get_cmp_recorder_data(ctx) else { return } + let p = data.assumingMemoryBound(to: CmpCapture.self) + p.pointee.count += 1 + p.pointee.lastPC = pc + p.pointee.lastArg1 = arg1 + p.pointee.lastArg2 = arg2 + p.pointee.lastSize = size +} + +/// Raw pointer bits of a cmp recorder, for comparing against the getter seam. +private func cmpRecorderBits(_ hook: SanCovCmpRecorder) -> UnsafeMutableRawPointer { + unsafeBitCast(hook, to: UnsafeMutableRawPointer.self) +} + +@Suite("Per-context comparison recorders") +struct CmpRecorderTests { + + // MARK: - Attach / getter round-trip (no routing involved) + + @Test("Attaching a cmp recorder stores it and its data on the context") + func attachRoundTrip() { + let ctx = sancov_create_dummy_context() + defer { sancov_release_for_testing(ctx) } + + #expect(sancov_context_get_cmp_recorder_for_testing(ctx) == nil, + "A fresh context has no cmp recorder") + + var capture = CmpCapture() + withUnsafeMutablePointer(to: &capture) { data in + sancov_context_set_cmp_recorder(ctx, captureRecorder, UnsafeMutableRawPointer(data), nil, nil) + #expect(sancov_context_get_cmp_recorder_for_testing(ctx) == cmpRecorderBits(captureRecorder)) + #expect(sancov_context_get_cmp_recorder_data(ctx) == UnsafeMutableRawPointer(data)) + + sancov_context_set_cmp_recorder(ctx, nil, nil, nil, nil) + #expect(sancov_context_get_cmp_recorder_for_testing(ctx) == nil) + #expect(sancov_context_get_cmp_recorder_data(ctx) == nil) + } + } + + /// The cmp slot is independent of the edge slot: attaching a cmp recorder + /// must not touch the edge recorder, and vice versa. The comparisonCoverage + /// strategy attaches BOTH (edge union + value profile). + @Test("The cmp recorder slot is independent of the edge recorder slot") + func cmpAndEdgeSlotsAreIndependent() { + let ctx = sancov_create_dummy_context() + defer { sancov_release_for_testing(ctx) } + + var capture = CmpCapture() + withUnsafeMutablePointer(to: &capture) { data in + sancov_context_set_recorder(ctx, sancov_recorder_default, nil, nil, nil) + sancov_context_set_cmp_recorder(ctx, captureRecorder, UnsafeMutableRawPointer(data), nil, nil) + + #expect(sancov_context_get_recorder_for_testing(ctx) != nil, + "Attaching a cmp recorder must not clear the edge recorder") + #expect(sancov_context_get_cmp_recorder_for_testing(ctx) == cmpRecorderBits(captureRecorder)) + + sancov_context_set_cmp_recorder(ctx, nil, nil, nil, nil) + #expect(sancov_context_get_recorder_for_testing(ctx) != nil, + "Clearing the cmp recorder must not clear the edge recorder") + } + } + + /// The header promises "release is called exactly once" for any + /// ownership-transferring set call — including the clear-with-payload shape. + @Test("Clearing the cmp recorder with a payload still releases it exactly once") + func clearingReleasesPassedPayload() { + let ctx = sancov_create_dummy_context() + defer { sancov_release_for_testing(ctx) } + + var releaseCount = 0 + withUnsafeMutablePointer(to: &releaseCount) { counter in + sancov_context_set_cmp_recorder( + ctx, nil, UnsafeMutableRawPointer(counter), nil, + { data in data?.assumingMemoryBound(to: Int.self).pointee += 1 } + ) + } + + #expect(releaseCount == 1, + "ownership transferred to a cleared slot is released, not dropped") + #expect(sancov_context_get_cmp_recorder_data(ctx) == nil) + } + + // MARK: - Dispatch routes operands to the attached cmp recorder + + @Test("Dispatch routes the comparison operands to the attached recorder") + func dispatchRoutesOperands() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + var capture = CmpCapture() + withUnsafeMutablePointer(to: &capture) { data in + sancov_context_set_cmp_recorder( + context.rawContext, captureRecorder, UnsafeMutableRawPointer(data), nil, nil) + + sancov_dispatch_cmp(0xBEEF, 4, 5, 8) + + // Snapshot then DETACH before asserting. The test target is built + // with `trace-cmp`, so the `#expect` integer comparisons below are + // themselves instrumented: while the recorder is attached each one + // dispatches into it and overwrites the captured operands. Capturing + // the single dispatch's result and detaching first keeps the + // assertions measuring exactly that one dispatch. + let captured = data.pointee + sancov_context_set_cmp_recorder(context.rawContext, nil, nil, nil, nil) + + #expect(captured.count == 1, "the recorder fires once per dispatched comparison") + #expect(captured.lastPC == 0xBEEF) + #expect(captured.lastArg1 == 4) + #expect(captured.lastArg2 == 5) + #expect(captured.lastSize == 8) + } + } + + @Test("Dispatch with no cmp recorder attached is a harmless no-op") + func dispatchWithoutRecorderIsNoOp() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + // No recorder attached: must not crash, must record nothing observable. + sancov_dispatch_cmp(0x1234, 1, 2, 4) + #expect(sancov_context_get_cmp_recorder_for_testing(context.rawContext) == nil) + } + + // MARK: - Lifecycle + + @Test("Reset invokes the cmp recorder's reset hook with its data") + func resetInvokesResetHook() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + var resetCount = 0 + withUnsafeMutablePointer(to: &resetCount) { counter in + sancov_context_set_cmp_recorder( + context.rawContext, captureRecorder, UnsafeMutableRawPointer(counter), + { data in data?.assumingMemoryBound(to: Int.self).pointee += 1 }, + nil) + + SanCovCounters.resetCoverage(context) + #expect(counter.pointee == 1, "resetCoverage must invoke the cmp recorder's reset hook") + + sancov_context_set_cmp_recorder(context.rawContext, nil, nil, nil, nil) + } + } + + @Test("Freeing the context releases the cmp recorder data exactly once") + func freeReleasesCmpData() { + let context = SanCovCounters.beginMeasurement() + + let releaseCount = UnsafeMutablePointer.allocate(capacity: 1) + releaseCount.initialize(to: 0) + defer { releaseCount.deallocate() } + + sancov_context_set_cmp_recorder( + context.rawContext, captureRecorder, UnsafeMutableRawPointer(releaseCount), nil, + { data in data?.assumingMemoryBound(to: Int.self).pointee += 1 }) + + SanCovCounters.endMeasurement(context) + #expect(releaseCount.pointee == 1, + "the context's final release must release the cmp recorder data once") + } +} diff --git a/Tests/PropertyTestingKitTests/Coverage/ComparisonObserverTests.swift b/Tests/PropertyTestingKitTests/Coverage/ComparisonObserverTests.swift new file mode 100644 index 00000000..6935d96e --- /dev/null +++ b/Tests/PropertyTestingKitTests/Coverage/ComparisonObserverTests.swift @@ -0,0 +1,145 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the Swift comparison-observer bridge: a strategy expresses +// per-comparison work as an `onCompare` closure, the measurement context +// co-owns the observer, and `sancov_dispatch_cmp` routes each comparison's +// operands to it. Mirrors ContextRecorderTests for the cmp channel. +// + +import Testing +import Foundation +import SanCovHooks +@testable import PropertyTestingKit + +/// Raw pointer bits of a cmp recorder, for comparing against the getter seam. +private func cmpRecorderBits(_ hook: SanCovCmpRecorder) -> UnsafeMutableRawPointer { + unsafeBitCast(hook, to: UnsafeMutableRawPointer.self) +} + +/// Deinit canary: captured strongly by an observer, held weakly by the test. +private final class Canary: Sendable {} + +@Suite("Comparison observers") +struct ComparisonObserverTests { + + @Test("A strategy's onCompare closure receives dispatched comparisons") + func onCompareReceivesComparisons() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let seen = PropertyTestingKit.SyncBox<[(UInt, UInt64, UInt64, UInt32)]>([]) + let strategy = CoverageStrategy(makeEngine: { + CoverageEngine(onCompare: { pc, a, b, size in + seen.update { $0.append((pc, a, b, size)) } + }) { _ in false } + }) + + let evaluator = strategy.makeEvaluator() + evaluator.setup?(context) + + sancov_dispatch_cmp(0xABC, 3, 7, 4) + + #expect(seen.value.contains { $0.0 == 0xABC && $0.1 == 3 && $0.2 == 7 && $0.3 == 4 }, + "onCompare must observe the dispatched comparison's operands") + } + + @Test("onCompare's setup attaches a comparison observer") + func setupAttachesComparisonObserver() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let strategy = CoverageStrategy(makeEngine: { + CoverageEngine(onCompare: { _, _, _, _ in }) { _ in false } + }) + let evaluator = strategy.makeEvaluator() + evaluator.setup?(context) + + #expect(sancov_context_get_cmp_recorder_for_testing(context.rawContext) == cmpRecorderBits(comparisonObserverRecorder)) + #expect(sancov_context_get_cmp_recorder_data(context.rawContext) != nil, + "The strategy's comparison observer rides along as cmp recorder data") + } + + /// A strategy with no onCompare attaches no comparison observer — the cmp + /// channel stays dormant (no per-comparison overhead). + @Test("A strategy without onCompare attaches no comparison observer") + func noOnCompareAttachesNothing() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let evaluator = CoverageStrategy.pathTrie.makeEvaluator() + evaluator.setup?(context) + + #expect(sancov_context_get_cmp_recorder_for_testing(context.rawContext) == nil, + "pathTrie attaches an edge observer but no comparison observer") + } + + @Test("onCompare and onEdge can coexist on one engine") + func onCompareAndOnEdgeCoexist() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let edges = PropertyTestingKit.SyncBox<[UInt32]>([]) + let cmps = PropertyTestingKit.SyncBox(0) + let strategy = CoverageStrategy(makeEngine: { + CoverageEngine( + onEdge: { e, _ in edges.update { $0.append(e) } }, + onCompare: { _, _, _, _ in cmps.update { $0 += 1 } } + ) { _ in false } + }) + let evaluator = strategy.makeEvaluator() + evaluator.setup?(context) + + var g7: UInt32 = 7 + sancov_dispatch_edge(&g7) + sancov_dispatch_cmp(0x1, 1, 2, 8) + + #expect(edges.value.filter { $0 == 7 }.count >= 1, "the edge observer still fires") + #expect(cmps.value >= 1, "the comparison observer fires alongside it") + } + + @Test("The context co-owns the comparison observer after the test drops it") + func contextSharesOwnership() { + weak var weakCanary: Canary? + let context = SanCovCounters.beginMeasurement() + + do { + let canary = Canary() + weakCanary = canary + SanCovCounters.attachComparisonObserver( + ComparisonObserver(onCompare: { _, _, _, _ in withExtendedLifetime(canary) {} }), + to: context + ) + } + #expect(weakCanary != nil, "The context must retain the observer after attach") + + SanCovCounters.endMeasurement(context) + #expect(weakCanary == nil, "Freeing the context must release the observer") + } + + @Test("A comparison observer's onReset fires when coverage is reset") + func onResetFiresOnResetCoverage() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let resets = PropertyTestingKit.SyncBox(0) + SanCovCounters.attachComparisonObserver( + ComparisonObserver(onCompare: { _, _, _, _ in }, onReset: { resets.update { $0 += 1 } }), + to: context + ) + + SanCovCounters.resetCoverage(context) + #expect(resets.value == 1, "resetCoverage must invoke the comparison observer's onReset") + } +} diff --git a/Tests/PropertyTestingKitTests/Coverage/ContextRecorderTests.swift b/Tests/PropertyTestingKitTests/Coverage/ContextRecorderTests.swift index 79c7eaf7..d76914dd 100644 --- a/Tests/PropertyTestingKitTests/Coverage/ContextRecorderTests.swift +++ b/Tests/PropertyTestingKitTests/Coverage/ContextRecorderTests.swift @@ -342,7 +342,6 @@ struct ContextRecorderTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() // The PRODUCTION .pathTrie engine: setup attaches its trie observer, // evaluate judges (and marks) the run's path in one critical section. @@ -388,7 +387,6 @@ struct ContextRecorderTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(context) diff --git a/Tests/PropertyTestingKitTests/Coverage/GlobalEverCoveredTests.swift b/Tests/PropertyTestingKitTests/Coverage/GlobalEverCoveredTests.swift new file mode 100644 index 00000000..0dc694c0 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Coverage/GlobalEverCoveredTests.swift @@ -0,0 +1,90 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the process-global "ever-covered" edge bitmap: a diagnostic +// accumulator that sets a bit on EVERY allowed edge fire, independent of the +// per-task measurement context, the engine's per-iteration reset, and corpus +// banking. It answers "what is the TRUE union of edges executed across a whole +// run?" — the question that ctx.coveredIndices (cleared each iteration) and +// corpus.coveredIndices (only admitted inputs) cannot. +// +// The global bitmap is process-wide and shared across the parallel test suite, +// so these assertions use only concurrency-safe invariants (superset and +// monotonicity); they never pin an exact count, which other concurrent tests +// would pollute. + +import Testing +import Foundation +import SanCovHooks +@testable import PropertyTestingKit + +@Suite("Global ever-covered bitmap") +struct GlobalEverCoveredTests { + /// Some instrumented branching work so real edges fire. `@inline(never)` so + /// the edges live in a stable, attributable function. + @inline(never) + static func work(_ n: Int) -> Int { + var acc = 0 + for i in 0..= globalAfterFirst.count, + "global accumulator is monotonic across per-iteration resets") + + // A second, longer path only grows the union. + _ = Self.work(11) + #expect(SanCovCounters.globalEverCoveredCount >= globalAfterFirst.count) + } + + @Test("explicit reset clears the global accumulator") + func explicitResetClears() throws { + try SanCovCounters.checkAvailabilty() + SanCovCounters.enableGlobalEverCovered() + // NOTE: cannot assert == 0 here — the parallel suite fires edges + // concurrently. We assert the reset is observable: immediately after a + // reset the count is no larger than after we then run more work. + SanCovCounters.resetGlobalEverCovered() + let afterReset = SanCovCounters.globalEverCoveredCount + _ = Self.work(20) + #expect(SanCovCounters.globalEverCoveredCount >= afterReset) + } +} diff --git a/Tests/PropertyTestingKitTests/Coverage/InheritanceTest.swift b/Tests/PropertyTestingKitTests/Coverage/InheritanceTest.swift index ede0388b..8cfe4086 100644 --- a/Tests/PropertyTestingKitTests/Coverage/InheritanceTest.swift +++ b/Tests/PropertyTestingKitTests/Coverage/InheritanceTest.swift @@ -351,7 +351,7 @@ struct InheritanceTest { maxIterations: 100, seeds: [(1,), (2,), (3,)], persistence: .ephemeral, - coverageStrategy: .newEdge + scheduler: MutationScheduler.weightedPool(coverageStrategy: .newEdge) ) { (input: Int) in await withTaskGroup(of: Void.self) { group in group.addTask { diff --git a/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthChainTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthChainTests.swift new file mode 100644 index 00000000..cc13695e --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthChainTests.swift @@ -0,0 +1,54 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Multi-generation mutation: a depth-d mutant chains the mutator d times +// (mutate∘mutate∘…), and the per-seed depth is carried by the pool core via +// the `.setMutationDepth` action so a policy can escalate depth on stale seeds. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("Adaptive mutation depth (chaining + core plumbing)") +struct AdaptiveDepthChainTests { + + private final class Setter: PoolPlugin { + let onInsert: (Int) -> [PoolAction] + init(onInsert: @escaping (Int) -> [PoolAction]) { self.onInsert = onInsert } + func handle(event: PoolEvent) -> [PoolAction] { + if case let .inserted(id, _, _, _, _) = event { return onInsert(id) } + return [] + } + } + + @Test("chainMutate applies the mutator exactly depth times (min 1)") + func chainAppliesDepthTimes() { + let m = Mutator(seeds: [""], mutate: { s, _ in s + "*" }) + var rng = FastRNG() + #expect(chainMutate("", depth: 1, inputSize: 1, rng: &rng, mutators: m) == "*") + #expect(chainMutate("", depth: 3, inputSize: 1, rng: &rng, mutators: m) == "***") + // depth below 1 clamps to a single application (never a no-op pass-through). + #expect(chainMutate("", depth: 0, inputSize: 1, rng: &rng, mutators: m) == "*") + } + + @Test("core stores per-entry mutation depth; defaults to 1") + func coreStoresDepth() { + let setter = Setter { id in [.setMutationDepth(id: id, depth: 3)] } + let core = WeightedPoolHarness.core(admission: .everyDiscovery, policies: [setter]) + + #expect(core.mutationDepth(for: 0) == 1) // default before any entry exists + _ = WeightedPoolHarness.accept(core, edges: [1]) + #expect(core.mutationDepth(for: 0) == 3) // policy escalated it + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthInsertedTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthInsertedTests.swift new file mode 100644 index 00000000..415c7c05 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthInsertedTests.swift @@ -0,0 +1,55 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The `.inserted` event must carry the source parent and the number of +// features the admitted entry newly OWNED, so a draw-weight policy can credit +// the right parent by how much its mutant found (Score 1 = w ×(1+claimed)). +// + +import Testing +@testable import PropertyTestingKit + +@Suite("inserted carries parent + claimed") +struct AdaptiveDepthInsertedTests { + + private final class Recorder: PoolPlugin { + var events: [PoolEvent] = [] + func handle(event: PoolEvent) -> [PoolAction] { events.append(event); return [] } + } + + @Test("inserted reports source parent and newly-owned feature count") + func insertedCarriesParentAndClaimed() { + let rec = Recorder() + let core = WeightedPoolHarness.core(admission: .featureOwnership, policies: [rec]) + + // Entry 0: generated, owns edges {1,2}. + _ = WeightedPoolHarness.accept(core, edges: [1, 2]) + // Entry 1: a mutant of parent 0, owns one NEW edge {3}. + _ = WeightedPoolHarness.accept(core, edges: [3], parent: 0) + + func inserted(_ id: Int) -> (parent: Int?, claimed: Int)? { + for e in rec.events { + if case let .inserted(eid, _, _, parent, claimed) = e, eid == id { + return (parent, claimed) + } + } + return nil + } + + #expect(inserted(0)?.parent == nil) // generated → no parent + #expect(inserted(0)?.claimed == 2) // claimed {1,2} + #expect(inserted(1)?.parent == 0) // mutant of entry 0 + #expect(inserted(1)?.claimed == 1) // claimed {3} + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthMathTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthMathTests.swift new file mode 100644 index 00000000..2410c7b4 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthMathTests.swift @@ -0,0 +1,106 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The pure scoring math for the productivity-weighted, adaptive-depth pool +// policy. Two scores, both per-seed: +// 1. draw weight — spikes ×(1+n) on an n-feature-owning mutant, decays ×0.95 +// on a fruitless one. Asymptotic to 0, never 0. +// 2. depth cascade — per-level "advance past this depth" scores in [0, C<100); +// a miss climbs the stop level slowly toward C (never reaches +// it), a hit anchors it. Sampling walks the levels. +// These functions are pinned here against hand-computed values; the policy +// (AdaptiveDepthPolicy) wires them onto the pool event stream. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("Adaptive-depth scoring math") +struct AdaptiveDepthMathTests { + + // MARK: - Score 1: draw weight + + @Test("owning n features multiplies weight by (1 + n)") + func weightSpikesWithOwnership() { + #expect(adaptiveDrawWeightUpdate(1.0, ownedFeatures: 3) == 4.0) + #expect(adaptiveDrawWeightUpdate(2.0, ownedFeatures: 1) == 4.0) + #expect(adaptiveDrawWeightUpdate(1.0, ownedFeatures: 1) == 2.0) + } + + @Test("a fruitless mutant decays weight by 0.95") + func weightDecaysOnMiss() { + #expect(adaptiveDrawWeightUpdate(1.0, ownedFeatures: 0) == 0.95) + #expect(adaptiveDrawWeightUpdate(10.0, ownedFeatures: 0) == 9.5) + } + + @Test("weight never reaches zero under unbounded decay") + func weightNeverZero() { + var w = 1.0 + for _ in 0..<100_000 { w = adaptiveDrawWeightUpdate(w, ownedFeatures: 0) } + #expect(w > 0.0) + } + + // MARK: - Score 2: depth advance scores + + @Test("a miss climbs the level slowly toward the ceiling") + func depthClimbsOnMiss() { + // Formula characterization (explicit params, default-independent). + // s=0, alpha=0.05, C=90 → 0 + 0.05*90 = 4.5 + #expect(depthAdvanceUpdate(0.0, hit: false, alpha: 0.05, ceiling: 90) == 4.5) + // s=4.5 → 4.5 + 0.05*(90-4.5) = 4.5 + 4.275 = 8.775 + #expect(abs(depthAdvanceUpdate(4.5, hit: false, alpha: 0.05, ceiling: 90) - 8.775) < 1e-9) + } + + @Test("tuned defaults climb slowly toward a low ceiling") + func tunedDefaultsAreShallow() { + // The swept optimum (alpha=0.02, ceiling=45) is the default: a miss from + // 0 advances only 0.02*45 = 0.9 toward a 45 asymptote — far shallower than + // the original 0.05/90 (which overshot into the 0%-productive deep tail). + #expect(abs(depthAdvanceUpdate(0.0, hit: false) - 0.9) < 1e-9) + } + + @Test("a hit anchors the level by decaying it") + func depthAnchorsOnHit() { + #expect(abs(depthAdvanceUpdate(10.0, hit: true) - 9.5) < 1e-9) + } + + @Test("depth advance score never reaches the ceiling") + func depthNeverReachesCeiling() { + var s = 0.0 + for _ in 0..<100_000 { s = depthAdvanceUpdate(s, hit: false, alpha: 0.05, ceiling: 90) } + #expect(s < 90.0) + } + + // MARK: - Score 2: cascade sampling + + @Test("score 0 always stops at the current depth") + func sampleStopsWhenScoreZero() { + #expect(sampleMutationDepth(scores: [0.0], rolls: [50.0]) == 1) + } + + @Test("roll below the level's score advances deeper") + func sampleAdvancesWhenRollBelowScore() { + // [90]: r=50<90 → advance past the only level → new rung at depth 2 + #expect(sampleMutationDepth(scores: [90.0], rolls: [50.0]) == 2) + // [90,90]: advance, advance → depth 3 + #expect(sampleMutationDepth(scores: [90.0, 90.0], rolls: [50.0, 50.0]) == 3) + } + + @Test("roll at or above the level's score stops there") + func sampleStopsWhenRollAboveScore() { + #expect(sampleMutationDepth(scores: [90.0], rolls: [95.0]) == 1) + // advance level 0 (40<50), stop at level 1 (60≥50) → depth 2 + #expect(sampleMutationDepth(scores: [50.0, 50.0], rolls: [40.0, 60.0]) == 2) + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthPolicyTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthPolicyTests.swift new file mode 100644 index 00000000..4b860833 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/AdaptiveDepthPolicyTests.swift @@ -0,0 +1,97 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The productivity-weighted, adaptive-depth policy: a hit spikes the parent's +// draw weight, a miss decays it, and sustained misses at a depth escalate that +// seed's mutation depth while a productive depth stays shallow. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("Adaptive-depth pool policy") +struct AdaptiveDepthPolicyTests { + + private func setWeight(_ actions: [PoolAction], id: Int) -> Double? { + for a in actions { if case let .setWeight(i, w) = a, i == id { return w } } + return nil + } + private func setDepth(_ actions: [PoolAction], id: Int) -> Int? { + for a in actions { if case let .setMutationDepth(i, d) = a, i == id { return d } } + return nil + } + + private func insert(_ p: AdaptiveDepthPolicy, id: Int, parent: Int? = nil, claimed: Int = 1) { + _ = p.handle(event: .inserted( + id: id, coverage: SparseCoverage(indices: [UInt32(id) + 1]), + features: [UInt64(id) + 1], parent: parent, claimed: claimed)) + } + + @Test("an owning mutant spikes its parent's weight by (1 + claimed)") + func hitSpikesWeight() { + let p = AdaptiveDepthPolicy(roll: { 50.0 }) + insert(p, id: 0) + // A mutant of entry 0 finds new coverage... + _ = p.handle(event: .iteration(PoolIterationOutcome( + source: .pool(parent: 0), newCoverage: SparseCoverage(indices: [9])))) + // ...and is admitted owning 2 features. + insert(p, id: 1, parent: 0, claimed: 2) + let actions = p.handle(event: .willDraw) // flush the resolved mutant + #expect(setWeight(actions, id: 0) == 3.0) // 1.0 × (1 + 2) + } + + @Test("a fruitless mutant decays its parent's weight by 0.95") + func missDecaysWeight() { + let p = AdaptiveDepthPolicy(roll: { 50.0 }) + insert(p, id: 0) + _ = p.handle(event: .iteration(PoolIterationOutcome( + source: .pool(parent: 0), newCoverage: nil))) + let actions = p.handle(event: .willDraw) + #expect(setWeight(actions, id: 0) == 0.95) + } + + @Test("sustained misses at depth 1 escalate the seed's depth") + func sustainedMissesEscalateDepth() { + // Explicit alpha/ceiling=0.05/90 so the climbing score passes the fixed + // roll of 50 within the loop (the tuned defaults 0.02/45 asymptote below + // 50 by design); this test characterizes the escalate-on-miss behavior, + // not the tuned magnitude. + let p = AdaptiveDepthPolicy(alpha: 0.05, ceiling: 90.0, roll: { 50.0 }) // advances once a level's score passes 50 + insert(p, id: 0) + var maxDepth = 1 + for _ in 0..<40 { + _ = p.handle(event: .iteration(PoolIterationOutcome( + source: .pool(parent: 0), newCoverage: nil))) + let actions = p.handle(event: .willDraw) + if let d = setDepth(actions, id: 0) { maxDepth = max(maxDepth, d) } + } + #expect(maxDepth >= 2) // depth-1 neighborhood mined out → dig deeper + } + + @Test("a productive depth stays shallow (hits anchor depth 1)") + func productiveDepthStaysShallow() { + let p = AdaptiveDepthPolicy(roll: { 50.0 }) + insert(p, id: 0) + var escalated = false + for i in 0..<40 { + // Every mutant of entry 0 hits and is admitted (owns 1 new feature). + _ = p.handle(event: .iteration(PoolIterationOutcome( + source: .pool(parent: 0), newCoverage: SparseCoverage(indices: [UInt32(100 + i)])))) + insert(p, id: 1 + i, parent: 0, claimed: 1) + let actions = p.handle(event: .willDraw) + if let d = setDepth(actions, id: 0), d > 1 { escalated = true } + } + #expect(!escalated) // depth 1 keeps paying off → never escalates + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/AtomicFeatureSetTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/AtomicFeatureSetTests.swift new file mode 100644 index 00000000..3cb89fb1 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/AtomicFeatureSetTests.swift @@ -0,0 +1,62 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for AtomicFeatureSet: the lock-free insert-only UInt64 set that replaces +// the per-dispatch SyncBox(NSLock) in ComparisonCoverageStrategy.onCompare +// (Finding 42). Records the distinct value-profile features seen this run. + +import Testing +import Foundation +@testable import PropertyTestingKit + +@Suite("AtomicFeatureSet") +struct AtomicFeatureSetTests { + + @Test("insert dedups; snapshot is the distinct features") + func dedups() { + let set = AtomicFeatureSet() + set.insert(5); set.insert(5); set.insert(9) + #expect(Set(set.snapshot()) == [5, 9]) + } + + @Test("feature value 0 is recorded (not the empty-slot sentinel)") + func zeroRecorded() { + let set = AtomicFeatureSet() + set.insert(0); set.insert(0); set.insert(7) + #expect(Set(set.snapshot()) == [0, 7]) + } + + @Test("reset clears but the set is reusable") + func resetClears() { + let set = AtomicFeatureSet() + set.insert(1); set.insert(0) + set.reset() + #expect(set.snapshot().isEmpty) + set.insert(2) + #expect(Set(set.snapshot()) == [2]) + } + + @Test("concurrent inserts dedup exactly, no overflow") + func concurrentInserts() { + let set = AtomicFeatureSet() + let distinct = 500 + + DispatchQueue.concurrentPerform(iterations: 8) { _ in + for v in 0.. OwnershipLedger.Verdict { + var claimed = edges.claims(edges: features, size: size) + claimed += boundaries.claims(distances: distances, size: size) + return ledger.record(claimed: claimed) + } +} + +@Suite("Boundary-distance ownership (evaluators + ledger)") +struct BoundaryDistanceLedgerTests { + + @Test("An unowned boundary is claimed and the entry admitted") + func claimsUnownedBoundary() { + var judge = DualJudge() + let verdict = judge(features: [], size: 1, distances: [100: 8]) + #expect(verdict.admit) + #expect(verdict.evict.isEmpty) + } + + @Test("A strictly closer input steals the boundary; a farther or equal one does not") + func closerSteals() { + var judge = DualJudge() + _ = judge(features: [], size: 1, distances: [100: 8]) // entry 0 owns pc100 @ 8 + // Farther: nothing to claim. + #expect(!judge(features: [], size: 1, distances: [100: 9]).admit) + // Equal distance, equal size: ties don't steal. + #expect(!judge(features: [], size: 1, distances: [100: 8]).admit) + // Closer: claims it. + #expect(judge(features: [], size: 1, distances: [100: 3]).admit) + } + + @Test("Losing the last owned boundary evicts the previous owner") + func lastBoundaryLossEvicts() { + var judge = DualJudge() + _ = judge(features: [], size: 1, distances: [100: 8]) // entry 0 owns {pc100} + let verdict = judge(features: [], size: 1, distances: [100: 1]) + #expect(verdict.admit) + #expect(verdict.evict == [0]) + } + + @Test("Edge ownership (REDUCE by size) coexists with boundary ownership") + func edgesAndBoundariesAreAdditive() { + var judge = DualJudge() + // entry 0: owns edge 1 (size 3) and pc100 @ 8. + _ = judge(features: [1], size: 3, distances: [100: 8]) + // Smaller input claims edge 1 (REDUCE) but is FARTHER on pc100: admitted + // on the edge alone; entry 0 keeps pc100, so it survives. + let verdict = judge(features: [1], size: 2, distances: [100: 9]) + #expect(verdict.admit) + #expect(verdict.evict.isEmpty, "entry 0 still owns pc100") + } + + @Test("An entry that owns neither a new edge nor a closer boundary is rejected") + func noClaimRejected() { + var judge = DualJudge() + _ = judge(features: [1], size: 2, distances: [100: 4]) // entry 0 + let verdict = judge(features: [1], size: 5, distances: [100: 9]) + #expect(!verdict.admit) + } + + @Test("Both dimensions are additive in one verdict") + func bothDimensionsAdditive() { + var judge = DualJudge() + let v = judge(features: [1], size: 3, distances: [100: 8]) + #expect(v.admit) + #expect(v.claimed == 2, "1 edge + 1 boundary") + } + + @Test("On equal distance the smaller input steals the boundary (tie-break)") + func tieBreakBySize() { + var judge = DualJudge() + _ = judge(features: [], size: 5, distances: [100: 8]) // entry 0 owns pc100 @ 8, size 5 + // Same distance, strictly smaller input: steals (refinement over the old + // ledger, which never broke boundary ties). + let verdict = judge(features: [], size: 4, distances: [100: 8]) + #expect(verdict.admit) + #expect(verdict.evict == [0]) + } + + @Test("Admitted entries take sequential IDs across eviction") + func sequentialIDs() { + var judge = DualJudge() + #expect(judge(features: [], size: 1, distances: [100: 8]).entryID == 0) + #expect(judge(features: [], size: 1, distances: [100: 1]).entryID == 1) // evicts 0 + #expect(judge(features: [], size: 1, distances: [200: 4]).entryID == 2) + // A later tie on pc100 must contest the CURRENT owner (entry 1), not the + // dead entry 0. + #expect(!judge(features: [], size: 1, distances: [100: 1]).admit) + } +} + +// MARK: - Admission wired into the pool core + +@Suite("Boundary-distance admission") +struct BoundaryDistanceAdmissionTests { + + private final class Listener: PoolPlugin { + var removed: [Int] = [] + func handle(event: PoolEvent) -> [PoolAction] { + if case let .removed(id) = event { removed.append(id) } + return [] + } + } + + /// Drive the white-box `admit` seam with a generated outcome carrying both + /// edges and per-site boundary distances (the harness's `accept` doesn't + /// thread distances). + @discardableResult + private func admit( + _ core: WeightedPoolCore, edges: [UInt32], distances: [UInt64: UInt64] + ) -> Int? { + core.admit( + 0, coverage: SparseCoverage(indices: edges), + boundaryDistances: distances, poolSource: .generated) + } + + @Test("A strictly closer boundary admits and evicts the bankrupted owner") + func closerBoundaryEvicts() { + let listener = Listener() + let core = WeightedPoolHarness.core( + admission: .featureOwnership, policies: [listener]) + + // Entry 0 owns ONLY pc100 (no edges), so losing it bankrupts it. + #expect(admit(core, edges: [], distances: [100: 8]) == 0) + #expect(admit(core, edges: [], distances: [100: 2]) == 1) + #expect(listener.removed == [0]) + } + + @Test("Edge ownership still earns residence with no closer boundary") + func edgeRetentionSurvives() { + let core = WeightedPoolHarness.core( + admission: .featureOwnership, policies: []) + + #expect(admit(core, edges: [1], distances: [100: 8]) == 0) + // New edge, FARTHER boundary: admitted on the edge alone. + #expect(admit(core, edges: [2], distances: [100: 9]) == 1) + // Nothing new in either dimension: rejected. + #expect(admit(core, edges: [1, 2], distances: [100: 9]) == nil) + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/BoundaryDistanceStrategyTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/BoundaryDistanceStrategyTests.swift new file mode 100644 index 00000000..b563b0bc --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/BoundaryDistanceStrategyTests.swift @@ -0,0 +1,125 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the boundaryDistance strategy: an input is interesting when it +// drives some comparison site's operands STRICTLY CLOSER than this engine has +// seen (lower |arg1 - arg2|), OR covers a new edge. It publishes the run's +// per-site minimum distance so the pool can cull on boundary ownership. +// + +import Testing +import Foundation +import SanCovHooks +@testable import PropertyTestingKit + +@Suite("boundaryDistance strategy") +struct BoundaryDistanceStrategyTests { + + /// Drives one iteration through the real evaluator: reset, fire edges + a + /// single comparison, evaluate. Mirrors the proven-deterministic + /// `comparisonCoverage` harness (one `dispatch_cmp`, no array loop) so + /// real-code coverage is identical across identical-shape fires. Returns + /// the acceptance (nil when rejected). + private func makeHarness() -> ( + fire: (_ pc: UInt, _ a: UInt64, _ b: UInt64, _ edges: [UInt32], _ input: Int) -> CoverageAcceptance?, + teardown: () -> Void + ) { + let context = SanCovCounters.beginMeasurement() + let evaluator = CoverageStrategy.boundaryDistance.makeEvaluator() + evaluator.setup?(context) + let client = CoverageCountersClient.liveValue + + let fire: (UInt, UInt64, UInt64, [UInt32], Int) -> CoverageAcceptance? = { pc, a, b, edges, _ in + SanCovCounters.resetCoverage(context) + for e in edges { + var g = e + sancov_dispatch_edge(&g) + } + sancov_dispatch_cmp(pc, a, b, 8) + return evaluator.evaluate(context, client) + } + return (fire, { SanCovCounters.endMeasurement(context) }) + } + + @Test("A strictly closer distance at a site is interesting; replaying it is not") + func closerDistanceIsInteresting() { + let h = makeHarness() + defer { h.teardown() } + + // Identical-shape fires (same edges, same cmp call, same input): only + // the dispatched operand distance varies, so any acceptance after the + // first comes from the distance gradient, not real-code noise. + #expect(h.fire(0xAA, 4, 5, [40, 41], 1) != nil, "first sighting of pc 0xAA @ |4-5|=1") + #expect(h.fire(0xAA, 4, 5, [40, 41], 1) == nil, "same distance, same edges: nothing new") + #expect(h.fire(0xAA, 5, 5, [40, 41], 1) != nil, "|5-5|=0 is strictly closer") + } + + @Test("A farther distance at a known site is NOT interesting (monotone, unlike value profile)") + func fartherDistanceIsNotInteresting() { + let h = makeHarness() + defer { h.teardown() } + + _ = h.fire(0xBB, 5, 5, [40, 41], 1) // pc 0xBB @ 0 + #expect(h.fire(0xBB, 0, 9, [40, 41], 1) == nil, + "|0-9|=9 is farther than the seen 0 — earns nothing") + } + + @Test("Published distance is the absolute numeric difference, overflow-safe") + func publishesAbsoluteDifference() { + let h = makeHarness() + defer { h.teardown() } + + // The absolute NUMERIC difference (not Hamming): |0 - 2^40| = 2^40. + // Hamming distance here is 1, so a 2^40 result proves it is the numeric + // gradient. + let inRange = h.fire(0xDC, 0, 1 << 40, [40, 41], 1) + #expect(inRange?.boundaryDistances?[UInt64(0xDC)] == (1 << 40)) + + // A near-full-width difference must not trap (the `b &- a` path) and is + // stored exactly — the value word holds the whole 64-bit distance, no + // packing, no saturation. (A distance of exactly UInt64.max coincides + // with the "no hit yet" sentinel, so it can never read as strictly + // closer; a sub-maximal value exercises the full-width storage.) + let extreme = h.fire(0xDD, 1, .max, [40, 41], 1) + #expect(extreme?.boundaryDistances?[UInt64(0xDD)] == UInt64.max - 1, + "|1 - UInt64.max| computed and stored without overflow") + } + + @Test("A new edge is interesting via the union even with no closer distance") + func newEdgeIsInterestingViaUnion() { + let h = makeHarness() + defer { h.teardown() } + + _ = h.fire(0xEE, 4, 5, [40, 41], 1) + let acc = h.fire(0xEE, 4, 5, [40, 41, 42], 1) // identical cmp, one new edge + #expect(acc != nil, "boundaryDistance unions with edge coverage") + // An edge-only accept still publishes the run's distances so the + // ledger can claim boundaries on it. + #expect(acc?.boundaryDistances?[UInt64(0xEE)] == 1) + } + + @Test("boundaryDistance uses default edge recording plus a comparison observer") + func attachesCmpObserverAndDefaultEdgeRecording() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let evaluator = CoverageStrategy.boundaryDistance.makeEvaluator() + evaluator.setup?(context) + + #expect(sancov_context_get_recorder_for_testing(context.rawContext) == nil, + "no edge observer — edges use the default first-hit recorder for the union") + #expect(sancov_context_get_cmp_recorder_for_testing(context.rawContext) != nil, + "a comparison observer carries the distance state") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/BoundarySiteAccumulatorTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/BoundarySiteAccumulatorTests.swift new file mode 100644 index 00000000..0e20afa8 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/BoundarySiteAccumulatorTests.swift @@ -0,0 +1,126 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Unit tests for BoundarySiteAccumulator: the concrete open-addressing +// PC -> minDistance map that replaces the per-comparison Swift.Dictionary on +// the boundary cmp hot path. A single atomic word per site holds the minimum +// |arg1 - arg2| the run drove it to (Findings 45/46/47 removed the sign +// dimension — it bought no bug-finding). It must reduce by minimum distance, +// hold many distinct sites within its fixed capacity, aggregate correctly under +// concurrent (inherited-child-task) records without corruption — it is +// LOCK-FREE — and reset. + +import Testing +@testable import PropertyTestingKit + +@Suite("BoundarySiteAccumulator") +struct BoundarySiteAccumulatorTests { + + /// Snapshot as a [pc: distance] dict for order-independent assertions. + private func asDict(_ acc: BoundarySiteAccumulator) -> [UInt64: UInt64] { + var out: [UInt64: UInt64] = [:] + for s in acc.snapshot() { out[s.pc] = s.distance } + return out + } + + @Test("keeps the minimum distance across repeated hits of one site") + func minDistance() { + let acc = BoundarySiteAccumulator() + acc.record(pc: 100, distance: 5) + acc.record(pc: 100, distance: 2) // closest + acc.record(pc: 100, distance: 9) // farther, ignored + #expect(asDict(acc)[100] == 2) + } + + @Test("a strictly closer later hit lowers the recorded distance") + func closerHitLowers() { + let acc = BoundarySiteAccumulator() + acc.record(pc: 7, distance: 3) + acc.record(pc: 7, distance: 0) // distance 0 = the global min + #expect(asDict(acc)[7] == 0) + } + + @Test("distinct sites are all retained") + func distinctSites() { + let acc = BoundarySiteAccumulator() + acc.record(pc: 10, distance: 1) + acc.record(pc: 20, distance: 2) + acc.record(pc: 30, distance: 3) + let d = asDict(acc) + #expect(d.count == 3) + #expect(d[10] == 1 && d[20] == 2 && d[30] == 3) + } + + @Test("a full-width distance is stored without overflow or saturation") + func fullWidthDistance() { + let acc = BoundarySiteAccumulator() + acc.record(pc: 42, distance: UInt64.max) + #expect(asDict(acc)[42] == UInt64.max) + } + + @Test("retains many distinct sites within the fixed capacity") + func manyDistinctSites() { + let acc = BoundarySiteAccumulator() + // Far more distinct PCs than the small initial table the old grow-based + // version started with, but within the fixed capacity. Each hit twice, + // smaller distance the second time. + let n: UInt64 = 5000 + for pc in 1...n { acc.record(pc: pc &* 2654435761, distance: 50) } + for pc in 1...n { acc.record(pc: pc &* 2654435761, distance: 7) } + let d = asDict(acc) + #expect(d.count == Int(n)) + #expect(!acc.didOverflow) + for pc in [UInt64(1), 2500, n] { + #expect(d[pc &* 2654435761] == 7, "min distance for pc \(pc &* 2654435761)") + } + } + + @Test("concurrent records aggregate without corruption (lock-free safety)") + func concurrentRecords() async { + let acc = BoundarySiteAccumulator() + // 8 tasks hammer 16 shared sites at once — the inherited-child-task case + // the accumulator must survive lock-free. Every task drives each site to + // distance 0, so the converged min is unambiguous. + await withTaskGroup(of: Void.self) { group in + for _ in 0..<8 { + group.addTask { + for r in 0..<5000 { + let pc = UInt64((r % 16) + 1) + let distance = UInt64((r / 16) % 50) // hits 0 for each site + acc.record(pc: pc, distance: distance) + } + } + } + } + let d = asDict(acc) + #expect(d.count == 16, "no claims lost under contention") + #expect(!acc.didOverflow) + for pc in UInt64(1)...16 { + #expect(d[pc] == 0, "global min survived the races for pc \(pc)") + } + } + + @Test("reset clears all entries") + func resetClears() { + let acc = BoundarySiteAccumulator() + acc.record(pc: 1, distance: 1) + acc.record(pc: 2, distance: 2) + acc.reset() + #expect(acc.snapshot().isEmpty) + // Reusable after reset. + acc.record(pc: 3, distance: 3) + #expect(asDict(acc)[3] == 3) + #expect(acc.snapshot().count == 1) + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CmpOnlySchedulerTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CmpOnlySchedulerTests.swift new file mode 100644 index 00000000..4082a1ed --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/CmpOnlySchedulerTests.swift @@ -0,0 +1,77 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// cmp as a first-class, scheduler-vended signal — the payoff of the +// Signal/Evaluator/Ledger decomposition. The scheduler carries the comparison +// signal itself (`.boundaryDistance`): it vends the cmp-recording provider, and +// `featureOwnership` culls over the comparison-distance axis through the same +// `OwnershipLedger` as edges (the boundary evaluator is a peer of the edge +// evaluator, not a bolt-on). The engine names no signal. +// +// NOTE on scope: a *pure* cmp-only run (no edge axis at all, `.boundaryDistanceOnly`) +// additionally needs (a) an instrumented comparison in the SUT — a bare stdlib +// `Int ==` lowers to the uninstrumented standard library at -Onone, so it emits +// no `trace_cmp` in-target — and (b) a corpus retention signature that isn't +// `SparseCoverage` (edges). Both are the remaining Phase-3 work; this test pins +// the part that is wired and green: cmp travels with the scheduler and shapes +// the pool. +// + +import Testing +@testable import PropertyTestingKit +@testable import FuzzCore + +@Suite("Cmp signal scheduler") +struct CmpOnlySchedulerTests { + + /// Deterministic, load-independent stop (a busy core can run zero iterations + /// in a small wall-clock budget — see PoollessSchedulerTests). + private func stopAfter(_ count: Int) -> FuzzPlugin { + let seen = SyncBox(0) + return FuzzPlugin(id: "iteration_counter", handleSync: { event in + switch event { + case .iteration: + seen.update { $0 += 1 } + return seen.value >= count + ? [.stop(.init(reason: .custom("observed_enough")))] + : [] + } + }) + } + + @Test("A scheduler that vends the cmp signal drives the engine and culls over it") + func cmpSignalSchedulerRuns() async throws { + let result = try await fuzz( + duration: .seconds(60), + persistence: .ephemeral, + // The scheduler carries the comparison signal: it vends the + // cmp-recording provider and culls over the boundary-distance axis + // (composed with edges) through the unified ownership ledger. + scheduler: MutationScheduler.weightedPool(coverageStrategy: .boundaryDistance), + parallelism: 1, + plugins: { [self.stopAfter(200)] } + ) { (input: Int) in + if input % 2 == 0 { + blackHole(input &* 3) + } else { + blackHole(input &+ 1) + } + } + + // The cmp-signal scheduler drove the engine and retained interesting + // inputs through the same ledger edges use. + #expect(result.stats.totalInputs > 0) + #expect(result.corpus.count > 0, "the cmp-signal scheduler must drive retention") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/ComparisonCoverageStrategyTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/ComparisonCoverageStrategyTests.swift new file mode 100644 index 00000000..cad14f85 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/ComparisonCoverageStrategyTests.swift @@ -0,0 +1,112 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the comparisonCoverage strategy: value-profile (cmplog) novelty. +// An input is interesting when it produces a (comparison-site, Hamming +// distance of the operands) pair this engine hasn't seen, OR a new edge. The +// Hamming-distance gradient is what pulls inputs toward a boundary `i < c`. +// + +import Testing +import Foundation +import SanCovHooks +@testable import PropertyTestingKit + +@Suite("comparisonCoverage strategy") +struct ComparisonCoverageStrategyTests { + + /// Drives one iteration through the real evaluator: reset, fire edges + one + /// comparison, then evaluate. Returns whether the input was accepted. + private func makeHarness() -> ( + fire: (_ pc: UInt, _ a: UInt64, _ b: UInt64, _ edges: [UInt32], _ input: Int) -> Bool, + teardown: () -> Void + ) { + let context = SanCovCounters.beginMeasurement() + let evaluator = CoverageStrategy.comparisonCoverage.makeEvaluator() + evaluator.setup?(context) + let client = CoverageCountersClient.liveValue + + let fire: (UInt, UInt64, UInt64, [UInt32], Int) -> Bool = { pc, a, b, edges, _ in + SanCovCounters.resetCoverage(context) + for e in edges { + var g = e + sancov_dispatch_edge(&g) + } + sancov_dispatch_cmp(pc, a, b, 8) + return evaluator.evaluate(context, client) != nil + } + return (fire, { SanCovCounters.endMeasurement(context) }) + } + + @Test("A new (site, Hamming distance) pair is interesting; replaying it is not") + func newComparisonFeatureIsInteresting() { + let h = makeHarness() + defer { h.teardown() } + + // Same edges both passes (so novelty can only come from the comparison). + let first = h.fire(0xAA, 4, 5, [40, 41], 1) + let replay = h.fire(0xAA, 4, 5, [40, 41], 2) + + #expect(first, "a never-seen comparison feature is interesting") + #expect(!replay, "replaying the identical comparison is not interesting") + } + + @Test("Approaching the boundary (new Hamming distance at the same site) is interesting") + func boundaryApproachIsInteresting() { + let h = makeHarness() + defer { h.teardown() } + + // distance(8 ^ 5) = popcount(1101) = 3, then distance(4 ^ 5) = popcount(1) = 1. + _ = h.fire(0xBB, 8, 5, [40, 41], 1) // seed the site + let closer = h.fire(0xBB, 4, 5, [40, 41], 2) // a new distance at the same site + + #expect(closer, "a new operand distance at a known site is a new value-profile feature") + } + + @Test("A new edge is interesting even with no new comparison feature (union)") + func newEdgeIsInterestingViaUnion() { + let h = makeHarness() + defer { h.teardown() } + + _ = h.fire(0xCC, 4, 5, [40, 41], 1) // seed both the site and edges + let newEdge = h.fire(0xCC, 4, 5, [40, 41, 42], 2) // identical cmp, one new edge + + #expect(newEdge, "comparisonCoverage unions with edge coverage") + } + + @Test("Neither a new edge nor a new comparison feature is not interesting") + func nothingNewIsNotInteresting() { + let h = makeHarness() + defer { h.teardown() } + + _ = h.fire(0xDD, 4, 5, [40, 41], 1) + let stale = h.fire(0xDD, 4, 5, [40, 41], 2) + + #expect(!stale, "an input that repeats known edges and a known comparison is rejected") + } + + @Test("comparisonCoverage uses default edge recording plus a comparison observer") + func attachesCmpObserverAndDefaultEdgeRecording() { + let context = SanCovCounters.beginMeasurement() + defer { SanCovCounters.endMeasurement(context) } + + let evaluator = CoverageStrategy.comparisonCoverage.makeEvaluator() + evaluator.setup?(context) + + #expect(sancov_context_get_recorder_for_testing(context.rawContext) == nil, + "no edge observer — edges use the default first-hit recorder (covered_indices feed the union)") + #expect(sancov_context_get_cmp_recorder_for_testing(context.rawContext) != nil, + "a comparison observer carries the value-profile state") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/ComparisonDictionaryTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/ComparisonDictionaryTests.swift new file mode 100644 index 00000000..12eb133f --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/ComparisonDictionaryTests.swift @@ -0,0 +1,89 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for ComparisonDictionary: the learned pool of comparison operands that +// backs input-to-state mutation. Operands captured by the cmp observer land +// here; numeric mutators sample from `.current` to jump straight to a value a +// comparison cared about. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("ComparisonDictionary") +struct ComparisonDictionaryTests { + + @Test("An empty dictionary samples nil") + func emptySamplesNil() { + let dict = ComparisonDictionary() + var rng = FastRNG() + #expect(dict.randomValue(using: &rng) == nil) + #expect(dict.isEmpty) + } + + @Test("A recorded value is sampled back") + func recordedValueIsSampled() { + let dict = ComparisonDictionary() + dict.record(0xDEADBEEF) + var rng = FastRNG() + #expect(!dict.isEmpty) + #expect(dict.randomValue(using: &rng) == 0xDEADBEEF) + } + + @Test("Sampling only ever returns recorded values") + func samplingReturnsOnlyRecorded() { + let dict = ComparisonDictionary() + let recorded: Set = [10, 20, 30, 40] + for v in recorded { dict.record(v) } + var rng = FastRNG() + for _ in 0..<100 { + guard let v = dict.randomValue(using: &rng) else { + Issue.record("non-empty dictionary returned nil") + return + } + #expect(recorded.contains(v)) + } + } + + @Test("The dictionary is bounded to its capacity (ring eviction)") + func boundedToCapacity() { + let dict = ComparisonDictionary(capacity: 8) + // Record well past capacity; only the most recent `capacity` survive. + for v in 0..<100 { dict.record(UInt64(v)) } + var rng = FastRNG() + var seen = Set() + for _ in 0..<500 { + if let v = dict.randomValue(using: &rng) { seen.insert(v) } + } + #expect(seen.count <= 8, "at most `capacity` distinct values are retained") + // The oldest values (0, 1, ...) must have been evicted by the newest. + #expect(!seen.contains(0), "the oldest recorded value is evicted") + #expect(seen.contains(99), "the most recent recorded value is retained") + } + + @Test("current is nil outside a withValue scope") + func currentNilByDefault() { + #expect(ComparisonDictionary.current == nil) + } + + @Test("withValue installs the dictionary as current for the scope") + func withValueInstallsCurrent() { + let dict = ComparisonDictionary() + #expect(ComparisonDictionary.current == nil) + ComparisonDictionary.$current.withValue(dict) { + #expect(ComparisonDictionary.current === dict) + } + #expect(ComparisonDictionary.current == nil, "current is restored after the scope") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CorpusTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CorpusTests.swift deleted file mode 100644 index bfaa5a0d..00000000 --- a/Tests/PropertyTestingKitTests/Fuzzing/CorpusTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2026 DoorDash, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Testing -import Foundation -import Dependencies -import FunctionSpy -@testable import PropertyTestingKit - -@Suite("Corpus") -struct CorpusTests { - - @Test("Corpus adds interesting entries") - func testCorpusAddsInteresting() { - let corpus = Corpus() - var signatureHashes = Set() - - let sparse1 = SparseCoverage(indices: [0]) - let sparse2 = SparseCoverage(indices: [1]) - let sparse3 = SparseCoverage(indices: [0]) // Duplicate coverage - - let added1 = corpus.addIfInteresting(input: (1), sparse: sparse1, signatureHashes: &signatureHashes) - let added2 = corpus.addIfInteresting(input: (2), sparse: sparse2, signatureHashes: &signatureHashes) - let added3 = corpus.addIfInteresting(input: (3), sparse: sparse3, signatureHashes: &signatureHashes) - - #expect(added1) - #expect(added2) - #expect(!added3) // Redundant - - #expect(corpus.count == 2) - } -} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CoverageEngineTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CoverageEngineTests.swift index 53e30a8f..55cf5f29 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/CoverageEngineTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/CoverageEngineTests.swift @@ -34,7 +34,6 @@ struct CoverageEngineTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() // The engine's state: a per-engine iteration counter. Only the FIRST // iteration of EACH engine is "interesting". @@ -62,7 +61,6 @@ struct CoverageEngineTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let strategy = CoverageStrategy(makeEngine: { let edges = PropertyTestingKit.SyncBox>([]) @@ -110,7 +108,6 @@ struct CoverageEngineTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let strategy = CoverageStrategy(makeEngine: { // Novelty state is the STRATEGY's own — no corpus access at all. @@ -131,51 +128,40 @@ struct CoverageEngineTests { // Identical instrumented code in both passes, reset through evaluate // (see the trie dispatch test for why): pass 1 brings new edges, // pass 2 replays exactly the same set. - func firePass(_ input: Int) -> Bool { + func firePass() -> Bool { SanCovCounters.resetCoverage(context) var g31: UInt32 = 31 var g32: UInt32 = 32 sancov_dispatch_edge(&g31) sancov_dispatch_edge(&g32) - let sparse = evaluator.evaluate(context, coverageClient) - if let s = sparse?.sparse { - corpus.mergeCoverageAndAdd(input: input, scheduleBytes: nil, sparse: s) - } - return sparse != nil + return evaluator.evaluate(context, coverageClient) != nil } - let first = firePass(1) - let second = firePass(2) + let first = firePass() + let second = firePass() #expect(first, "First pass covers unseen edges") #expect(!second, "An identical replay brings no new edge") - #expect(corpus.count == 1, "Only the novel run joins the corpus") } /// Strategies are pure judgement: they never see the corpus or the typed - /// input. When decide says yes, the ENGINE records the input — with its - /// coverage and schedule bytes — in the corpus. - @Test("The engine, not the strategy, records interesting inputs") - func engineOwnsStorage() { + /// input. An always-true decision is interesting, and a retained input's + /// schedule bytes ride with its corpus entry as a storage concern. + @Test("An always-true strategy judges interesting; schedule bytes ride with the entry") + func alwaysTrueJudgesInteresting() { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let strategy = CoverageStrategy { _ in true } let evaluator: CoverageEvaluator = strategy.makeEvaluator() let sparse = evaluator.evaluate(context, coverageClient) - if let s = sparse?.sparse { - corpus.mergeCoverageAndAdd(input: 7, scheduleBytes: [9, 9], sparse: s) - } #expect(sparse != nil, "An always-true decision is interesting") - #expect(corpus.count == 1, "The engine records the interesting input") - #expect(corpus.entries.first?.scheduleBytes == [9, 9], + let entry = CorpusEntry(input: 7, scheduleBytes: [9, 9]) + #expect(entry.scheduleBytes == [9, 9], "Schedule bytes ride with the entry as a storage concern") - #expect(corpus.entries.first?.sparseCoverage == sparse?.sparse, - "The entry carries the run's judged coverage") } /// The aggregate covered-edge set used to live on the `Corpus` as a bitmap; @@ -247,7 +233,6 @@ struct CoverageEngineTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let strategy = CoverageStrategy.newEdge let engine1: CoverageEvaluator = strategy.makeEvaluator() @@ -283,7 +268,6 @@ struct CoverageEngineTests { return SparseCoverage(indices: [1]) } ) - let corpus = Corpus() let strategy = CoverageStrategy { _ in false } let evaluator: CoverageEvaluator = strategy.makeEvaluator() @@ -307,19 +291,14 @@ struct CoverageEngineTests { return SparseCoverage(indices: [5]) } ) - let corpus = Corpus() let strategy = CoverageStrategy { coverage in !coverage.indices.isEmpty } let evaluator: CoverageEvaluator = strategy.makeEvaluator() let sparse = evaluator.evaluate(context, client) - if let s = sparse?.sparse { - corpus.mergeCoverageAndAdd(input: 1, scheduleBytes: nil, sparse: s) - } + #expect(sparse != nil, "a non-empty coverage decision is interesting") #expect(snapshots.value == 1, - "the decision's snapshot is reused for the corpus add") - #expect(corpus.entries.first?.sparseCoverage == sparse?.sparse, - "the entry carries the judged coverage") + "evaluating the decision takes exactly one coverage snapshot") } /// `decide` may live in instrumented code (a user's test target). Edges it @@ -332,7 +311,6 @@ struct CoverageEngineTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let observed = PropertyTestingKit.SyncBox>([]) let strategy = CoverageStrategy(makeEngine: { @@ -369,7 +347,7 @@ struct CoverageEngineTests { _ = try await fuzzWithMaxIterations( maxIterations: 8, persistence: .ephemeral, - coverageStrategy: strategy, + scheduler: MutationScheduler.weightedPool(coverageStrategy: strategy), parallelism: 4 ) { (_: Int) in } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CoverageGapDetectorTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CoverageGapDetectorTests.swift index 76caad73..ddc1a3ad 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/CoverageGapDetectorTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/CoverageGapDetectorTests.swift @@ -260,8 +260,7 @@ struct CoverageGapDetectorTests { @Test("Coverage gap detection in fuzz result") func fuzzResultIncludesGapReport() async throws { // Verify that FuzzResult has the coverageGapReport computed property - let emptyCorpus = Corpus() - let emptySnapshot = await emptyCorpus.snapshot() + let emptySnapshot = CorpusSnapshot(entries: []) let stats = FuzzStats( totalInputs: 0, seeds: 0, @@ -284,7 +283,10 @@ struct CoverageGapDetectorTests { @Test("Realistic coverage gap test") func realisticCoverageGapTest() async throws { - // Use a hash-based check that value profile can't solve easily + // Use a hash-based check that value profile can't solve easily. + // `funcAnchor` pins the expected gap line to the function below via `#line`, + // so edits ELSEWHERE in this file can't shift it (only edits to the body do). + let funcAnchor = #line @Sendable func partiallyCoveredFunction(input: Int) { // Simple hash to defeat value profile guidance @@ -305,8 +307,9 @@ struct CoverageGapDetectorTests { // This test intentionally creates a coverage gap to verify detection works. // The detector reports the edge AFTER the unreachable body — i.e. the line - // of the `} else if input < 0 {` above. Update if the function above is edited. - let expectedLine = 299 + // of the `} else if input < 0 {`, which is `funcAnchor + 12`. Update the + // offset only if the function body above is edited. + let expectedLine = funcAnchor + 12 // Realistic regression test: replay the on-disk corpus // (Corpus/realisticCoverageGapTest/corpus.json = [[0],[-1]]; 0 → `else`, diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CoverageStrategyCompositionTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CoverageStrategyCompositionTests.swift new file mode 100644 index 00000000..7e7b5701 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/CoverageStrategyCompositionTests.swift @@ -0,0 +1,107 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for composing coverage strategies: CoverageStrategy.compose / .combined +// builds one engine whose acceptance is the UNION of the substrategies (an +// input is interesting iff ANY substrategy finds it so) and whose pool +// vocabularies are the namespaced union of the substrategies'. This is what +// lets the comparison channel mix-and-match with edge strategies, e.g. +// `.pathTrie.combined(with: .boundaryDistanceOnly)`. +// + +import Testing +import Foundation +import SanCovHooks +@testable import PropertyTestingKit + +@Suite("Coverage strategy composition") +struct CoverageStrategyCompositionTests { + + /// Drive one iteration through a composed strategy's real evaluator: reset, + /// fire edges + one comparison, evaluate. Returns the acceptance (nil when + /// rejected). Mirrors the BoundaryDistanceStrategy harness. + private func makeHarness(_ strategy: CoverageStrategy) -> ( + fire: (_ pc: UInt, _ a: UInt64, _ b: UInt64, _ edges: [UInt32]) -> CoverageAcceptance?, + teardown: () -> Void + ) { + let context = SanCovCounters.beginMeasurement() + let evaluator = strategy.makeEvaluator() + evaluator.setup?(context) + let client = CoverageCountersClient.liveValue + let fire: (UInt, UInt64, UInt64, [UInt32]) -> CoverageAcceptance? = { pc, a, b, edges in + SanCovCounters.resetCoverage(context) + for e in edges { var g = e; sancov_dispatch_edge(&g) } + sancov_dispatch_cmp(pc, a, b, 8) + return evaluator.evaluate(context, client) + } + return (fire, { SanCovCounters.endMeasurement(context) }) + } + + @Test("Composed acceptance is the union: edge novelty OR distance novelty triggers") + func acceptanceIsUnion() { + let h = makeHarness(.newEdge.combined(with: .boundaryDistanceOnly)) + defer { h.teardown() } + + // First sighting: new edges AND a first distance — interesting. + #expect(h.fire(0xAA, 4, 5, [10, 11]) != nil, "new edges + first distance") + // Same edges, same distance: neither substrategy finds novelty. + #expect(h.fire(0xAA, 4, 5, [10, 11]) == nil, "nothing new on either axis") + // Same edges, strictly closer distance: the cmp substrategy triggers. + #expect(h.fire(0xAA, 5, 5, [10, 11]) != nil, "|5-5|=0 strictly closer (cmp axis)") + // New edge, same (already-seen) distance: the edge substrategy triggers. + #expect(h.fire(0xAA, 5, 5, [12]) != nil, "new edge 12 (edge axis)") + } + + @Test("A composed cmp×edge strategy publishes BOTH vocabularies") + func publishesBothVocabularies() { + // pathTrie(gramLength:) publishes path k-gram `features`; + // boundaryDistanceOnly publishes `boundaryDistances`. The two channels + // are orthogonal, so a composed engine carries both at once. (Default + // .pathTrie publishes no features by design — it culls on edges — so the + // gram-length variant is used here to exercise the feature channel.) + let h = makeHarness(.pathTrie(gramLength: 2).combined(with: .boundaryDistanceOnly)) + defer { h.teardown() } + + let acc = h.fire(0xCC, 3, 9, [20, 21, 22]) + #expect(acc != nil, "first sighting is interesting") + #expect(acc?.features?.isEmpty == false, "pathTrie k-gram features present") + #expect(acc?.boundaryDistances?.isEmpty == false, "boundary distances present") + #expect(acc?.boundaryDistances?[UInt64(0xCC)] == 6, "site 0xCC distance |3-9|=6") + } + + @Test("Composition namespaces features so substrategies' raw values can't collide") + func featuresAreNamespaced() { + // Two stub substrategies that each always accept and publish the SAME + // raw feature value. Without namespacing they'd collapse to one feature + // in the shared ownership space; with it, two distinct features survive. + func stub(_ v: UInt64) -> CoverageStrategy { + CoverageStrategy(makeEngine: { CoverageEngine(features: { [v] }) { _ in true } }) + } + let h = makeHarness(.compose([stub(7), stub(7)])) + defer { h.teardown() } + + let acc = h.fire(0xDD, 1, 1, [30]) + #expect(acc != nil) + #expect(acc?.features?.count == 2, "two substrategies → two features, even with equal raw values") + #expect(Set(acc?.features ?? []).count == 2, "the namespaced features are distinct") + } + + @Test("A single-element compose is the identity (no namespacing churn)") + func singleComposeIsIdentity() { + let h = makeHarness(.compose([.newEdge])) + defer { h.teardown() } + #expect(h.fire(0xEE, 1, 2, [40]) != nil) + #expect(h.fire(0xEE, 1, 2, [40]) == nil, "replay of seen edges is not novel") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CustomCoverageStrategyTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CustomCoverageStrategyTests.swift index 76b49d78..d82924b3 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/CustomCoverageStrategyTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/CustomCoverageStrategyTests.swift @@ -35,7 +35,7 @@ struct CustomCoverageStrategyTests { maxIterations: 20, seeds: [1, 2, 3], persistence: .ephemeral, - coverageStrategy: everything + scheduler: MutationScheduler.weightedPool(coverageStrategy: everything) ) { (_: Int) in } #expect(!result.corpus.entries.isEmpty, "Custom strategy should have added corpus entries") @@ -49,7 +49,7 @@ struct CustomCoverageStrategyTests { maxIterations: 20, seeds: [1, 2, 3], persistence: .ephemeral, - coverageStrategy: nothing + scheduler: MutationScheduler.weightedPool(coverageStrategy: nothing) ) { (_: Int) in } #expect(result.corpus.entries.isEmpty, "Rejecting strategy should add nothing to the corpus") diff --git a/Tests/PropertyTestingKitTests/Fuzzing/CustomFuzzableTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/CustomFuzzableTests.swift index ce0ba86f..86b30541 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/CustomFuzzableTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/CustomFuzzableTests.swift @@ -45,7 +45,7 @@ struct CustomMutatorProvidingTests { let result = await fuzzEngineWithMaxIterations( maxIterations: 50, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (input: TestConfig) in await seenTimeouts.update { $0.insert(input.timeout) } await seenRetries.update { $0.insert(input.retries) } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/EdgeUnionBitmapTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/EdgeUnionBitmapTests.swift new file mode 100644 index 00000000..9c65eaa8 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/EdgeUnionBitmapTests.swift @@ -0,0 +1,60 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// EdgeUnionBitmap: the test-and-set edge-coverage union oracle that replaces +// Set across the coverage strategies (Finding 41m — Set.insert + Hasher +// was ~7% of the process). It must reproduce Set.insert's `.inserted` contract. + +import Testing +@testable import PropertyTestingKit + +@Suite("Edge union bitmap") +struct EdgeUnionBitmapTests { + + @Test("insert returns true on first sight, false on repeat") + func insertOnceThenRepeat() { + var u = EdgeUnionBitmap() + #expect(u.insert(7) == true) + #expect(u.insert(7) == false) + } + + @Test("distinct edges are all newly inserted; count tracks them") + func distinctEdges() { + var u = EdgeUnionBitmap() + // span multiple 64-bit words (0,1 | 63 | 64,65 | 200) + for e in [UInt32(0), 1, 63, 64, 65, 200] { + #expect(u.insert(e) == true) + } + #expect(u.count == 6) + #expect(u.insert(64) == false) + } + + @Test("empty until first insert") + func startsEmpty() { + var u = EdgeUnionBitmap() + #expect(u.isEmpty) + _ = u.insert(1000) + #expect(!u.isEmpty) + #expect(u.count == 1) + } + + @Test("a large sparse index grows lazily without losing prior bits") + func sparseGrowth() { + var u = EdgeUnionBitmap() + #expect(u.insert(5) == true) + #expect(u.insert(100_000) == true) + #expect(u.insert(5) == false) // prior bit preserved across the grow + #expect(u.count == 2) + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/EntropicPolicyTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/EntropicPolicyTests.swift index 18fafd1d..2a7d990e 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/EntropicPolicyTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/EntropicPolicyTests.swift @@ -38,7 +38,8 @@ struct EntropicPolicyTests { // The pool emits the entry's RESOLVED features; with no strategy // vocabulary that's the widened covered edges. _ = p.handle(event: .inserted( - id: id, coverage: SparseCoverage(indices: edges), features: edges.map(UInt64.init))) + id: id, coverage: SparseCoverage(indices: edges), features: edges.map(UInt64.init), + parent: nil, claimed: edges.count)) } /// A pool mutant of `parent` that elicited coverage (admitted or rejected — diff --git a/Tests/PropertyTestingKitTests/Fuzzing/FeatureHashSetTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/FeatureHashSetTests.swift new file mode 100644 index 00000000..ae33a0a3 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/FeatureHashSetTests.swift @@ -0,0 +1,66 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// FeatureHashSet: an open-addressing UInt64 membership set keyed on the value +// directly (no Swift Hasher / SipHash). The sign-/comparison-feature novelty +// oracles store already-splitmix-mixed 64-bit hashes, so re-hashing them with +// SipHash was pure waste (Finding 41n). It must reproduce Set.insert's +// `.inserted` contract, including the literal value 0. + +import Testing +@testable import PropertyTestingKit + +@Suite("Feature hash set") +struct FeatureHashSetTests { + + @Test("insert returns true on first sight, false on repeat") + func insertOnceThenRepeat() { + var s = FeatureHashSet() + #expect(s.insert(0xDEAD_BEEF_CAFE_F00D) == true) + #expect(s.insert(0xDEAD_BEEF_CAFE_F00D) == false) + } + + @Test("zero is a valid distinct member (sentinel-safe)") + func zeroMember() { + var s = FeatureHashSet() + #expect(s.isEmpty) + #expect(s.insert(0) == true) + #expect(s.insert(0) == false) + #expect(s.insert(1) == true) // 1 is distinct from the 0 sentinel slot + #expect(s.count == 2) + } + + @Test("distinct values are all new; count tracks them") + func distinctValues() { + var s = FeatureHashSet() + let vals: [UInt64] = [1, 2, 3, 1 << 40, .max, 0xFFFF, 7] + for v in vals { #expect(s.insert(v) == true) } + #expect(s.count == vals.count) + for v in vals { #expect(s.insert(v) == false) } + } + + @Test("growth past the initial capacity preserves all members") + func growthPreservesMembers() { + var s = FeatureHashSet(minimumCapacity: 8) + // Mixed values, well past the initial capacity to force several rehashes. + var inserted: [UInt64] = [] + for i in 0..<500 { + let v = UInt64(i) &* 0x9E37_79B9_7F4A_7C15 ^ 0xABCD + inserted.append(v) + #expect(s.insert(v) == true) + } + #expect(s.count == 500) + for v in inserted { #expect(s.insert(v) == false, "member lost across grow: \(v)") } + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/FeatureOwnershipTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/FeatureOwnershipTests.swift index 3b717b48..1c349832 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/FeatureOwnershipTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/FeatureOwnershipTests.swift @@ -24,61 +24,67 @@ import Testing @testable import PropertyTestingKit -// MARK: - Ledger (pure state machine) +// MARK: - REDUCE behavior (edge evaluator composed with the generic ledger) + +/// `featureOwnership` is now the edge evaluator (the REDUCE criterion) composed +/// with the metric-agnostic `OwnershipLedger` (the roster). This helper composes +/// them exactly as the admission does, so these pin the REDUCE behavior that used +/// to live in the single `FeatureOwnershipLedger.judge`. +private struct ReduceJudge { + var edges = EdgeOwnershipEvaluator() + var ledger = OwnershipLedger() + mutating func callAsFunction(_ features: [UInt64], _ size: Int) -> OwnershipLedger.Verdict { + ledger.record(claimed: edges.claims(edges: features, size: size)) + } +} -@Suite("Feature-ownership ledger") +@Suite("Feature-ownership (edge evaluator + ledger)") struct FeatureOwnershipLedgerTests { @Test("Unowned features are claimed and the entry is admitted") func claimsUnownedFeatures() { - var ledger = FeatureOwnershipLedger() - let verdict = ledger.judge(features: [1, 2], size: 2) + var judge = ReduceJudge() + let verdict = judge([1, 2], 2) #expect(verdict.admit) #expect(verdict.evict.isEmpty) } @Test("Rejects when every feature is owned by a smaller or equal entry") func rejectsWhenAllFeaturesOwned() { - var ledger = FeatureOwnershipLedger() - _ = ledger.judge(features: [1, 2], size: 2) // entry 0 owns {1,2} - // Same features, LARGER input: nothing claimable. - let larger = ledger.judge(features: [1, 2], size: 3) - #expect(!larger.admit) - // Same features, EQUAL size: ties don't steal. - let tie = ledger.judge(features: [1, 2], size: 2) - #expect(!tie.admit) + var judge = ReduceJudge() + _ = judge([1, 2], 2) // entry 0 owns {1,2} + #expect(!judge([1, 2], 3).admit) // LARGER: nothing claimable + #expect(!judge([1, 2], 2).admit) // EQUAL: ties don't steal } @Test("A smaller input steals ownership (REDUCE); the loser keeps its remainder") func smallerInputSteals() { - var ledger = FeatureOwnershipLedger() - _ = ledger.judge(features: [1, 2, 3], size: 3) // entry 0 owns {1,2,3} - let verdict = ledger.judge(features: [1, 2], size: 2) + var judge = ReduceJudge() + _ = judge([1, 2, 3], 3) // entry 0 owns {1,2,3} + let verdict = judge([1, 2], 2) #expect(verdict.admit) #expect(verdict.evict.isEmpty, "entry 0 still owns {3} — not evicted") } @Test("Losing the last owned feature evicts the loser") func lastLossEvicts() { - var ledger = FeatureOwnershipLedger() - _ = ledger.judge(features: [1, 2], size: 3) // entry 0 owns {1,2} - let verdict = ledger.judge(features: [1, 2], size: 2) + var judge = ReduceJudge() + _ = judge([1, 2], 3) // entry 0 owns {1,2} + let verdict = judge([1, 2], 2) #expect(verdict.admit) #expect(verdict.evict == [0]) } @Test("Admitted entries take sequential IDs; evicted IDs are never reused") func sequentialIDsAcrossEviction() { - var ledger = FeatureOwnershipLedger() - _ = ledger.judge(features: [1], size: 2) // entry 0 - _ = ledger.judge(features: [1], size: 1) // entry 1 evicts 0 - let verdict = ledger.judge(features: [9], size: 1) // entry 2 - #expect(verdict.admit) - // Entry 2's claim must not collide with the dead entry 0: stealing 9 - // from it would be impossible (unowned), and a later size-1 input on - // feature 1 must contest entry 1, not entry 0. - let contest = ledger.judge(features: [1], size: 1) - #expect(!contest.admit, "tie against the CURRENT owner (entry 1)") + var judge = ReduceJudge() + #expect(judge([1], 2).entryID == 0) // entry 0 + #expect(judge([1], 1).entryID == 1) // entry 1 evicts 0 + let verdict = judge([9], 1) // entry 2 + #expect(verdict.entryID == 2) + // A later size-1 input on feature 1 must contest the CURRENT owner + // (entry 1), not the dead entry 0. + #expect(!judge([1], 1).admit, "tie against the CURRENT owner (entry 1)") } } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/FuzzAPITests.swift b/Tests/PropertyTestingKitTests/Fuzzing/FuzzAPITests.swift index ac458222..941f19b4 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/FuzzAPITests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/FuzzAPITests.swift @@ -42,7 +42,7 @@ struct FuzzAPITests { maxIterations: 100, corpusDir: corpusDir, persistence: .auto, - coverageStrategy: .pathTrie, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .pathTrie), additionalSeeds: numberParserSeeds ) { input in // Just call parse - coverage will be tracked automatically @@ -91,7 +91,7 @@ struct FuzzAPITests { maxIterations: 100, corpusDir: corpusDir, persistence: .auto, - coverageStrategy: .alwaysInteresting, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), additionalSeeds: numberParserSeeds ) { input in _ = NumberParser.parse(input) @@ -116,7 +116,7 @@ struct FuzzAPITests { maxIterations: 100, seeds: ["0", "-0", "-1", "abc", String(Int.max)], persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { input in let parsed = NumberParser.parse(input) @@ -220,7 +220,7 @@ struct FuzzAPITests { let result = await fuzzEngineWithMaxIterations( maxIterations: 100, config: config, - coverageStrategy: .alwaysInteresting, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), additionalSeeds: [true, false] ) { (_: Bool) in // Throw for any input to guarantee a failure @@ -257,7 +257,7 @@ struct FuzzAPITests { _ = try await fuzzWithMaxIterations( maxIterations: 10, persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (_: Bool) in throw TestFailure() } @@ -281,7 +281,7 @@ struct FuzzAPITests { try await fuzzWithMaxIterations( maxIterations: 50, seeds: ["a", "ab", "abc"], - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { input in _ = input.count } @@ -292,13 +292,8 @@ struct FuzzAPITests { @Test("fuzz reads existing corpus from filesystem") func testFuzzReadsCorpus() async throws { - // Create a mock corpus with known entries - var existingCorpus = Corpus() - existingCorpus.add( - input: ("from_corpus"), - sparse: SparseCoverage(indices: [1]) - ) - let corpusSnapshot = existingCorpus.snapshot() + // Create a mock corpus snapshot with a known entry + let corpusSnapshot = CorpusSnapshot(entries: [CorpusEntry(input: "from_corpus")]) let corpusData = try JSONEncoder.corpusEncoder().encode(corpusSnapshot) let (loadSpy, loadFn) = spy { (_: URL) -> Data in @@ -320,7 +315,7 @@ struct FuzzAPITests { try await fuzzWithMaxIterations( maxIterations: 50, seeds: ["from_corpus"], - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { input in await seenInputs.update { $0.append(input) } } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/FuzzEngineTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/FuzzEngineTests.swift index 23e76a76..384678f6 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/FuzzEngineTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/FuzzEngineTests.swift @@ -88,7 +88,7 @@ struct FuzzEngineTests { let result = await fuzzEngineWithMaxIterations( maxIterations: 100, config: config, - coverageStrategy: .alwaysInteresting, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), additionalSeeds: [0, 1, -1, 42] ) { (_: Int) in } @@ -109,7 +109,7 @@ struct FuzzEngineTests { let result = await fuzzEngineWithMaxIterations( maxIterations: 100, config: config, - coverageStrategy: .alwaysInteresting, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), additionalSeeds: [0, 1, 42, -1] ) { (input: Int) in if input == 42 { @@ -131,7 +131,7 @@ struct FuzzEngineTests { let result = await fuzzEngineWithMaxIterations( maxIterations: 100, config: config, - coverageStrategy: .alwaysInteresting, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), additionalSeeds: [0, 1, -1, 42] ) { (_: Int) in } @@ -151,7 +151,7 @@ struct FuzzEngineTests { let result = await fuzzEngineWithMaxIterations( maxIterations: 100, config: config, - coverageStrategy: .alwaysInteresting, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), additionalSeeds: [0, 10, 20, 1, 2] ) { (input: Int) in if input % 10 == 0 { @@ -170,7 +170,7 @@ struct FuzzEngineTests { } operation: { await fuzzEngineWithMaxIterations( maxIterations: 50, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (_: Int) in } } @@ -228,7 +228,7 @@ struct FuzzEngineTests { let result = await fuzzEngineWithMaxIterations( maxIterations: 50, config: config, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (_: Int) in } #expect(result.corpus.count >= 1, "Should have corpus entries") @@ -239,7 +239,7 @@ struct FuzzEngineTests { func testEmptyFuzzArray() async { let result = await fuzzEngineWithMaxIterations( maxIterations: 20, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (_: EmptyFuzzable) in } // With empty seeds, no seeds are processed and iterations skip via guard @@ -252,7 +252,7 @@ struct FuzzEngineTests { func testEmptyMutationsArray() async { let result = await fuzzEngineWithMaxIterations( maxIterations: 20, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (_: EmptyMutationsFuzzable) in } // With one seed value, corpus gets one entry, then mutations fail @@ -424,7 +424,7 @@ struct FuzzEngineTests { return await fuzzEngineWithMaxIterations( maxIterations: 100, config: config, - coverageStrategy: .pathTrie, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .pathTrie), additionalSeeds: [0, 1, -1, 100, -100, Int.max, Int.min] ) { (input: Int) in // Exercise different code paths based on input diff --git a/Tests/PropertyTestingKitTests/Fuzzing/FuzzInputToStateTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/FuzzInputToStateTests.swift new file mode 100644 index 00000000..adf2d950 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/FuzzInputToStateTests.swift @@ -0,0 +1,72 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// End-to-end input-to-state test. This target is built with +// `-sanitize-coverage=…,trace-cmp`, so the `==` comparison in the SUT below +// fires a real comparison hook. With PTK_INPUT_TO_STATE the engine attaches a +// comparison observer that feeds the operands into a ComparisonDictionary the +// Int mutator samples from, so the fuzzer jumps straight to the magic constant +// — a bug that random search over a 64-bit space would essentially never hit. +// + +import Testing +import Foundation +@testable import PropertyTestingKit + +/// A magic-value bug: `false` (the property fails) exactly when `x` equals the +/// constant. The `==` is instrumented (trace-cmp), so each execution reports +/// `(x, magicConstant)` to the I2S dictionary. `@inline(never)` keeps it a +/// distinct comparison site. +private let magicConstant = 0x5EED_CAFE_1357 + +@inline(never) +private func magicValueHolds(_ x: Int) -> Bool { + x != magicConstant +} + +@Suite("Fuzzing input-to-state") +struct FuzzInputToStateTests { + + /// Runs one fuzz campaign over the magic-value SUT, returning whether the + /// bug was found within the budget. I2S is enabled via the task-local — + /// bound only in this call's task tree, so parallel tests never race on it. + private func campaignFindsMagic(inputToState: Bool) async throws -> Bool { + let found = PropertyTestingKit.SyncBox(false) + try await ComparisonDictionary.$inputToStateEnabled.withValue(inputToState) { + _ = try await fuzz( + duration: .seconds(3), + persistence: .ephemeral, + parallelism: 1 + ) { (x: Int) in + if !magicValueHolds(x) { found.update { $0 = true } } + } + } + return found.value + } + + /// Both phases in one test so the I2S task-local is bound and torn down + /// sequentially — no inter-test interference. With I2S the comparison + /// operand feeds the dictionary and the Int mutator jumps to the constant; + /// without it, random search over a ~10^14 space cannot stumble onto it. + @Test("I2S reaches a magic-value bug that random search cannot") + func inputToStateReachesMagicConstant() async throws { + let foundWithI2S = try await campaignFindsMagic(inputToState: true) + #expect(foundWithI2S, + "with I2S the Int mutator jumps to the learned comparison operand") + + let foundWithout = try await campaignFindsMagic(inputToState: false) + #expect(!foundWithout, + "without I2S, random mutation must not conjure a ~10^14 constant") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/HitCountAccumulatorTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/HitCountAccumulatorTests.swift new file mode 100644 index 00000000..9eece9dc --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/HitCountAccumulatorTests.swift @@ -0,0 +1,73 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for HitCountAccumulator: the lock-free per-edge hit counter that +// replaces the per-dispatch SyncBox(NSLock) in HitCountBucketsStrategy.onEdge +// (Finding 42 — that lock was taken ~714x per test). Mirrors +// BoundarySiteAccumulator: fixed capacity, per-slot atomics, O(occupied) drain. + +import Testing +import Foundation +@testable import PropertyTestingKit + +@Suite("HitCountAccumulator") +struct HitCountAccumulatorTests { + + @Test("counts hits per edge") + func countsPerEdge() { + let acc = HitCountAccumulator() + for _ in 0..<3 { acc.record(edge: 5) } + for _ in 0..<2 { acc.record(edge: 9) } + + let counts = Dictionary(uniqueKeysWithValues: acc.snapshot().map { ($0.edge, $0.count) }) + #expect(counts == [5: 3, 9: 2]) + } + + @Test("edge 0 is recorded (not confused with the empty-slot sentinel)") + func edgeZeroRecorded() { + let acc = HitCountAccumulator() + acc.record(edge: 0) + acc.record(edge: 0) + + let counts = Dictionary(uniqueKeysWithValues: acc.snapshot().map { ($0.edge, $0.count) }) + #expect(counts == [0: 2]) + } + + @Test("reset clears counts but the accumulator is reusable") + func resetClears() { + let acc = HitCountAccumulator() + acc.record(edge: 1) + acc.reset() + #expect(acc.snapshot().isEmpty) + + acc.record(edge: 2) + let counts = Dictionary(uniqueKeysWithValues: acc.snapshot().map { ($0.edge, $0.count) }) + #expect(counts == [2: 1]) + } + + @Test("concurrent records from many threads sum exactly (lock-free)") + func concurrentRecordsSumExactly() { + let acc = HitCountAccumulator() + let threads = 8 + let perThread = 2000 + + DispatchQueue.concurrentPerform(iterations: threads) { _ in + for _ in 0.. + _ coverageClient: CoverageCountersClient ) -> Bool { SanCovCounters.resetCoverage(context) var guardValue = edge for _ in 0..() let evaluator: CoverageEvaluator = CoverageStrategy.hitCountBuckets.makeEvaluator() evaluator.setup?(context) - #expect(firePass(evaluator, edge: 61, hits: 1, input: 1, context, coverageClient, corpus), + #expect(firePass(evaluator, edge: 61, hits: 1, context, coverageClient), "An unseen edge's first bucket is always new") - #expect(corpus.count == 1, "The interesting run joins the corpus") } @Test("An identical replay lands in known buckets and is not interesting") @@ -69,14 +61,13 @@ struct HitCountBucketsStrategyTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let evaluator: CoverageEvaluator = CoverageStrategy.hitCountBuckets.makeEvaluator() evaluator.setup?(context) - #expect(firePass(evaluator, edge: 62, hits: 1, input: 1, context, coverageClient, corpus), + #expect(firePass(evaluator, edge: 62, hits: 1, context, coverageClient), "First pass covers unseen buckets") - #expect(!firePass(evaluator, edge: 62, hits: 1, input: 2, context, coverageClient, corpus), + #expect(!firePass(evaluator, edge: 62, hits: 1, context, coverageClient), "A replay with identical hit counts brings no new bucket — and proves per-run counts reset between iterations") } @@ -85,14 +76,13 @@ struct HitCountBucketsStrategyTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let evaluator: CoverageEvaluator = CoverageStrategy.hitCountBuckets.makeEvaluator() evaluator.setup?(context) - #expect(firePass(evaluator, edge: 63, hits: 1, input: 1, context, coverageClient, corpus), + #expect(firePass(evaluator, edge: 63, hits: 1, context, coverageClient), "Count 1 = bucket {1}, unseen") - #expect(firePass(evaluator, edge: 63, hits: 2, input: 2, context, coverageClient, corpus), + #expect(firePass(evaluator, edge: 63, hits: 2, context, coverageClient), "Count 2 = bucket {2}: a new bucket on a KNOWN edge must be interesting — this is what .newEdge cannot see") } @@ -101,16 +91,15 @@ struct HitCountBucketsStrategyTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let evaluator: CoverageEvaluator = CoverageStrategy.hitCountBuckets.makeEvaluator() evaluator.setup?(context) - #expect(firePass(evaluator, edge: 64, hits: 4, input: 1, context, coverageClient, corpus), + #expect(firePass(evaluator, edge: 64, hits: 4, context, coverageClient), "Count 4 = bucket {4-7}, unseen") - #expect(!firePass(evaluator, edge: 64, hits: 5, input: 2, context, coverageClient, corpus), + #expect(!firePass(evaluator, edge: 64, hits: 5, context, coverageClient), "Count 5 is still bucket {4-7} — only the observed bucket was marked, not a threshold") - #expect(firePass(evaluator, edge: 64, hits: 2, input: 3, context, coverageClient, corpus), + #expect(firePass(evaluator, edge: 64, hits: 2, context, coverageClient), "Count 2 = bucket {2} was never observed (buckets below a seen one are not implied)") } @@ -119,20 +108,19 @@ struct HitCountBucketsStrategyTests { let context = SanCovCounters.beginMeasurement() defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() let strategy = CoverageStrategy.hitCountBuckets let engine1: CoverageEvaluator = strategy.makeEvaluator() let engine2: CoverageEvaluator = strategy.makeEvaluator() engine1.setup?(context) - #expect(firePass(engine1, edge: 65, hits: 1, input: 1, context, coverageClient, corpus), + #expect(firePass(engine1, edge: 65, hits: 1, context, coverageClient), "Engine 1: first sight") - #expect(!firePass(engine1, edge: 65, hits: 1, input: 2, context, coverageClient, corpus), + #expect(!firePass(engine1, edge: 65, hits: 1, context, coverageClient), "Engine 1: replay brings nothing new") engine2.setup?(context) - #expect(firePass(engine2, edge: 65, hits: 1, input: 3, context, coverageClient, corpus), + #expect(firePass(engine2, edge: 65, hits: 1, context, coverageClient), "Engine 2 judges with its OWN bucket state — engine 1 having seen these buckets must not decide for it") } @@ -218,7 +206,7 @@ struct HitCountBucketsStrategyTests { let result = try await fuzzWithMaxIterations( maxIterations: 50, persistence: .ephemeral, - coverageStrategy: .hitCountBuckets, + scheduler: MutationScheduler.weightedPool(coverageStrategy: .hitCountBuckets), parallelism: 2 ) { (input: Int) in // Input-dependent loop so hit counts actually vary across inputs. diff --git a/Tests/PropertyTestingKitTests/Fuzzing/InstrumentationSeamTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/InstrumentationSeamTests.swift index 34b66526..51c35ec8 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/InstrumentationSeamTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/InstrumentationSeamTests.swift @@ -93,12 +93,15 @@ struct InstrumentationSeamTests { /// A scheduler that reads the count probe and retains inputs that covered /// something — proving a scheduler decides admission purely from a view it - /// asked for, owns its own production, and erases behind `AnyScheduler` with - /// the engine naming no signal. Generic over the pack, like any real + /// asked for, owns its own production and working set, and erases behind + /// `AnyScheduler` with the engine naming no signal. `observe` folds into its + /// own `retained` set; `snapshot` vends that set (which the engine reads back + /// at run-end to build the corpus). Generic over the pack, like any real /// scheduler that owns the typed inputs it schedules. final class CountThresholdScheduler { let requiredProbes: [any InstrumentationKey.Type] = [CountKey.self] private(set) var observedEdges: [Int] = [] + private(set) var retained: [(repeat each Input)] = [] private let produce: () -> ScheduledInput init(produce: @escaping () -> ScheduledInput) { @@ -111,43 +114,49 @@ struct InstrumentationSeamTests { _ input: (repeat each Input), _ context: RawExecutionContext, source: SchedulerSource - ) -> SparseCoverage? { - guard let view = context[CountKey.self] else { return nil } + ) { + guard let view = context[CountKey.self] else { return } observedEdges.append(view.edges) - guard view.edges > 0 else { return nil } - return SparseCoverage() // a non-nil signature ⇒ retain + guard view.edges > 0 else { return } + retained.append((repeat each input)) // covered something ⇒ retain } + func snapshot() -> [(repeat each Input)] { retained } + func erased() -> AnyScheduler { AnyScheduler( requiredProbes: requiredProbes, next: { self.next() }, - observe: { self.observe($0, $1, source: $2) } + observe: { self.observe($0, $1, source: $2) }, + snapshot: { self.snapshot() } ) } } - @Test("A scheduler reads its required probe and signals admission") + @Test("A scheduler reads its required probe and retains on admission") func schedulerReadsProbeAndAdmits() { let sched = CountThresholdScheduler( produce: { ScheduledInput(input: 0, poolParentID: nil) }) var hit = RawExecutionContext() hit.set(CountKey.self, CountView(edges: 3)) - #expect(sched.observe(0, hit, source: .scheduled) != nil) + sched.observe(0, hit, source: .scheduled) #expect(sched.observedEdges == [3]) + #expect(sched.snapshot().count == 1) // covered ⇒ retained var miss = RawExecutionContext() miss.set(CountKey.self, CountView(edges: 0)) - #expect(sched.observe(0, miss, source: .scheduled) == nil) + sched.observe(0, miss, source: .scheduled) + #expect(sched.snapshot().count == 1) // covered nothing ⇒ not retained } @Test("A scheduler retains nothing when its probe is absent") func schedulerIgnoresAbsentProbe() { let sched = CountThresholdScheduler( produce: { ScheduledInput(input: 0, poolParentID: nil) }) - #expect(sched.observe(0, RawExecutionContext(), source: .external) == nil) + sched.observe(0, RawExecutionContext(), source: .external) #expect(sched.observedEdges.isEmpty) + #expect(sched.snapshot().isEmpty) } @Test("requiredProbes advertises the scheduler's instrumentation needs") diff --git a/Tests/PropertyTestingKitTests/Fuzzing/IntInputToStateTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/IntInputToStateTests.swift new file mode 100644 index 00000000..b9a282e0 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/IntInputToStateTests.swift @@ -0,0 +1,80 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The framework's Int mutator consults ComparisonDictionary.current for +// input-to-state mutation: when a dictionary is installed, mutation/generation +// frequently jumps to a recorded comparison operand (or its ±1 neighbour, for +// `<`/`<=` boundary bugs). With no dictionary it behaves exactly as before. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("Int input-to-state mutation") +struct IntInputToStateTests { + + /// With a magic operand recorded and the dictionary installed, the Int + /// mutator must reach that operand (or a ±1 neighbour) far more often than + /// random search would — that is the whole point of I2S. + @Test("Mutation jumps to a recorded operand when a dictionary is current") + func mutationReachesRecordedOperand() { + let dict = ComparisonDictionary() + let magic = 0x0BADF00D + dict.record(UInt64(magic)) + + let targets: Set = [magic, magic + 1, magic - 1] + var hits = 0 + ComparisonDictionary.$current.withValue(dict) { + var rng = FastRNG() + for _ in 0..<1000 { + if targets.contains(Int.defaultMutator.mutate(0, &rng)) { hits += 1 } + } + } + // Random mutation of 0 would essentially never produce 0x0BADF00D; I2S + // should hit the operand neighbourhood a large fraction of the time. + #expect(hits > 100, "I2S should reach the recorded operand frequently (got \(hits)/1000)") + } + + @Test("Generation can produce a recorded operand when a dictionary is current") + func generationReachesRecordedOperand() { + let dict = ComparisonDictionary() + let magic = 0x0BADF00D + dict.record(UInt64(magic)) + + let targets: Set = [magic, magic + 1, magic - 1] + var hits = 0 + ComparisonDictionary.$current.withValue(dict) { + var rng = FastRNG() + for _ in 0..<1000 { + if targets.contains(Int.defaultMutator.generate(&rng)) { hits += 1 } + } + } + #expect(hits > 100, "I2S should generate the recorded operand frequently (got \(hits)/1000)") + } + + /// No dictionary installed → never produces the magic value, and mutation + /// still works normally (the I2S branch is inert). + @Test("With no dictionary current, mutation is unaffected") + func noDictionaryLeavesMutationUnchanged() { + let magic = 0x0BADF00D + var rng = FastRNG() + var sawMagic = false + for _ in 0..<1000 { + let m = Int.defaultMutator.mutate(0, &rng) + if m == magic { sawMagic = true } + } + #expect(ComparisonDictionary.current == nil) + #expect(!sawMagic, "without I2S, mutating 0 must not conjure the magic constant") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/LockMetricsTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/LockMetricsTests.swift new file mode 100644 index 00000000..b9ba3c5a --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/LockMetricsTests.swift @@ -0,0 +1,76 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the env-gated lock-acquisition metrics (PTK_LOCK_METRICS): used to +// validate empirically which SyncBox locks sit on the per-dispatch hot path and +// whether they ever contend. The metric must be off by default and count both +// total and contended acquisitions when on. + +import Testing +import Foundation +@testable import FuzzCore +@testable import PropertyTestingKit + +@Suite("Lock metrics") +struct LockMetricsTests { + + @Test("when enabled, every acquisition is counted (single thread → no contention)") + func acquisitionsCountedWhenEnabled() { + let label = "test.acq.\(UUID().uuidString)" + let box = PropertyTestingKit.SyncBox(0, label: label, forceMetrics: true) + for _ in 0..<37 { box.update { $0 += 1 } } + + let m = LockMetrics.snapshotForTesting(label) + #expect(m?.acquisitions == 37) + #expect(m?.contended == 0) + } + + @Test("a contended acquisition (lock already held) is counted as contended") + func contentionIsCounted() { + let label = "test.contend.\(UUID().uuidString)" + let box = PropertyTestingKit.SyncBox(0, label: label, forceMetrics: true) + + let holderHasLock = DispatchSemaphore(value: 0) + let contenderDone = DispatchSemaphore(value: 0) + + let holder = Thread { + box.update { _ in + holderHasLock.signal() + Thread.sleep(forTimeInterval: 0.2) // hold the lock so the contender's try() fails + } + } + holder.start() + holderHasLock.wait() + + let contender = Thread { + box.update { _ in } // try() fails while holder sleeps → contended++ + contenderDone.signal() + } + contender.start() + contenderDone.wait() + + let m = LockMetrics.snapshotForTesting(label) + #expect(m?.acquisitions == 2) + #expect(m?.contended == 1) + } + + @Test("without forcing, a label opened while disabled records nothing") + func disabledByDefaultRecordsNothing() { + let label = "test.disabled.\(UUID().uuidString)" + let box = PropertyTestingKit.SyncBox(0, label: label) // forceMetrics defaults false; env unset in tests + for _ in 0..<10 { box.update { $0 += 1 } } + + #expect(LockMetrics.snapshotForTesting(label) == nil) + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/MutatorTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/MutatorTests.swift index b21a8284..6268fe9f 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/MutatorTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/MutatorTests.swift @@ -382,7 +382,7 @@ struct MutatorFuzzEngineTests { maxIterations: 2, using: mutator, persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { input in await testedInputs.update { $0.append(input) } } @@ -407,7 +407,7 @@ struct MutatorFuzzEngineTests { maxIterations: 3, using: mutator, persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { input in await testedInputs.update { $0.append(input) } } @@ -438,7 +438,7 @@ struct MutatorPublicAPITests { maxIterations: 50, using: mutator, persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (input: String) in await testedInputs.update { $0.append(input) } } @@ -457,7 +457,7 @@ struct MutatorPublicAPITests { maxIterations: 50, using: emptyStringMutator, persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (input: String) in await testedInputs.update { $0.append(input) } } @@ -492,7 +492,7 @@ struct MutatorPublicAPITests { _ = try await fuzzWithMaxIterations( maxIterations: 50, using: stringMutator, intMutator, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (str: String, num: Int) in await testedInputs.update { $0.append((str, num)) } } @@ -527,7 +527,7 @@ struct MutatorPublicAPITests { _ = try await fuzzWithMaxIterations( maxIterations: 50, using: emptyStringMutator, intBoundaryMutator, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (str: String, num: Int) in await testedInputs.update { $0.append((str, num)) } } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/OwnershipEvaluatorTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/OwnershipEvaluatorTests.swift new file mode 100644 index 00000000..c4b1a3a7 --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/OwnershipEvaluatorTests.swift @@ -0,0 +1,76 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Evaluators own the ownership *criterion* and the metric state; they emit the +// features a run wins as opaque `Feature`s for the generic `OwnershipLedger`. +// The edge evaluator owns by smallest input (REDUCE); the boundary evaluator +// owns by lowest comparison distance, breaking ties toward the smaller input. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("Edge ownership evaluator") +struct EdgeOwnershipEvaluatorTests { + + @Test("an unowned edge is claimed, in the edge domain") + func unownedClaimed() { + var e = EdgeOwnershipEvaluator() + let claims = e.claims(edges: [10, 20], size: 5) + #expect(claims == [Feature(domain: FeatureDomain.edge, id: 10), + Feature(domain: FeatureDomain.edge, id: 20)]) + } + + @Test("a strictly smaller input steals an owned edge; equal or larger does not") + func reduceSteal() { + var e = EdgeOwnershipEvaluator() + _ = e.claims(edges: [10], size: 5) // entry A owns 10 at size 5 + #expect(e.claims(edges: [10], size: 5).isEmpty) // tie: no steal + #expect(e.claims(edges: [10], size: 6).isEmpty) // larger: no steal + #expect(e.claims(edges: [10], size: 4) // smaller: steal + == [Feature(domain: FeatureDomain.edge, id: 10)]) + // The best is now 4; a 5 can no longer steal. + #expect(e.claims(edges: [10], size: 5).isEmpty) + } +} + +@Suite("Boundary distance evaluator") +struct BoundaryDistanceEvaluatorTests { + + @Test("an unseen comparison site is claimed, in the comparison domain") + func unseenClaimed() { + var e = BoundaryDistanceEvaluator() + let claims = e.claims(distances: [0xAB: 100], size: 5) + #expect(claims == [Feature(domain: FeatureDomain.comparison, id: 0xAB)]) + } + + @Test("a strictly closer distance steals the site; a farther one does not") + func closerSteals() { + var e = BoundaryDistanceEvaluator() + _ = e.claims(distances: [0xAB: 100], size: 5) // owns pc at distance 100 + #expect(e.claims(distances: [0xAB: 150], size: 5).isEmpty) // farther: no + #expect(e.claims(distances: [0xAB: 80], size: 5) // closer: yes + == [Feature(domain: FeatureDomain.comparison, id: 0xAB)]) + } + + @Test("on equal distance the smaller input steals; equal or larger does not") + func tieBreakBySize() { + var e = BoundaryDistanceEvaluator() + _ = e.claims(distances: [0xAB: 80], size: 5) // owns pc at distance 80, size 5 + #expect(e.claims(distances: [0xAB: 80], size: 5).isEmpty) // same distance, same size + #expect(e.claims(distances: [0xAB: 80], size: 6).isEmpty) // same distance, larger + #expect(e.claims(distances: [0xAB: 80], size: 4) // same distance, smaller: steal + == [Feature(domain: FeatureDomain.comparison, id: 0xAB)]) + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/OwnershipLedgerTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/OwnershipLedgerTests.swift new file mode 100644 index 00000000..3ba7465e --- /dev/null +++ b/Tests/PropertyTestingKitTests/Fuzzing/OwnershipLedgerTests.swift @@ -0,0 +1,75 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The generic, metric-agnostic ownership ledger: it records which entry owns +// which feature, reassigns features the caller (an evaluator) decided this run +// won, and evicts an entry the moment it owns nothing. It knows NOTHING about +// why a feature was claimed — no size, no distance. The ownership criterion +// lives in the evaluators; these tests feed pre-decided claims and pin only the +// roster/eviction mechanics. +// + +import Testing +@testable import PropertyTestingKit + +@Suite("Ownership ledger") +struct OwnershipLedgerTests { + private func f(_ domain: UInt8, _ id: UInt64) -> Feature { Feature(domain: domain, id: id) } + + @Test("claiming unowned features admits the entry and reports the claim count") + func claimsUnowned() { + var ledger = OwnershipLedger() + let v = ledger.record(claimed: [f(0, 1), f(0, 2)]) + #expect(v.admit) + #expect(v.claimed == 2) + #expect(v.evict.isEmpty) + } + + @Test("an empty claim set is rejected and creates no entry") + func emptyClaimRejected() { + var ledger = OwnershipLedger() + let first = ledger.record(claimed: []) + #expect(!first.admit) + #expect(first.entryID == nil) + // The rejected run consumed no ID: the next admission is still entry 0. + #expect(ledger.record(claimed: [f(0, 1)]).entryID == 0) + } + + @Test("entry IDs are assigned sequentially across admissions") + func sequentialIDs() { + var ledger = OwnershipLedger() + #expect(ledger.record(claimed: [f(0, 1)]).entryID == 0) + #expect(ledger.record(claimed: [f(0, 2)]).entryID == 1) + #expect(ledger.record(claimed: [f(0, 3)]).entryID == 2) + } + + @Test("reassigning a feature evicts the prior owner only when it loses its last feature") + func evictsOnLastFeatureLoss() { + var ledger = OwnershipLedger() + _ = ledger.record(claimed: [f(0, 1), f(0, 2)]) // entry 0 owns {1,2} + let keepsOne = ledger.record(claimed: [f(0, 1)]) // entry 1 takes 1; entry 0 keeps 2 + #expect(keepsOne.evict.isEmpty) + let takesLast = ledger.record(claimed: [f(0, 2)]) // entry 2 takes 2; entry 0 bankrupt + #expect(takesLast.evict == [0]) + } + + @Test("features in different domains never collide") + func domainsDoNotCollide() { + var ledger = OwnershipLedger() + _ = ledger.record(claimed: [f(0, 7)]) // edge-domain feature 7 + let v = ledger.record(claimed: [f(1, 7)]) // boundary-domain feature 7 + #expect(v.admit) + #expect(v.evict.isEmpty, "claiming boundary 7 must not steal the edge-domain 7") + } +} diff --git a/Tests/PropertyTestingKitTests/Fuzzing/PathTrieStrategyTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/PathTrieStrategyTests.swift index f689e7fb..dcbfcf9c 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/PathTrieStrategyTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/PathTrieStrategyTests.swift @@ -31,7 +31,6 @@ struct PathTrieStrategyTests { // lifetime pinning needed even though edges dispatch until the end. defer { SanCovCounters.endMeasurement(context) } let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() // Call setup BEFORE recording edges — this attaches the trie strategy.setup?(context) @@ -45,14 +44,9 @@ struct PathTrieStrategyTests { sancov_dispatch_edge(&g2) // Evaluate the strategy - let firstSparse = strategy.evaluate(context, coverageClient) - if let s = firstSparse { - corpus.mergeCoverageAndAdd(input: 42, scheduleBytes: nil, sparse: s.sparse) - } - let didAdd = firstSparse != nil + let didAdd = strategy.evaluate(context, coverageClient) != nil #expect(didAdd, "First iteration should be interesting") - #expect(corpus.entries.count == 1, "Should have one corpus entry") // Second iteration with the SAME edges should be a duplicate. // If the trie recorded the path on iteration 1, this is not novel. diff --git a/Tests/PropertyTestingKitTests/Fuzzing/PoolCapacityTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/PoolCapacityTests.swift index 9266160c..c07c968f 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/PoolCapacityTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/PoolCapacityTests.swift @@ -61,7 +61,7 @@ struct PoolCapacityTests { @Test("The lowest-weight resident is the capacity victim") func lowestWeightEvicted() { let weigher = ScriptedPolicy { event in - if case .inserted(1, _, _) = event { + if case .inserted(1, _, _, _, _) = event { return [.setWeight(id: 0, 5.0), .setWeight(id: 1, 0.1)] } return [] diff --git a/Tests/PropertyTestingKitTests/Fuzzing/PoollessSchedulerTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/PoollessSchedulerTests.swift index afa24c43..666bb46e 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/PoollessSchedulerTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/PoollessSchedulerTests.swift @@ -24,7 +24,7 @@ import Testing @testable import FuzzCore /// A scheduler with no working set: every `next()` generates fresh, `observe` -/// retains nothing, and it requires no probes. +/// retains nothing, its `snapshot` is empty, and it requires no probes. private struct GenerativeOnlyFactory: SchedulerFactory { func makeScheduler( mutators: repeat Mutator @@ -39,7 +39,36 @@ private struct GenerativeOnlyFactory: SchedulerFactory { poolParentID: nil ) }, - observe: { _, _, _ in nil } + observe: { _, _, _ in }, + snapshot: { [] } + ) + } +} + +/// A scheduler whose `observe` retains nothing per iteration, but whose +/// `snapshot` vends exactly one (freshly generated) input. It proves the engine +/// builds the corpus from `snapshot()` at run-end — NOT from `observe` as the run +/// proceeds: the corpus must hold exactly the one snapshot entry regardless of +/// how many iterations ran. +private struct SnapshotOnlyFactory: SchedulerFactory { + func makeScheduler( + mutators: repeat Mutator + ) -> AnyScheduler { + let mutators = (repeat each mutators) + return AnyScheduler( + requiredProbes: [], + next: { + var rng = FastRNG() + return ScheduledInput( + input: generateInput(rng: &rng, mutators: repeat each mutators), + poolParentID: nil + ) + }, + observe: { _, _, _ in }, + snapshot: { + var rng = FastRNG() + return [generateInput(rng: &rng, mutators: repeat each mutators)] + } ) } } @@ -47,15 +76,35 @@ private struct GenerativeOnlyFactory: SchedulerFactory { @Suite("Pool-less scheduler") struct PoollessSchedulerTests { + /// Stop a run once the bus has observed `count` iterations — deterministic + /// and load-independent, unlike a wall-clock budget (a busy CI core under + /// the full parallel suite can run zero iterations in 0.3s if probe setup + /// alone exhausts the budget, which is what made this test flaky). + private func stopAfter(_ count: Int) -> FuzzPlugin { + let seen = SyncBox(0) + return FuzzPlugin(id: "iteration_counter", handleSync: { event in + switch event { + case .iteration: + seen.update { $0 += 1 } + return seen.value >= count + ? [.stop(.init(reason: .custom("observed_enough")))] + : [] + } + }) + } + @Test("A scheduler with no pool drives the engine end-to-end") func poollessSchedulerRuns() async throws { // `fuzz` accepts any `SchedulerFactory` directly — a userspace scheduler - // needs no `MutationScheduler` wrapper and no pool. + // needs no `MutationScheduler` wrapper and no pool. A generous wall-clock + // ceiling plus a count-based stop makes "it ran inputs" deterministic + // regardless of how loaded the machine is. let result = try await fuzz( - duration: .seconds(0.3), + duration: .seconds(60), persistence: .ephemeral, scheduler: GenerativeOnlyFactory(), - parallelism: 1 + parallelism: 1, + plugins: { [self.stopAfter(50)] } ) { (input: Int) in blackHole(input) } @@ -64,4 +113,22 @@ struct PoollessSchedulerTests { #expect(result.stats.totalInputs > 0) #expect(result.corpus.count == 0) } + + @Test("The engine builds the corpus from the scheduler's run-end snapshot") + func corpusComesFromSnapshot() async throws { + let result = try await fuzz( + duration: .seconds(60), + persistence: .ephemeral, + scheduler: SnapshotOnlyFactory(), + parallelism: 1, + plugins: { [self.stopAfter(50)] } + ) { (input: Int) in + blackHole(input) + } + + // Many iterations ran, but `observe` retained nothing — the corpus holds + // exactly the one input `snapshot()` vended at run-end. + #expect(result.stats.totalInputs > 0) + #expect(result.corpus.count == 1) + } } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/ShrinkingPluginTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/ShrinkingPluginTests.swift index e135ed51..8a881bb5 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/ShrinkingPluginTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/ShrinkingPluginTests.swift @@ -76,20 +76,19 @@ struct ShrinkingHandlerTests { let actions = try await handler.handleAsync(AsyncPluginEvent<[Int]>.failureFound(failureContext)) - // Should return 3 actions: selectForMutation, submitToCorpus, recordIssue - #expect(actions.count == 3) + // Should return 2 actions: selectForMutation, recordIssue. The minimized + // failing input is no longer submitted to the corpus (failure retention + // for regression is tracked as a separate feature). + #expect(actions.count == 2) // Verify action types var hasSelectForMutation = false - var hasSubmitToCorpus = false var hasRecordIssue = false for action in actions { switch action { case .selectForMutation: hasSelectForMutation = true - case .submitToCorpus: - hasSubmitToCorpus = true case .recordIssue: hasRecordIssue = true default: @@ -98,7 +97,6 @@ struct ShrinkingHandlerTests { } #expect(hasSelectForMutation) - #expect(hasSubmitToCorpus) #expect(hasRecordIssue) } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/StrategyFeatureTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/StrategyFeatureTests.swift index 9d2c03f5..5072dd26 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/StrategyFeatureTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/StrategyFeatureTests.swift @@ -216,7 +216,7 @@ struct StrategyFeatureTests { final class CapturePolicy: PoolPlugin { var insertedFeatures: [[UInt64]] = [] func handle(event: PoolEvent) -> [PoolAction] { - if case let .inserted(_, _, features) = event { + if case let .inserted(_, _, features, _, _) = event { insertedFeatures.append(features) } return [] diff --git a/Tests/PropertyTestingKitTests/Fuzzing/TrieEdgeHookTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/TrieEdgeHookTests.swift index 212123fb..a630170c 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/TrieEdgeHookTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/TrieEdgeHookTests.swift @@ -245,7 +245,6 @@ struct TrieEdgeHookTests { // The dispatched edges must have advanced the engine's trie: a first // sight of this path judges unique. let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() #expect(evaluator.evaluate(context, coverageClient) != nil) // Also verify coverage map was written diff --git a/Tests/PropertyTestingKitTests/Fuzzing/WeightedPoolCoreTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/WeightedPoolCoreTests.swift index c8a839b9..2cb438fc 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/WeightedPoolCoreTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/WeightedPoolCoreTests.swift @@ -92,6 +92,23 @@ struct WeightedPoolCoreTests { } } + @Test("The default weightedPool admission culls: a same-feature, non-smaller redundant input is rejected") + func defaultAdmissionCulls() { + // The library default is feature ownership (REDUCE), not everyDiscovery: + // an unbounded pool of every accepted input bloats with large entries + // whose features a smaller input already owns. Build the core straight + // from the public default so this pins the default itself. + // `.featureOwnership` is `MutationScheduler.weightedPool(admission:)`'s + // documented default, so this pins the default admission's behavior. + let core = WeightedPoolHarness.core(admission: .featureOwnership) + // First input owns edges {1,2} (size 2) — admitted as id 0. + #expect(WeightedPoolHarness.accept(core, edges: [1, 2]) == 0) + // Second input: SAME features, SAME size — owns nothing new, steals + // nothing (ties don't steal), so it is rejected (nil). Under the old + // everyDiscovery default it would have been admitted as id 1. + #expect(WeightedPoolHarness.accept(core, edges: [1, 2]) == nil) + } + @Test("Admitted entries get sequential stable IDs") func sequentialIDs() { let core = makeCore() @@ -103,13 +120,13 @@ struct WeightedPoolCoreTests { @Test("Children hear inserted events and their remove actions empty the pool") func childRemoveOnInsert() { let child = ScriptedPolicy { event in - if case let .inserted(id, _, _) = event { return [.remove(id: id)] } + if case let .inserted(id, _, _, _, _) = event { return [.remove(id: id)] } return [] } let core = makeCore(policies: [child], generationRatio: 0) #expect(accept(core, edges: [1, 2]) == 0) - #expect(child.events.contains { if case .inserted(0, _, _) = $0 { return true }; return false }) + #expect(child.events.contains { if case .inserted(0, _, _, _, _) = $0 { return true }; return false }) // The child evicted the only entry: the pool is empty, so generate. #expect(core.decide() == .generate) } @@ -117,7 +134,7 @@ struct WeightedPoolCoreTests { @Test("Children hear removed notifications for other policies' evictions") func childHearsRemovals() { let remover = ScriptedPolicy { event in - if case .inserted(1, _, _) = event { return [.remove(id: 0)] } + if case .inserted(1, _, _, _, _) = event { return [.remove(id: 0)] } return [] } let listener = ScriptedPolicy() @@ -131,7 +148,7 @@ struct WeightedPoolCoreTests { @Test("Zero-weighted entries are never drawn") func zeroWeightNeverDrawn() { let child = ScriptedPolicy { event in - if case .inserted(0, _, _) = event { return [.setWeight(id: 0, 0.0)] } + if case .inserted(0, _, _, _, _) = event { return [.setWeight(id: 0, 0.0)] } return [] } // Ratio 0: every decision with a non-empty pool is a weighted draw, so diff --git a/Tests/PropertyTestingKitTests/PropertyBasedSelfTests.swift b/Tests/PropertyTestingKitTests/PropertyBasedSelfTests.swift index 4d60c4d0..0ce36c07 100644 --- a/Tests/PropertyTestingKitTests/PropertyBasedSelfTests.swift +++ b/Tests/PropertyTestingKitTests/PropertyBasedSelfTests.swift @@ -166,55 +166,23 @@ struct MutatorProvidingPropertyTests { @Suite("Corpus Properties") struct CorpusPropertyTests { - @Test("Corpus addIfInteresting uses signature-based uniqueness") - func testAddIfInterestingSignatureBasedUniqueness() throws { - var corpus = Corpus() - var signatureHashes = Set() - - let sparse1 = SparseCoverage(indices: [0, 1, 2]) - - // First add should succeed - let added1 = corpus.addIfInteresting(input: ("first"), sparse: sparse1, signatureHashes: &signatureHashes) - #expect(added1 == true, "First entry should be added") - - // Different signature (subset) IS interesting - represents a different code path - let sparseSubset = SparseCoverage(indices: [0, 1]) - let added2 = corpus.addIfInteresting(input: ("subset"), sparse: sparseSubset, signatureHashes: &signatureHashes) - #expect(added2 == true, "Different signature should be accepted (unique code path)") - - // Same signature as first should be rejected - let sparseSame = SparseCoverage(indices: [0, 1, 2]) - let added3 = corpus.addIfInteresting(input: ("same"), sparse: sparseSame, signatureHashes: &signatureHashes) - #expect(added3 == false, "Identical signature should be rejected") - - // Different signature with new edges should be accepted - let sparseNew = SparseCoverage(indices: [3]) - let added4 = corpus.addIfInteresting(input: ("new"), sparse: sparseNew, signatureHashes: &signatureHashes) - #expect(added4 == true, "New signature should be accepted") - } - - @Test("Corpus isEmpty property") - func testCorpusIsEmpty() throws { - var corpus = Corpus() - var isEmpty = corpus.isEmpty - #expect(isEmpty, "New corpus should be empty") - - corpus.add(input: ("a"), sparse: SparseCoverage(indices: [0])) - isEmpty = corpus.isEmpty - #expect(!isEmpty, "Corpus with entry should not be empty") - } - - @Test("Corpus inputs property") - func testCorpusInputs() throws { - var corpus = Corpus() - - corpus.add(input: ("hello"), sparse: SparseCoverage(indices: [0])) - corpus.add(input: ("world"), sparse: SparseCoverage(indices: [1])) - - let inputs = corpus.inputs - #expect(inputs.count == 2, "Should have 2 inputs") - #expect(inputs[0] == "hello", "First input should match") - #expect(inputs[1] == "world", "Second input should match") + @Test("Cross-engine merge deduplicates by input identity") + func testMergeDeduplicatesByInput() throws { + // Two parallel engines retained overlapping inputs; coverage is no longer + // a corpus concern, so the merge keeps one copy per distinct input. + let engineA = CorpusSnapshot(entries: [ + CorpusEntry(input: "first"), + CorpusEntry(input: "second"), + ]) + let engineB = CorpusSnapshot(entries: [ + CorpusEntry(input: "second"), // identical input ⇒ dropped + CorpusEntry(input: "third"), + ]) + + let merged = mergeCorpusSnapshots([engineA, engineB]) + + #expect(merged.entries.map(\.input).sorted() == ["first", "second", "third"], + "each distinct input survives exactly once") } } @@ -226,12 +194,7 @@ struct CorpusEntryPropertyTests { @Test("CorpusEntry preserves input through Codable") func testCorpusEntryCodable() async throws { - let entry = CorpusEntry( - input: "test input", - sparseCoverage: SparseCoverage(indices: [0, 5]), - entryType: .coverage, - failure: nil - ) + let entry = CorpusEntry(input: "test input") let encoder = JSONEncoder.corpusEncoder() let decoder = JSONDecoder.corpusDecoder() @@ -240,9 +203,7 @@ struct CorpusEntryPropertyTests { let decoded = try decoder.decode(CorpusEntry.self, from: data) #expect(decoded.input == entry.input) - #expect(decoded.sparseCoverage == SparseCoverage(), "Coverage is not persisted") - #expect(decoded.entryType == .coverage, "Defaults to .coverage on decode") - #expect(decoded.failure == nil) + #expect(decoded.scheduleBytes == nil, "Schedule bytes are not persisted") } } @@ -280,26 +241,6 @@ struct FuzzErrorTests { @Suite("Edge Cases") struct EdgeCaseTests { - @Test("Corpus with complex input types") - func testCorpusComplexTypes() throws { - var corpus = Corpus<[String]>() - - corpus.add( - input: (["a", "b", "c"]), - sparse: SparseCoverage(indices: [0]) - ) - corpus.add( - input: ([]), - sparse: SparseCoverage(indices: [1]) - ) - corpus.add( - input: (["single"]), - sparse: SparseCoverage(indices: [2]) - ) - - let count = corpus.count - #expect(count == 3) - } } @@ -325,7 +266,7 @@ struct FuzzAPIPropertyTests { try await fuzzWithMaxIterations( maxIterations: 50, persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (input: Int) in _ = input > 0 ? "positive" : "non-positive" } @@ -345,7 +286,7 @@ struct FuzzAPIPropertyTests { maxIterations: 50, seeds: ["custom1", "custom2", "custom3"], persistence: .ephemeral, - coverageStrategy: .alwaysInteresting + scheduler: MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting) ) { (input: String) in // Just exercise the input - no actor involvement _ = input.count diff --git a/Tests/PropertyTestingKitTests/Support/DeterministicRNG.swift b/Tests/PropertyTestingKitTests/Support/DeterministicRNG.swift new file mode 100644 index 00000000..6be53e5e --- /dev/null +++ b/Tests/PropertyTestingKitTests/Support/DeterministicRNG.swift @@ -0,0 +1,39 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A deterministic mock `RandomNumberGenerator` for tests. The production +// `FastRNG` is backed by per-thread XorShift state and cannot be seeded, so +// tests that assert on the *distribution* of weighted pool draws ride on +// non-deterministic state and flake on near-ties. Injected via the +// `\.poolDrawRNG` dependency to make those draws reproducible. +// + +/// SplitMix64 — a fully deterministic `RandomNumberGenerator` whose output is +/// determined entirely by its seed. Well-distributed, so weighted sampling over +/// it still exercises the real draw distribution. +struct DeterministicRNG: RandomNumberGenerator, Sendable { + private var state: UInt64 + + init(seed: UInt64) { + self.state = seed + } + + mutating func next() -> UInt64 { + state &+= 0x9E37_79B9_7F4A_7C15 + var z = state + z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 + z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB + return z ^ (z >> 31) + } +} diff --git a/Tests/PropertyTestingKitTests/TestHelpers.swift b/Tests/PropertyTestingKitTests/TestHelpers.swift index 7fef2d8f..3ae706df 100644 --- a/Tests/PropertyTestingKitTests/TestHelpers.swift +++ b/Tests/PropertyTestingKitTests/TestHelpers.swift @@ -41,7 +41,7 @@ func fuzzWithMaxIterations( seeds: [(repeat each Input)] = [], duration: Duration = .seconds(60), persistence: CorpusPersistence = .auto, - coverageStrategy: CoverageStrategy = .signatureMatch, + scheduler: any SchedulerFactory = MutationScheduler.weightedPool(coverageStrategy: .signatureMatch), parallelism: Int = 1, filePath: StaticString = #filePath, function: StaticString = #function, @@ -65,7 +65,7 @@ func fuzzWithMaxIterations( seeds: seeds, duration: .seconds(10), persistence: persistence, - coverageStrategy: coverageStrategy, + scheduler: scheduler, parallelism: parallelism, filePath: filePath, function: function, @@ -97,7 +97,7 @@ func fuzzWithMaxIterations( func fuzzEngineWithMaxIterations( maxIterations: Int, config: FuzzEngineConfig? = nil, - coverageStrategy: CoverageStrategy = .signatureMatch, + scheduler: any SchedulerFactory = MutationScheduler.weightedPool(coverageStrategy: .signatureMatch), additionalSeeds: [(repeat each Input)] = [], test: @escaping @Sendable ((repeat each Input)) async throws -> Void ) async -> FuzzResult { @@ -129,7 +129,7 @@ func fuzzEngineWithMaxIterations maxIterations: Int, corpusDir: URL, persistence: CorpusPersistence, - coverageStrategy: CoverageStrategy = .alwaysInteresting, + scheduler: any SchedulerFactory = MutationScheduler.weightedPool(coverageStrategy: .alwaysInteresting), parallelism: Int = 1, makeHandlers: @escaping @Sendable () -> [FuzzPlugin] = { [] }, additionalSeeds: [(repeat each Input)] = [], @@ -192,8 +192,7 @@ func runFuzzWithMaxIterations parallelism: parallelism, duration: .seconds(10), verbose: false, - coverageStrategy: coverageStrategy, - scheduler: MutationScheduler.weightedPool(), + scheduler: scheduler, projectPath: nil, sourceFileID: "PropertyTestingKitTests/TestHelpers.swift", sourceFilePath: "PropertyTestingKitTests/TestHelpers.swift", @@ -266,7 +265,7 @@ func fuzzWithMaxIterations( using mutators: repeat Mutator, seeds: [(repeat each Input)] = [], persistence: CorpusPersistence = .auto, - coverageStrategy: CoverageStrategy = .signatureMatch, + scheduler: any SchedulerFactory = MutationScheduler.weightedPool(coverageStrategy: .signatureMatch), parallelism: Int = 1, filePath: StaticString = #filePath, function: StaticString = #function, @@ -291,7 +290,7 @@ func fuzzWithMaxIterations( seeds: seeds, duration: .seconds(10), persistence: persistence, - coverageStrategy: coverageStrategy, + scheduler: scheduler, parallelism: parallelism, filePath: filePath, function: function, diff --git a/Tests/SanCovTests/PCResolutionTest.swift b/Tests/SanCovTests/PCResolutionTest.swift index 0adfb74d..27d8df37 100644 --- a/Tests/SanCovTests/PCResolutionTest.swift +++ b/Tests/SanCovTests/PCResolutionTest.swift @@ -6,57 +6,13 @@ import SanCovHooks @Suite("PC Resolution") struct PCResolutionTest { - @Test("Edge filter catches bare async resume/yield patterns (TQ, TY suffixes)") - func filterCatchesAsyncResumeYield() { - // TQ = async resume, TY = async yield. These are compiler-generated - // continuation points that vary between runs. - let asyncPatterns = [ - "$s20SomeModule10someFunc1yyYaKFTQ3_", // bare TQ (resume point 3) - "$s20SomeModule10someFunc1yyYaKFTY4_", // bare TY (yield point 4) - "$s20SomeModule10someFunc1yyYaKFTQ0_", // TQ0_ (resume point 0) - "$s20SomeModule10someFunc1yyYaKFTY1_", // TY1_ (yield point 1) - "$s20SomeModule10closureYbcfU_TQ0_", // closure TQ - "$s20SomeModule10closureYbcfU_TY1_", // closure TY - ] - - let nonAsyncPatterns = [ - "$s20SomeModule10someFunc1yyF", // regular function - "$s20SomeModule10SomeStructV5countSivg", // property getter - "$s20SomeModule10SomeStructV5countSivs", // property setter - ] - - for sym in asyncPatterns { - let result = sym.withCString { sancov_is_compiler_generated($0) } - #expect(result, "Should filter async pattern: \(sym)") - } - - for sym in nonAsyncPatterns { - let result = sym.withCString { sancov_is_compiler_generated($0) } - #expect(!result, "Should NOT filter: \(sym)") - } - } - - @Test("Edge filter catches global variable addressors (vau suffix)") - func filterCatchesGlobalAddressors() { - let addressorPatterns = [ - "$s20SomeModule8lane1OpsSayAA8PollerOpOGvau", // global let addressor - "$s20SomeModule13scheduleBytesS5UInt8VGvau", // static let addressor - ] - - let nonAddressorPatterns = [ - "$s20SomeModule8lane1OpsSayAA8PollerOpOGvg", // getter (not addressor) - ] - - for sym in addressorPatterns { - let result = sym.withCString { sancov_is_compiler_generated($0) } - #expect(result, "Should filter addressor: \(sym)") - } - - for sym in nonAddressorPatterns { - let result = sym.withCString { sancov_is_compiler_generated($0) } - #expect(!result, "Should NOT filter: \(sym)") - } - } + // Compiler-generated-edge classification (async resume/yield TQ/TY, vau + // addressors, outlined ops, thunks) moved to the TagCompilerGenerated LLVM + // pass plugin, which tags those functions NoSanitizeCoverage at compile time. + // Its correctness is exercised end-to-end by the determinism tests + // (CoverageDeterminismTest): if async edges weren't filtered, pathTrie + // determinism would break. The former runtime classifier + // (sancov_is_compiler_generated) and its unit tests have been removed. @Test("All guard indices have resolvable PCs") func allGuardsHavePCs() { diff --git a/Tests/SanCovTests/SanCovCmpRecorderGateTests.swift b/Tests/SanCovTests/SanCovCmpRecorderGateTests.swift new file mode 100644 index 00000000..91bb7a5c --- /dev/null +++ b/Tests/SanCovTests/SanCovCmpRecorderGateTests.swift @@ -0,0 +1,59 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the process-global cmp-recorder count that gates sancov_dispatch_cmp: +// when no measurement context has a comparison recorder attached, the cmp hook +// must early-return BEFORE the per-thread TLS fetch (Finding 42 follow-up — edge- +// only strategies were paying ~33M unconsumed cmp-dispatch TLS fetches / 6s). +// +// The count is process-global, but nothing else in this test target attaches a +// cmp recorder (the production Swift observer layer isn't running here), so the +// lifecycle assertions are deterministic. Serialized for belt-and-suspenders. + +import Testing +import SanCovHooks + +@Suite("SanCov cmp-recorder gate", .serialized) +struct SanCovCmpRecorderGateTests { + + @Test("global cmp-recorder count tracks attach, re-attach, clear, and end_measurement") + func countTracksLifecycle() { + guard let ctx = sancov_begin_measurement() else { + Issue.record("failed to begin measurement") + return + } + let rec: @convention(c) (UInt, UInt64, UInt64, UInt32, UnsafeMutablePointer?) -> Void = { _, _, _, _, _ in } + + // begin_measurement attaches no cmp recorder. + #expect(sancov_cmp_recorder_count_for_testing() == 0) + + sancov_context_set_cmp_recorder(ctx, rec, nil, nil, nil) + #expect(sancov_cmp_recorder_count_for_testing() == 1) + + // Re-attaching to the same context must not double-count. + sancov_context_set_cmp_recorder(ctx, rec, nil, nil, nil) + #expect(sancov_cmp_recorder_count_for_testing() == 1) + + // Explicit clear (recorder == nil) drops the count. + sancov_context_set_cmp_recorder(ctx, nil, nil, nil, nil) + #expect(sancov_cmp_recorder_count_for_testing() == 0) + + // Re-attach, then end_measurement must also release the count (the sever + // path, not just the explicit clear). + sancov_context_set_cmp_recorder(ctx, rec, nil, nil, nil) + #expect(sancov_cmp_recorder_count_for_testing() == 1) + sancov_end_measurement(ctx) + #expect(sancov_cmp_recorder_count_for_testing() == 0) + } +} diff --git a/Tests/SanCovTests/SanCovEdgeFilterTests.swift b/Tests/SanCovTests/SanCovEdgeFilterTests.swift deleted file mode 100644 index 4c0340db..00000000 --- a/Tests/SanCovTests/SanCovEdgeFilterTests.swift +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2026 DoorDash, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Tests for sancov_apply_edge_filter() which disables compiler-generated -// edges (outlined destroyers, lazy witness table accessors, etc.). -// - -import Testing -import SanCovHooks -import Foundation - -@Suite("SanCov Edge Filter") -struct SanCovEdgeFilterTests { - - @Test("applyEdgeFilter marks compiler-generated edges") - func filterMarksCompilerEdges() { - // Precondition: guards and PCs must be available - guard sancov_counters_available() else { - Issue.record("Coverage counters not available — binary not compiled with -sanitize-coverage=edge") - return - } - guard sancov_pcs_available() else { - Issue.record("PC table not available — binary not compiled with -sanitize-coverage=pc-table") - return - } - - let totalEdges = sancov_get_counter_count() - #expect(totalEdges > 0, "Should have instrumented edges") - - // Apply the filter - sancov_apply_edge_filter() - - let filteredCount = sancov_get_filtered_count() - - // In a Swift binary compiled with -sanitize-coverage=edge, there should be - // at least some compiler-generated edges (outlined destroyers, lazy accessors). - // If this fails, the test binary may not contain any Swift standard library code. - #expect(filteredCount > 0, "Expected at least some compiler-generated edges to be filtered, got 0 out of \(totalEdges)") - - // Verify the ratio is reasonable — typically 30-65% of edges are compiler-generated - // (metadata accessors, async resume/yield points, outlined ops, global addressors). - let ratio = Double(filteredCount) / Double(totalEdges) - #expect(ratio < 0.75, "Filtered \(filteredCount)/\(totalEdges) (\(Int(ratio * 100))%) — more than 75% seems wrong") - } - - @Test("filtered edges are not recorded in coverage") - func filteredEdgesNotRecorded() { - guard sancov_counters_available() else { - Issue.record("Coverage counters not available") - return - } - guard sancov_pcs_available() else { - Issue.record("PC table not available") - return - } - - // Apply filter first - sancov_apply_edge_filter() - let filteredCount = sancov_get_filtered_count() - guard filteredCount > 0 else { - // Nothing was filtered, can't test this - return - } - - // Begin a measurement context - guard let context = sancov_begin_measurement() else { - Issue.record("Failed to begin measurement") - return - } - defer { sancov_end_measurement(context) } - - // Exercise some code that will trigger coverage - exerciseCode() - - // Get the covered indices - let coveredCount = sancov_get_covered_count_with_context(context) - guard coveredCount > 0 else { - // No coverage at all — can't verify - return - } - - var outCount: Int = 0 - guard let indices = sancov_get_covered_indices(context, &outCount) else { - return - } - - // Verify none of the covered edges have the SANCOV_GUARD_SKIP sentinel - // We can't read the guard values directly from Swift, but we know that - // any edge that was filtered would have guard = UINT32_MAX, which means - // it can't pass the `*guard < g_guard_count` check, so it should never - // appear in the covered indices. - let totalEdges = sancov_get_counter_count() - for i in 0.. 3 } - _ = array.reduce(0, +) - - // String operations - let strings = ["hello", "world", "test"] - _ = strings.joined(separator: ", ") - - // Dictionary operations - var dict: [String: Int] = [:] - dict["a"] = 1 - dict["b"] = 2 - _ = dict.count -} diff --git a/Tests/SanCovTests/SanCovSuppressionTests.swift b/Tests/SanCovTests/SanCovSuppressionTests.swift new file mode 100644 index 00000000..587d0bab --- /dev/null +++ b/Tests/SanCovTests/SanCovSuppressionTests.swift @@ -0,0 +1,76 @@ +// Copyright 2026 DoorDash, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for the per-thread generation guard (sancov_set_dispatch_suppressed): +// the fuzz loop sets it around input generation/mutation so instrumented SUT +// code run by the generator is not dispatched/recorded (Finding 41p). + +import Testing +import SanCovHooks +import Foundation + +@Suite("SanCov Dispatch Suppression") +struct SanCovSuppressionTests { + + @Test("the suppression flag round-trips on the calling thread") + func flagRoundTrips() { + #expect(sancov_dispatch_is_suppressed() == false) + sancov_set_dispatch_suppressed(true) + #expect(sancov_dispatch_is_suppressed() == true) + sancov_set_dispatch_suppressed(false) + #expect(sancov_dispatch_is_suppressed() == false) + } + + @Test("edges fired while suppressed are not recorded; unsuppressed are") + func suppressedRecordsNothing() { + guard sancov_counters_available(), sancov_pcs_available() else { return } + guard let ctx = sancov_begin_measurement() else { + Issue.record("Failed to begin measurement") + return + } + defer { + sancov_set_dispatch_suppressed(false) // never leak the flag + sancov_end_measurement(ctx) + } + + // Suppressed: instrumented work records nothing. Enable suppression + // FIRST, then reset — so the count reflects only the suppressed exercise, + // not the test's own edges fired between begin_measurement and here. + sancov_set_dispatch_suppressed(true) + sancov_reset_coverage(ctx) + exerciseInstrumentedCode() + let suppressed = sancov_get_covered_count_with_context(ctx) + + // Unsuppressed: the same work records coverage. + sancov_set_dispatch_suppressed(false) + sancov_reset_coverage(ctx) + exerciseInstrumentedCode() + let unsuppressed = sancov_get_covered_count_with_context(ctx) + + #expect(suppressed == 0, "suppressed dispatch should record nothing, got \(suppressed)") + #expect(unsuppressed > 0, "unsuppressed dispatch should record edges, got \(unsuppressed)") + } +} + +@inline(never) +private func exerciseInstrumentedCode() { + var acc = 0 + var array = [3, 1, 4, 1, 5, 9, 2, 6] + array.append(5) + for v in array where v > 2 { + acc &+= v * 2 + } + _ = array.sorted() + _ = acc +} diff --git a/Tests/ScheduleControlTests/CoverageDeterminismTest.swift b/Tests/ScheduleControlTests/CoverageDeterminismTest.swift index 3955a7ee..91d88ab2 100644 --- a/Tests/ScheduleControlTests/CoverageDeterminismTest.swift +++ b/Tests/ScheduleControlTests/CoverageDeterminismTest.swift @@ -194,7 +194,6 @@ private func measureDeterminism( let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(ctx) let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() var uniqueCount = 0 @@ -230,9 +229,8 @@ struct DeterminismIsolationTest { @Test("GenericTimerPoller coverage is deterministic under schedule control (1000 runs)", .timeLimit(.minutes(2))) func pollerDeterminism1000() async throws { - // Apply edge filter (same as production fuzz API) - SanCovCounters.applyEdgeFilter() - + // Compiler-generated edges (incl. async TQ/TY) are filtered at compile + // time by the TagCompilerGenerated pass plugin — no runtime call needed. let pollerBody: @Sendable () async -> Void = { await withDependencies { $0.continuousClock = ImmediateClock() @@ -408,7 +406,6 @@ struct PathTrieReuseTest { let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(ctx) let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() // Run 1: first path should be unique Self.stableCode() @@ -442,9 +439,8 @@ struct PathTrieReuseTest { } } - // Apply edge filter to remove TQ/TY/TA/Wl noise - SanCovCounters.applyEdgeFilter() - + // TQ/TY/TA/Wl noise is filtered at compile time by the + // TagCompilerGenerated pass plugin — no runtime filter call needed. // Warmup using the SAME closure try await ScheduleController.run(scheduleBytes: bytes) { await body() @@ -454,7 +450,6 @@ struct PathTrieReuseTest { let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(ctx) let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() // Run 3 times in a loop using the same closure + same call site var results: [Bool] = [] diff --git a/Tests/ScheduleControlTests/InterleavingContrastTest.swift b/Tests/ScheduleControlTests/InterleavingContrastTest.swift index 5c4cbee2..ea01effa 100644 --- a/Tests/ScheduleControlTests/InterleavingContrastTest.swift +++ b/Tests/ScheduleControlTests/InterleavingContrastTest.swift @@ -112,7 +112,8 @@ struct InterleavingContrastTest { // may be mid-`ScheduleController.run`, so the check races. This test's // validity does not depend on it: with no SessionTag/TLS set, this // task's enqueues pass through `original` regardless of installation. - SanCovCounters.applyEdgeFilter() + // (Compiler-generated edges are filtered at compile time by the + // TagCompilerGenerated pass plugin — no runtime filter call needed.) // The PRODUCTION .pathTrie engine: setup attaches its trie observer, // evaluate judges the run's path (and resets the trie for the next). @@ -121,7 +122,6 @@ struct InterleavingContrastTest { let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(ctx) let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() var unique = 0 let iters = 500 @@ -150,8 +150,6 @@ struct InterleavingContrastTest { @Test("CONTROLLED: schedule bytes pin the ordering to 1 unique path", .timeLimit(.minutes(1))) func controlledHasOnePath() async throws { - SanCovCounters.applyEdgeFilter() - try await ScheduleController.run(scheduleBytes: Self.scheduleBytes) { await Self.body() } @@ -161,7 +159,6 @@ struct InterleavingContrastTest { let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(ctx) let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() var unique = 0 let iters = 200 diff --git a/Tests/ScheduleControlTests/ScheduleCoverageTest.swift b/Tests/ScheduleControlTests/ScheduleCoverageTest.swift index cd386ff5..c5fa1283 100644 --- a/Tests/ScheduleControlTests/ScheduleCoverageTest.swift +++ b/Tests/ScheduleControlTests/ScheduleCoverageTest.swift @@ -148,7 +148,6 @@ struct ScheduleCoverageTest { let evaluator: CoverageEvaluator = CoverageStrategy.pathTrie.makeEvaluator() evaluator.setup?(ctx) let coverageClient = CoverageCountersClient.liveValue - let corpus = Corpus() // Two runs over DIFFERENT branches. If g_target_context routes edges // into the context, both paths advance the trie and both judge diff --git a/Tests/ScheduleControlTests/ScheduleDeterminismTest.swift b/Tests/ScheduleControlTests/ScheduleDeterminismTest.swift index a377c8a9..ac119cc4 100644 --- a/Tests/ScheduleControlTests/ScheduleDeterminismTest.swift +++ b/Tests/ScheduleControlTests/ScheduleDeterminismTest.swift @@ -126,10 +126,7 @@ struct ScheduleDeterminismTest { let entry = CorpusEntry<[UInt8], Int>( input: bytes, 0, - scheduleBytes: bytes, - sparseCoverage: SparseCoverage(indices: []), - entryType: .coverage, - failure: nil + scheduleBytes: bytes ) let snapshot = CorpusSnapshot<[UInt8], Int>(entries: [entry]) try persistence.save(snapshot, to: dir) diff --git a/Tests/TSanTests/RaceConditionTests.swift b/Tests/TSanTests/RaceConditionTests.swift index e2380b4d..192853c6 100644 --- a/Tests/TSanTests/RaceConditionTests.swift +++ b/Tests/TSanTests/RaceConditionTests.swift @@ -148,9 +148,10 @@ struct HighContentionTests { @Test("Sequential corpus and coverage operations", .timeLimit(.minutes(1))) func sequentialCorpusAndCoverage() async { - // Note: Corpus is not thread-safe. This test verifies the API works correctly - // in a sequential context. - var corpus = Corpus() + // Exercises repeated coverage measurement alongside building a corpus + // snapshot in a sequential context (the engine materializes the corpus + // from retained inputs the same way at run-end). + var entries: [CorpusEntry] = [] for i in 0..<30 { for j in 0..<10 { @@ -168,13 +169,13 @@ struct HighContentionTests { sparse = makeSparse(indices: [i, j]) } SanCovCounters.endMeasurement(context) + _ = sparse // measurement still exercised under TSan; the corpus no longer stores it - // Add to corpus - corpus.add(input: (i * 100 + j), sparse: sparse) + entries.append(CorpusEntry(input: i * 100 + j)) } } - // Verify corpus has entries - _ = corpus.snapshot() + // Materialize the snapshot, as the engine does at run-end. + _ = CorpusSnapshot(entries: entries) } } diff --git a/scripts/aggregate-time-profile.py b/scripts/aggregate-time-profile.py new file mode 100755 index 00000000..fe146731 --- /dev/null +++ b/scripts/aggregate-time-profile.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +Aggregate an Instruments Time Profiler trace into CPU-weighted self/total time +per symbol — fully headless (no GUI "Deep Copy" step that parse-call-tree.py +needs). + +Pipeline: + DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer \ + xcrun xctrace export --input X.trace \ + --xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]' > tp.xml + ./scripts/aggregate-time-profile.py tp.xml --top 30 [--grep PATTERN] + +Each is one sample carrying a (ns) and a whose frames +are listed innermost-first. Self time is attributed to the leaf (first) frame; +total time to every distinct symbol appearing in the stack. + +Instruments dedups THREE levels by ref=, and ALL must be resolved or attribution +silently vanishes: (id+name, then ref), (id+ns, then ref), AND + (id + child frames, then ref). The backtrace dedup is the big one — +a hot loop samples the SAME stack millions of times, so the vast majority of rows +are `` with no inline frames. Miss it and those rows count +toward grand_total but attribute to nothing → a phantom "unsymbolicated" majority. +We resolve all three id->value maps while streaming (handles the 10s-of-MB export +without loading it all). +""" +import sys +import argparse +import xml.etree.ElementTree as ET +from collections import defaultdict + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("xml") + ap.add_argument("--top", type=int, default=30) + ap.add_argument("--grep", default=None, help="only show symbols matching this substring") + ap.add_argument("--total", action="store_true", help="sort by total (inclusive) time") + ap.add_argument("--under", default=None, + help="only count samples whose stack contains a frame matching this " + "substring (isolates one subtree's internal self-time breakdown)") + ap.add_argument("--stacks", type=int, default=0, + help="instead of per-symbol, report the N heaviest full call stacks " + "(samples keyed by their entire leaf→root backtrace)") + ap.add_argument("--depth", type=int, default=12, + help="frames of each stack to print in --stacks mode (leaf first)") + ap.add_argument("--fromroot", action="store_true", + help="in --stacks mode, key/print from the OUTERMOST (root-side) frames " + "instead of the leaf — shows the top-level branches of the call tree") + args = ap.parse_args() + + frame_name = {} # frame id -> symbol name + weight_by_id = {} # weight id -> ns (Instruments dedups repeats by ref=) + backtrace_frames = {} # backtrace id -> ordered [leaf..root] symbols + self_ns = defaultdict(int) # leaf symbol -> ns + total_ns = defaultdict(int) # symbol -> ns (counted once per sample) + grand_total = 0 + + stack_ns = defaultdict(int) # full-path key -> ns + stack_path = {} # full-path key -> ordered [leaf..root] symbols + + cur_weight = 0 + in_row = False + # The frame list for the backtrace currently being parsed. Frames append here + # (leaf→root, as emitted); resolved to stack_order at . + cur_bt_order = None + in_backtrace = False + # The resolved stack for the current row (set at ). + stack_order = None + + # Stream: clear elements as we go to bound memory. + for event, el in ET.iterparse(args.xml, events=("start", "end")): + tag = el.tag + if event == "start": + if tag == "row": + in_row = True + cur_weight = 0 + stack_order = [] + elif tag == "backtrace": + in_backtrace = True + cur_bt_order = [] + continue + # end events + if tag == "weight": + if in_row: + wid = el.get("id") + ref = el.get("ref") + if wid is not None and el.text: + cur_weight = int(el.text) + weight_by_id[wid] = cur_weight + elif ref is not None: + cur_weight = weight_by_id.get(ref, 0) + elif tag == "frame": + # Resolve name: defined (id+name) or referenced (ref). + fid = el.get("id") + name = el.get("name") + ref = el.get("ref") + if fid is not None and name is not None: + frame_name[fid] = name + sym = name + elif ref is not None: + sym = frame_name.get(ref) + else: + sym = name + if in_backtrace and sym is not None: + cur_bt_order.append(sym) + elif tag == "backtrace": + # Resolve the row's stack: a backtrace is either DEFINED (id + inline + # frames) or a back-REFERENCE (ref=) to one defined earlier. The hot + # loop makes the vast majority refs, so this is where most weight is. + bid = el.get("id") + ref = el.get("ref") + if bid is not None: + backtrace_frames[bid] = cur_bt_order + stack_order = cur_bt_order + elif ref is not None: + stack_order = backtrace_frames.get(ref, []) + else: + stack_order = cur_bt_order + in_backtrace = False + cur_bt_order = None + elif tag == "row": + stack_order = stack_order or [] + stack_syms = set(stack_order) + leaf = stack_order[0] if stack_order else None + include = True + if args.under is not None: + under = args.under.lower() + include = any(under in s.lower() for s in (stack_syms or ())) + if include: + if leaf is not None: + self_ns[leaf] += cur_weight + for s in (stack_syms or ()): + total_ns[s] += cur_weight + if args.stacks and stack_order: + if args.fromroot: + # Outermost frames (root-side): the top-level branches of + # the call tree. stack_order is leaf→root, so the root is + # the tail; print root→leaf. + truncated = list(reversed(stack_order))[: args.depth] + else: + # Leaf-side frames only: the deep task/runtime prefix + # varies per sample and would fragment otherwise-identical + # hot paths. `--depth` frames define the tree. + truncated = stack_order[: args.depth] + key = "\x01".join(truncated) + stack_ns[key] += cur_weight + if key not in stack_path: + stack_path[key] = truncated + grand_total += cur_weight + in_row = False + el.clear() + + if grand_total == 0: + print("No samples found. Did the xpath/export succeed?", file=sys.stderr) + sys.exit(1) + + if args.stacks: + rows = sorted(stack_ns.items(), key=lambda kv: kv[1], reverse=True)[: args.stacks] + print(f"Total CPU sampled: {grand_total/1e6:.1f} ms across {len(stack_ns)} distinct stacks\n") + for rank, (key, ns) in enumerate(rows, 1): + path = stack_path[key] + print(f"#{rank} {100*ns/grand_total:6.2f}% {ns/1e6:8.1f} ms (leaf→ {len(path)} frames)") + for sym in path: + print(f" {sym}") + print() + return + + key = total_ns if args.total else self_ns + label = "TOTAL" if args.total else "SELF" + rows = sorted(key.items(), key=lambda kv: kv[1], reverse=True) + if args.grep: + rows = [(s, v) for s, v in rows if args.grep.lower() in s.lower()] + + gt_ms = grand_total / 1e6 + print(f"Total CPU sampled: {gt_ms:.1f} ms across {len(self_ns)} leaf symbols\n") + print(f"{'self%':>7} {'total%':>7} {'self ms':>9} {'total ms':>9} symbol") + print("-" * 90) + for sym, _ in rows[: args.top]: + s = self_ns.get(sym, 0) + t = total_ns.get(sym, 0) + print(f"{100*s/grand_total:6.2f}% {100*t/grand_total:6.2f}% " + f"{s/1e6:9.1f} {t/1e6:9.1f} {sym}") + + +if __name__ == "__main__": + main() diff --git a/scripts/build-llvm-plugins.sh b/scripts/build-llvm-plugins.sh new file mode 100755 index 00000000..13a32904 --- /dev/null +++ b/scripts/build-llvm-plugins.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Build the out-of-tree LLVM pass plugins that provide PTK's coverage +# instrumentation at COMPILE time (replacing the former runtime filters in +# SanCovHooks.c): +# +# EmitCmpTrace.dylib — emits __sanitizer_cov_trace_cmp* ourselves for +# the comparisons we want, dropping trap-guard +# cmps (bounds/overflow/precondition). Build the +# SUT with `-sanitize-coverage=edge,pc-table` +# (NO trace-cmp) and load this plugin. +# TagCompilerGenerated.dylib — tags compiler-generated functions +# NoSanitizeCoverage so SanCov emits no edge/cmp +# for them (and async resume/yield edges stay +# filtered for pathTrie determinism). +# +# Instrumented targets load them via `-Xswiftc -load-pass-plugin=` +# (wired in Package.swift). The plugins link against the patched toolchain's +# LLVM (same one PTK builds with), so they're rebuilt here rather than checked +# in. Output: .build/llvm-plugins/*.dylib. +set -e + +BUILD_ROOT="${BUILD_ROOT:-$HOME/Documents/OpenSourceDev/build/Ninja-RelWithDebInfoAssert}" +LLVM_CONFIG="$BUILD_ROOT/llvm-macosx-arm64/bin/llvm-config" + +cd "$(dirname "$0")/.." +SRC_DIR="LLVMPasses" +OUT_DIR=".build/llvm-plugins" + +if [ ! -x "$LLVM_CONFIG" ]; then + echo "error: llvm-config not found at $LLVM_CONFIG" >&2 + echo " set BUILD_ROOT to your patched-toolchain build dir." >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" +SDK="$(xcrun --sdk macosx --show-sdk-path)" +CXXFLAGS="$("$LLVM_CONFIG" --cxxflags)" + +build_one() { + local name="$1" + local src="$SRC_DIR/$name.cpp" + local out="$OUT_DIR/$name.dylib" + # Rebuild only when the source is newer than the dylib (plugins are tiny). + if [ -f "$out" ] && [ "$out" -nt "$src" ]; then + echo "up to date: $out" + return + fi + echo "building: $out" + # shellcheck disable=SC2086 + xcrun clang++ $CXXFLAGS -isysroot "$SDK" -dynamiclib -undefined dynamic_lookup \ + "$src" -o "$out" +} + +build_one EmitCmpTrace +build_one TagCompilerGenerated +echo "llvm plugins ready in $OUT_DIR" diff --git a/scripts/record-cmp-profile.sh b/scripts/record-cmp-profile.sh new file mode 100755 index 00000000..46147d07 --- /dev/null +++ b/scripts/record-cmp-profile.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# +# record-cmp-profile.sh [strategy] [cmp_per_input] [fuzz_ms] [time_limit] +# +# Headless CPU profile of the comparison-dispatch hot path (no Instruments GUI). +# Rebuilds ProfiledBenchmark, records a Time Profiler trace via xctrace --attach, +# exports the time-profile table, and aggregates self-time within the +# sancov_dispatch_cmp subtree. Reusable feedback loop for the onCompare rework. +# +# Outputs: traces/.trace, /tmp/-tp.xml, and the aggregated breakdown. +set -e + +NAME="${1:-cmp}" +STRATEGY="${2:-boundarystate}" +CMP="${3:-256}" +FUZZ_MS="${4:-400}" +LIMIT="${5:-25s}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT" + +: "${BUILD_ROOT:=/Users/fnord/Documents/OpenSourceDev/build/Ninja-RelWithDebInfoAssert}" +export BUILD_ROOT +export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode-beta.app/Contents/Developer}" +RT="$BUILD_ROOT/swift-macosx-arm64/lib/swift/macosx" +BIN=".build/debug/ProfiledBenchmark" + +echo "=== build ===" +./scripts/build-local-toolchain.sh build --product ProfiledBenchmark >/tmp/$NAME-build.log 2>&1 \ + || { echo "build failed"; tail -20 /tmp/$NAME-build.log; exit 1; } +dsymutil "$BIN" -o "$BIN.dSYM" 2>/dev/null || true + +mkdir -p traces +rm -rf "traces/$NAME.trace" +echo "=== record ($STRATEGY, cmp=$CMP, fuzz_ms=$FUZZ_MS, limit=$LIMIT) ===" +DYLD_LIBRARY_PATH="$RT" BENCHMARK_DISABLE_JEMALLOC=true \ + PROFILE_STRATEGY="$STRATEGY" CMP_PER_INPUT="$CMP" FUZZ_MS="$FUZZ_MS" \ + "$BIN" --quiet true >/tmp/$NAME-run.log 2>&1 & +P=$! +sleep 1.5 +if ! ps -p $P >/dev/null; then echo "benchmark exited early; see /tmp/$NAME-run.log"; cat /tmp/$NAME-run.log; exit 1; fi +xcrun xctrace record --template "Time Profiler" --output "traces/$NAME.trace" \ + --time-limit "$LIMIT" --attach $P 2>/tmp/$NAME-rec.log +wait $P 2>/dev/null || true + +echo "=== export + aggregate ===" +xcrun xctrace export --input "traces/$NAME.trace" \ + --xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]' >"/tmp/$NAME-tp.xml" 2>/dev/null +echo "--- self-time within sancov_dispatch_cmp subtree ---" +./scripts/aggregate-time-profile.py "/tmp/$NAME-tp.xml" --under sancov_dispatch_cmp --top 20 diff --git a/scripts/tree-from-xml.py b/scripts/tree-from-xml.py new file mode 100755 index 00000000..9dd94b3a --- /dev/null +++ b/scripts/tree-from-xml.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Reconstruct the Instruments "Deep Copy" call tree (Weight / Self Weight / indented +Symbol Names) directly from a headless `xctrace export` time-profile XML — the SAME +data aggregate-time-profile.py consumes. Proves the headless export contains +everything the GUI deep-copy does; the GUI step is never required. + + DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer \ + xcrun xctrace export --input X.trace \ + --xpath '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]' > tp.xml + ./scripts/tree-from-xml.py tp.xml # print the call tree + ./scripts/tree-from-xml.py tp.xml --verify call_trees/tree.txt # diff vs a GUI deep-copy + +Each is one CPU sample: a (ns) and a of frames listed +INNERMOST-FIRST (leaf→root). The call tree is those stacks merged from the root +down: a node's Weight is the inclusive ns of every sample passing through it; its +Self Weight is the ns of samples whose LEAF it is. Children are sorted by Weight +descending — exactly the GUI's default. Instruments dedups // + by ref=; all three id->value maps are resolved while streaming. +""" +import sys +import argparse +import xml.etree.ElementTree as ET + + +class Node: + __slots__ = ("sym", "incl", "self_w", "kids") + + def __init__(self, sym): + self.sym = sym + self.incl = 0 + self.self_w = 0 + self.kids = {} + + +def parse_samples(xml_path): + """Yield (weight_ns, [leaf..root] symbols) per sample, resolving all ref= dedup.""" + frame_name = {} + weight_by_id = {} + backtrace_frames = {} + cur_weight = 0 + in_row = False + in_backtrace = False + cur_bt_order = None + stack_order = None + + for event, el in ET.iterparse(xml_path, events=("start", "end")): + tag = el.tag + if event == "start": + if tag == "row": + in_row = True + cur_weight = 0 + stack_order = [] + elif tag == "backtrace": + in_backtrace = True + cur_bt_order = [] + continue + if tag == "weight" and in_row: + wid, ref = el.get("id"), el.get("ref") + if wid is not None and el.text: + cur_weight = int(el.text) + weight_by_id[wid] = cur_weight + elif ref is not None: + cur_weight = weight_by_id.get(ref, 0) + elif tag == "frame": + fid, name, ref = el.get("id"), el.get("name"), el.get("ref") + if fid is not None and name is not None: + frame_name[fid] = name + sym = name + elif ref is not None: + sym = frame_name.get(ref) + else: + sym = name + if in_backtrace and sym is not None: + cur_bt_order.append(sym) + elif tag == "backtrace": + bid, ref = el.get("id"), el.get("ref") + if bid is not None: + backtrace_frames[bid] = cur_bt_order + stack_order = cur_bt_order + elif ref is not None: + stack_order = backtrace_frames.get(ref, []) + else: + stack_order = cur_bt_order + in_backtrace = False + cur_bt_order = None + elif tag == "row": + yield cur_weight, (stack_order or []) + in_row = False + el.clear() + + +def build_tree(xml_path): + root = Node(None) # virtual root; its children are the sample roots + grand = 0 + for w, leaf_root in parse_samples(xml_path): + if not leaf_root: + continue + grand += w + node = root + # merge from the OUTERMOST frame (root-side) down to the leaf + path = list(reversed(leaf_root)) + for sym in path: + child = node.kids.get(sym) + if child is None: + child = Node(sym) + node.kids[sym] = child + child.incl += w + node = child + node.self_w += w # leaf of this sample + return root, grand + + +# --- Instruments-style formatting ------------------------------------------- + +def fmt_weight(ns): + if ns == 0: + return "0 s" + s = ns / 1e9 + if s >= 60: + return f"{s/60:.2f} min" + if s >= 1: + return f"{s:.2f} s" + ms = ns / 1e6 + if ms >= 1: + return f"{ms:.2f} ms" + us = ns / 1e3 + if us >= 1: + return f"{us:.2f} µs" + return f"{ns} ns" + + +def print_tree(root, grand, out=sys.stdout, max_lines=None): + print("Weight\tSelf Weight\tSymbol Names", file=out) + n = [0] + + def walk(node, depth): + if max_lines is not None and n[0] >= max_lines: + return + pct = 100.0 * node.incl / grand if grand else 0.0 + indent = " " * (depth + 2) + print(f"{fmt_weight(node.incl)} {pct:.1f}%\t{fmt_weight(node.self_w)}\t{indent}{node.sym}", + file=out) + n[0] += 1 + for kid in sorted(node.kids.values(), key=lambda c: c.incl, reverse=True): + walk(kid, depth + 1) + + for kid in sorted(root.kids.values(), key=lambda c: c.incl, reverse=True): + walk(kid, 0) + + +# --- verification against a GUI deep-copy ------------------------------------ + +def parse_units(s): + s = s.strip() + if s == "0 s" or not s: + return 0.0 + num, unit = s.rsplit(" ", 1) + mult = {"min": 60, "s": 1, "ms": 1e-3, "µs": 1e-6, "us": 1e-6, "ns": 1e-9}[unit] + return float(num) * mult + + +def self_by_symbol_from_deepcopy(path): + agg = {} + with open(path) as f: + next(f) # header + for line in f: + cols = line.rstrip("\n").split("\t") + if len(cols) < 3: + continue + self_w = parse_units(cols[1]) + sym = cols[2].strip() + agg[sym] = agg.get(sym, 0.0) + self_w + return agg + + +def self_by_symbol_from_tree(root): + agg = {} + + def walk(node): + if node.sym is not None: + agg[node.sym] = agg.get(node.sym, 0.0) + node.self_w / 1e9 + for k in node.kids.values(): + walk(k) + + walk(root) + return agg + + +def verify(root, grand, deepcopy_path): + gui = self_by_symbol_from_deepcopy(deepcopy_path) + mine = self_by_symbol_from_tree(root) + gui_total = sum(gui.values()) + mine_total = sum(mine.values()) + print(f"GUI deep-copy total self : {gui_total:8.2f}s across {len(gui)} symbols") + print(f"XML-reconstructed self : {mine_total:8.2f}s across {len(mine)} symbols") + print(f"grand (inclusive root) : {grand/1e9:8.2f}s\n") + syms = sorted(set(gui) | set(mine), key=lambda s: -max(gui.get(s, 0), mine.get(s, 0))) + print(f"{'GUI(s)':>9} {'XML(s)':>9} {'delta':>8} symbol") + print("-" * 80) + worst = 0.0 + for s in syms[:25]: + g, m = gui.get(s, 0.0), mine.get(s, 0.0) + worst = max(worst, abs(g - m)) + print(f"{g:9.2f} {m:9.2f} {g-m:+8.3f} {s[:60]}") + allworst = max((abs(gui.get(s, 0) - mine.get(s, 0)) for s in set(gui) | set(mine)), default=0) + print(f"\nmax abs self-weight delta over ALL symbols: {allworst:.3f}s") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("xml") + ap.add_argument("--verify", default=None, help="compare to a GUI deep-copy tree.txt") + ap.add_argument("--max-lines", type=int, default=None, help="limit printed tree lines") + args = ap.parse_args() + + root, grand = build_tree(args.xml) + if grand == 0: + print("No samples found. Did the xpath/export succeed?", file=sys.stderr) + sys.exit(1) + + if args.verify: + verify(root, grand, args.verify) + else: + print_tree(root, grand, max_lines=args.max_lines) + + +if __name__ == "__main__": + main()