From 76363293eadac9075fc1ba7d27e84103d5e864f3 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 14 Sep 2026 15:42:34 +0000 Subject: [PATCH 1/4] amt: mutants for LibStringSet --- mutants.toml | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 mutants.toml diff --git a/mutants.toml b/mutants.toml new file mode 100644 index 0000000..bc55976 --- /dev/null +++ b/mutants.toml @@ -0,0 +1,80 @@ +[suite] +root = "." +command = ["sh", "check.sh"] +proof = '(\d+) passed; (\d+) failed' +timeout-secs = 900 + +[[mutants]] +name = "M01 holds never returns true on a match" +file = "src/lib/LibStringSet.sol" +target = " return true;" +replacement = " return false;" + +[[mutants]] +name = "M02 holds returns true after a full miss" +file = "src/lib/LibStringSet.sol" +target = " return false;" +replacement = " return true;" + +[[mutants]] +name = "M03 empty haystack reports held" +file = "src/lib/LibStringSet.sol" +target = " return false;" +replacement = " return haystack.length == 0;" + +[[mutants]] +name = "M04 loop skips index 0" +file = "src/lib/LibStringSet.sol" +target = "uint256 i = 0; i < haystack.length" +replacement = "uint256 i = 1; i < haystack.length" + +[[mutants]] +name = "M05 loop skips the last index" +file = "src/lib/LibStringSet.sol" +target = "i < haystack.length; i++" +replacement = "i + 1 < haystack.length; i++" + +[[mutants]] +name = "M06 compares length only" +file = "src/lib/LibStringSet.sol" +target = "keccak256(bytes(haystack[i])) == keccak256(bytes(needle))" +replacement = "bytes(haystack[i]).length == bytes(needle).length" + +[[mutants]] +name = "M07 compares length and first byte only" +file = "src/lib/LibStringSet.sol" +target = "keccak256(bytes(haystack[i])) == keccak256(bytes(needle))" +replacement = "bytes(haystack[i]).length == bytes(needle).length && (bytes(needle).length == 0 || bytes(haystack[i])[0] == bytes(needle)[0])" + +[[mutants]] +name = "M08 empty needle is always held" +file = "src/lib/LibStringSet.sol" +target = " for (uint256 i = 0; i < haystack.length; i++) {" +replacement = " if (bytes(needle).length == 0) return true;\n for (uint256 i = 0; i < haystack.length; i++) {" + +[[mutants]] +name = "M09 equality by memory pointer identity not value" +file = "src/lib/LibStringSet.sol" +target = " if (keccak256(bytes(haystack[i])) == keccak256(bytes(needle))) {" +replacement = " bool same;\n string memory h = haystack[i];\n assembly (\"memory-safe\") {\n same := eq(h, needle)\n }\n if (same) {" + +[[mutants]] +name = "M10 keeps looping and returns the last comparison" +file = "src/lib/LibStringSet.sol" +target = """ for (uint256 i = 0; i < haystack.length; i++) { + if (keccak256(bytes(haystack[i])) == keccak256(bytes(needle))) { + return true; + } + } + return false;""" +replacement = """ bool found; + for (uint256 i = 0; i < haystack.length; i++) { + found = keccak256(bytes(haystack[i])) == keccak256(bytes(needle)); + } + return found;""" + +[[mutants]] +name = "M11 compares only the first 32 bytes of content" +file = "src/lib/LibStringSet.sol" +target = " if (keccak256(bytes(haystack[i])) == keccak256(bytes(needle))) {" +replacement = " bytes32 hw;\n bytes32 nw;\n string memory h = haystack[i];\n assembly (\"memory-safe\") {\n hw := mload(add(h, 0x20))\n nw := mload(add(needle, 0x20))\n }\n if (bytes(h).length == bytes(needle).length && hw == nw) {" From 083969d14d4be96dcf1a38b406fc9ea9c294591d Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 14 Sep 2026 18:08:40 +0000 Subject: [PATCH 2/4] amt: name the failing test in the LibStringSet probe output Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- mutants.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mutants.toml b/mutants.toml index bc55976..0a11adb 100644 --- a/mutants.toml +++ b/mutants.toml @@ -2,6 +2,7 @@ root = "." command = ["sh", "check.sh"] proof = '(\d+) passed; (\d+) failed' +fail-pattern = '\[FAIL[^\]]*\] (\S+)\(' timeout-secs = 900 [[mutants]] From 3bb68fbedbb04601d0672f42734945001ac0deec Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 14 Sep 2026 18:13:46 +0000 Subject: [PATCH 3/4] Test LibStringSet.holds membership directly holds had no test of its own. Its only exercise was indirect, through the snapshot shape and build tests, which never present two distinct strings of the same length to it and never present an empty list or an empty needle. Adds a unit test for membership as the NatSpec defines it: whole content equality by value over a list whose order does not matter, an empty list holding nothing, and the empty string held only when an element is itself empty. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- test/src/lib/LibStringSet.t.sol | 151 ++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 test/src/lib/LibStringSet.t.sol diff --git a/test/src/lib/LibStringSet.t.sol b/test/src/lib/LibStringSet.t.sol new file mode 100644 index 0000000..12a3a4c --- /dev/null +++ b/test/src/lib/LibStringSet.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.2/src/Test.sol"; +import {LibStringSet} from "../../../src/lib/LibStringSet.sol"; + +/// @title LibStringSetTest +/// `holds` is membership by whole-string equality over a list whose order it +/// does not fix. Every case here is a concrete list and needle whose answer is +/// derived from that definition rather than from the loop that implements it. +contract LibStringSetTest is Test { + /// @return A three element list of distinct single character strings. + function three() internal pure returns (string[] memory) { + string[] memory haystack = new string[](3); + haystack[0] = "a"; + haystack[1] = "b"; + haystack[2] = "c"; + return haystack; + } + + /// Position does not matter: the first, a middle and the last element are + /// all held. + function testHoldsFindsAnElementAtEveryIndex() external pure { + string[] memory haystack = three(); + assertTrue(LibStringSet.holds(haystack, "a")); + assertTrue(LibStringSet.holds(haystack, "b")); + assertTrue(LibStringSet.holds(haystack, "c")); + } + + /// A one element list holds exactly that element. + function testHoldsSingleElement() external pure { + string[] memory haystack = new string[](1); + haystack[0] = "only"; + assertTrue(LibStringSet.holds(haystack, "only")); + assertFalse(LibStringSet.holds(haystack, "other")); + } + + /// A string equal to none of the elements is not held. + function testHoldsIsFalseOnAMiss() external pure { + assertFalse(LibStringSet.holds(three(), "d")); + } + + /// An empty list holds nothing, not even the empty string. + function testHoldsIsFalseOnAnEmptyHaystack() external pure { + string[] memory haystack = new string[](0); + assertFalse(LibStringSet.holds(haystack, "a")); + assertFalse(LibStringSet.holds(haystack, "")); + } + + /// Equality is over the whole content: a prefix, an extension, a same + /// length string differing in any byte, and a case change are all + /// different strings. + function testHoldsComparesTheWholeContent() external pure { + string[] memory haystack = new string[](1); + haystack[0] = "ab"; + assertTrue(LibStringSet.holds(haystack, "ab")); + assertFalse(LibStringSet.holds(haystack, "a")); + assertFalse(LibStringSet.holds(haystack, "abc")); + assertFalse(LibStringSet.holds(haystack, "ac")); + assertFalse(LibStringSet.holds(haystack, "cb")); + assertFalse(LibStringSet.holds(haystack, "AB")); + assertFalse(LibStringSet.holds(haystack, "aB")); + } + + /// Content longer than one EVM word is compared in full: two strings that + /// share their first 32 bytes and differ only after them are different. + function testHoldsComparesBeyondTheFirstWord() external pure { + string memory head = "0123456789abcdef0123456789abcdef"; + assertEq(bytes(head).length, 32); + string[] memory haystack = new string[](1); + haystack[0] = string.concat(head, "tail-one"); + assertTrue(LibStringSet.holds(haystack, string.concat(head, "tail-one"))); + assertFalse(LibStringSet.holds(haystack, string.concat(head, "tail-two"))); + assertFalse(LibStringSet.holds(haystack, head)); + } + + /// The empty string is an ordinary member: it is held only when an element + /// is itself empty, and an empty element does not match a non empty needle. + function testHoldsEmptyNeedleMatchesOnlyAnEmptyElement() external pure { + string[] memory haystack = new string[](1); + haystack[0] = "a"; + assertFalse(LibStringSet.holds(haystack, "")); + + string[] memory withEmpty = new string[](2); + withEmpty[0] = "a"; + withEmpty[1] = ""; + assertTrue(LibStringSet.holds(withEmpty, "")); + assertFalse(LibStringSet.holds(withEmpty, "b")); + } + + /// Equality is by value, not by which memory the string sits in: a needle + /// built separately from the element it equals is held, and a needle equal + /// to no element is not, however it was built. + function testHoldsIsByValueNotIdentity() external pure { + string[] memory haystack = new string[](2); + haystack[0] = string.concat("du", "p"); + haystack[1] = string.concat("d", "up"); + assertTrue(LibStringSet.holds(haystack, string.concat("dup", ""))); + assertFalse(LibStringSet.holds(haystack, string.concat("du", ""))); + } + + /// A match is the answer whatever follows it, so an element that is held + /// is held no matter how many misses sit after it in the list. + function testHoldsAMatchIsNotUndoneByLaterMisses() external pure { + string[] memory haystack = new string[](4); + haystack[0] = "hit"; + haystack[1] = "miss-one"; + haystack[2] = "miss-two"; + haystack[3] = "miss-three"; + assertTrue(LibStringSet.holds(haystack, "hit")); + assertFalse(LibStringSet.holds(haystack, "miss-four")); + } + + /// Membership is over a set, so the order of the list does not change it. + function testHoldsIsOrderIndependent() external pure { + string[] memory forward = three(); + string[] memory reversed = new string[](3); + reversed[0] = forward[2]; + reversed[1] = forward[1]; + reversed[2] = forward[0]; + for (uint256 i = 0; i < forward.length; i++) { + assertTrue(LibStringSet.holds(reversed, forward[i])); + assertEq(LibStringSet.holds(forward, forward[i]), LibStringSet.holds(reversed, forward[i])); + } + assertFalse(LibStringSet.holds(reversed, "d")); + } + + /// Every element of any list is held by that list. The extra element keeps + /// the list non empty whatever the fuzzer produces. + function testHoldsFuzzMember(string[] memory haystack, string memory extra, uint256 index) external pure { + string[] memory withExtra = new string[](haystack.length + 1); + for (uint256 i = 0; i < haystack.length; i++) { + withExtra[i] = haystack[i]; + } + withExtra[haystack.length] = extra; + assertTrue(LibStringSet.holds(withExtra, withExtra[index % withExtra.length])); + } + + /// A string longer than every element cannot equal any of them, so it is + /// never held. + function testHoldsFuzzLongerThanEveryElementIsMissed(string[] memory haystack) external pure { + uint256 longest = 0; + for (uint256 i = 0; i < haystack.length; i++) { + if (bytes(haystack[i]).length > longest) { + longest = bytes(haystack[i]).length; + } + } + assertFalse(LibStringSet.holds(haystack, string(new bytes(longest + 1)))); + } +} From 0463185a36dfd98760cbb583af5a5dc7f0cf9ec2 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 14 Sep 2026 18:25:04 +0000 Subject: [PATCH 4/4] Keep the mutation probe config out of the tree mutants.toml is harness input for the mutation sweep, not part of the repository, and an unlicensed file fails the REUSE check. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- mutants.toml | 81 ---------------------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 mutants.toml diff --git a/mutants.toml b/mutants.toml deleted file mode 100644 index 0a11adb..0000000 --- a/mutants.toml +++ /dev/null @@ -1,81 +0,0 @@ -[suite] -root = "." -command = ["sh", "check.sh"] -proof = '(\d+) passed; (\d+) failed' -fail-pattern = '\[FAIL[^\]]*\] (\S+)\(' -timeout-secs = 900 - -[[mutants]] -name = "M01 holds never returns true on a match" -file = "src/lib/LibStringSet.sol" -target = " return true;" -replacement = " return false;" - -[[mutants]] -name = "M02 holds returns true after a full miss" -file = "src/lib/LibStringSet.sol" -target = " return false;" -replacement = " return true;" - -[[mutants]] -name = "M03 empty haystack reports held" -file = "src/lib/LibStringSet.sol" -target = " return false;" -replacement = " return haystack.length == 0;" - -[[mutants]] -name = "M04 loop skips index 0" -file = "src/lib/LibStringSet.sol" -target = "uint256 i = 0; i < haystack.length" -replacement = "uint256 i = 1; i < haystack.length" - -[[mutants]] -name = "M05 loop skips the last index" -file = "src/lib/LibStringSet.sol" -target = "i < haystack.length; i++" -replacement = "i + 1 < haystack.length; i++" - -[[mutants]] -name = "M06 compares length only" -file = "src/lib/LibStringSet.sol" -target = "keccak256(bytes(haystack[i])) == keccak256(bytes(needle))" -replacement = "bytes(haystack[i]).length == bytes(needle).length" - -[[mutants]] -name = "M07 compares length and first byte only" -file = "src/lib/LibStringSet.sol" -target = "keccak256(bytes(haystack[i])) == keccak256(bytes(needle))" -replacement = "bytes(haystack[i]).length == bytes(needle).length && (bytes(needle).length == 0 || bytes(haystack[i])[0] == bytes(needle)[0])" - -[[mutants]] -name = "M08 empty needle is always held" -file = "src/lib/LibStringSet.sol" -target = " for (uint256 i = 0; i < haystack.length; i++) {" -replacement = " if (bytes(needle).length == 0) return true;\n for (uint256 i = 0; i < haystack.length; i++) {" - -[[mutants]] -name = "M09 equality by memory pointer identity not value" -file = "src/lib/LibStringSet.sol" -target = " if (keccak256(bytes(haystack[i])) == keccak256(bytes(needle))) {" -replacement = " bool same;\n string memory h = haystack[i];\n assembly (\"memory-safe\") {\n same := eq(h, needle)\n }\n if (same) {" - -[[mutants]] -name = "M10 keeps looping and returns the last comparison" -file = "src/lib/LibStringSet.sol" -target = """ for (uint256 i = 0; i < haystack.length; i++) { - if (keccak256(bytes(haystack[i])) == keccak256(bytes(needle))) { - return true; - } - } - return false;""" -replacement = """ bool found; - for (uint256 i = 0; i < haystack.length; i++) { - found = keccak256(bytes(haystack[i])) == keccak256(bytes(needle)); - } - return found;""" - -[[mutants]] -name = "M11 compares only the first 32 bytes of content" -file = "src/lib/LibStringSet.sol" -target = " if (keccak256(bytes(haystack[i])) == keccak256(bytes(needle))) {" -replacement = " bytes32 hw;\n bytes32 nw;\n string memory h = haystack[i];\n assembly (\"memory-safe\") {\n hw := mload(add(h, 0x20))\n nw := mload(add(needle, 0x20))\n }\n if (bytes(h).length == bytes(needle).length && hw == nw) {"