From 6e86c6e915995444cbac1e13c86dc0e2154be1b0 Mon Sep 17 00:00:00 2001 From: Abhishek Enaguthi Date: Wed, 15 Jul 2026 18:17:13 -0700 Subject: [PATCH] Improve Character default mutator neighborhood exploration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mutate previously only jumped among 8 hard-coded seeds. Build a non-identity candidate set with scalar ±1, case flip, class-local adjacency, and generate-pool jumps so coverage-guided search can take small steps and reach beyond the seed set. --- .../Character+MutatorProviding.swift | 44 +++++++++++++++++-- .../Fuzzing/SingleValueMutatorTests.swift | 18 ++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Character+MutatorProviding.swift b/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Character+MutatorProviding.swift index a7323885..016b0698 100644 --- a/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Character+MutatorProviding.swift +++ b/Sources/FuzzCore/Fuzzing/Mutators/MutatorProviding/Character+MutatorProviding.swift @@ -25,9 +25,47 @@ private let _whitespace: [Character] = [" ", "\t", "\n", "\r"] private let _emojis: [Character] = ["😀", "🎉", "🚀", "💡", "⚡", "🔥", "✨", "🌟"] private func _characterMutate(_ value: Character, _ rng: inout FastRNG) -> Character { - // Pick ONE applicable seed (any seed other than the current value). - let candidates = _characterSeeds.filter { $0 != value } - guard let mutant = candidates.randomElement(using: &rng) else { return value } + // Build applicable non-identity candidates, then pick uniformly — same + // pattern as PortMutator / EmptyStringMutator. Neighborhood + class-local + // steps explore near the input; class jumps reuse the generate pools so + // mutation can reach beyond the 8 hard-coded seeds. + var candidates: [Character] = [] + + if let scalar = value.unicodeScalars.first { + let code = scalar.value + if code < 0x10FFFF, let up = Unicode.Scalar(code + 1) { + candidates.append(Character(up)) + } + if code > 0, let down = Unicode.Scalar(code - 1) { + candidates.append(Character(down)) + } + } + + let upperStr = String(value).uppercased() + if upperStr.count == 1, let upper = upperStr.first, upper != value { + candidates.append(upper) + } + let lowerStr = String(value).lowercased() + if lowerStr.count == 1, let lower = lowerStr.first, lower != value { + candidates.append(lower) + } + + for pool in [_lowercaseLetters, _uppercaseLetters, _digits, _whitespace, _asciiPrintable, _emojis] { + if let idx = pool.firstIndex(of: value) { + if idx + 1 < pool.count { candidates.append(pool[idx + 1]) } + if idx > 0 { candidates.append(pool[idx - 1]) } + } else if let jump = pool.randomElement(using: &rng) { + candidates.append(jump) + } + } + + for seed in _characterSeeds where seed != value { + candidates.append(seed) + } + + guard let mutant = candidates.filter({ $0 != value }).randomElement(using: &rng) else { + return value + } return mutant } diff --git a/Tests/PropertyTestingKitTests/Fuzzing/SingleValueMutatorTests.swift b/Tests/PropertyTestingKitTests/Fuzzing/SingleValueMutatorTests.swift index 4517e66b..981f7139 100644 --- a/Tests/PropertyTestingKitTests/Fuzzing/SingleValueMutatorTests.swift +++ b/Tests/PropertyTestingKitTests/Fuzzing/SingleValueMutatorTests.swift @@ -57,6 +57,24 @@ struct SingleValueMutatorTests { #expect(seen.count >= 4) } + @Test("Character default mutator explores beyond the 8 seed characters") + func characterDefaultMutatorReachesBeyondSeeds() { + var rng = FastRNG() + let seeds: Set = ["a", "Z", "0", " ", "\n", "\t", "😄", "\0"] + var seen = Set() + var sawNeighbor = false + for _ in 0..<800 { + let mutant = Character.defaultMutator.mutate("a", &rng) + #expect(mutant != "a") + seen.insert(mutant) + if mutant == "b" || mutant == "A" { + sawNeighbor = true + } + } + #expect(seen.count > seeds.count, "mutate should reach more than the 8 seed characters") + #expect(sawNeighbor, "expected neighborhood exploration near 'a'") + } + @Test("String default mutator returns a changed value and varies across draws") func stringDefaultMutatorVaries() { var rng = FastRNG()