Skip to content

Commit 62d7c94

Browse files
Change Argument.ID.bytes to a SHA256 hash (#1642)
It's wasteful to send the entire JSON content of an argument as the ID for an argument. Use the SHA256 instead. ### Motivation: The JSON string for each parameterized test argument can be very large; for one sample project, each argument was half a kilobyte, multiplied by ~10,000 cases and it starts to add up. This PR standardizes argument IDs as SHA256 hashes. I considered doing this only when it would be profitable to do so (e.g. if the JSON encoding is less than 256 bits), but this makes collisions more likely since we wouldn't be able to guarantee the distribution of bytes that SHA256 gives us. But if that's not really a concern, it's easy enough to only perform this transformation when the encoded input is >32 bytes. Fixes rdar://118992957 ### Modifications: - Adds a `_TestingUtilities` module and moves SHA256.swift into it - Imports that from both TestingMacros and Testing - This required rebuilding it a 2nd time while building TestingMacros in CMake, which I think is acceptable since it's just one file for now. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 1901502 commit 62d7c94

6 files changed

Lines changed: 95 additions & 55 deletions

File tree

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ add_library(Testing
101101
Support/Graph.swift
102102
Support/JSON.swift
103103
Support/Serializer.swift
104+
Support/SHA256.swift
104105
Support/VersionNumber.swift
105106
Support/Versions.swift
106107
Discovery+Macro.swift

Sources/Testing/Parameterization/CustomTestArgumentEncodable.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ extension Test.Case.Argument.ID {
7171
init?(identifying value: some Sendable, parameter: Test.Parameter) throws {
7272
#if canImport(Foundation)
7373
func customArgumentWrapper(for value: some CustomTestArgumentEncodable) -> some Encodable {
74-
_CustomArgumentWrapper(rawValue: value)
74+
CustomArgumentWrapper(rawValue: value)
7575
}
7676

7777
let encodableValue: (any Encodable)? = if let customEncodable = value as? any CustomTestArgumentEncodable {
@@ -110,13 +110,16 @@ extension Test.Case.Argument.ID {
110110
///
111111
/// - Throws: Any error encountered during encoding.
112112
private static func _encode(_ value: some Encodable, parameter: Test.Parameter) throws -> [UInt8] {
113-
try JSON.withEncoding(of: value, userInfo: [._testParameterUserInfoKey: parameter], Array.init)
113+
/// The encoded representation of an argument is the SHA256 hash of its Codable JSON representation.
114+
try JSON.withEncoding(of: value, userInfo: [._testParameterUserInfoKey: parameter]) { buffer in
115+
SHA256.hash(buffer)
116+
}
114117
}
115118
#endif
116119
}
117120

118121
/// A encodable type which wraps a ``CustomTestArgumentEncodable`` value.
119-
private struct _CustomArgumentWrapper<T>: RawRepresentable, Encodable where T: CustomTestArgumentEncodable {
122+
struct CustomArgumentWrapper<T>: RawRepresentable, Encodable where T: CustomTestArgumentEncodable {
120123
/// The value this instance wraps, which implements custom test argument
121124
/// encoding logic.
122125
var rawValue: T
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../TestingMacros/Support/SHA256.swift

Sources/TestingMacros/Support/SHA256.swift

Lines changed: 46 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -69,62 +69,63 @@ enum SHA256 {
6969

7070
/// Process and compute hash from a block.
7171
private static func _process(_ block: ArraySlice<UInt8>, hash: inout [UInt32]) {
72-
7372
// Compute message schedule.
74-
var W = [UInt32](repeating: 0, count: _konstants.count)
75-
for t in 0..<W.count {
76-
switch t {
77-
case 0...15:
78-
let index = block.startIndex.advanced(by: t * 4)
79-
// Put 4 bytes in each message.
73+
withUnsafeTemporaryAllocation(of: UInt32.self, capacity: _konstants.count) { W in
74+
// Load the 16 message words from the block.
75+
let base = block.startIndex
76+
for t in 0..<16 {
77+
let index = base + t * 4
8078
W[t] = UInt32(block[index + 0]) << 24
8179
W[t] |= UInt32(block[index + 1]) << 16
8280
W[t] |= UInt32(block[index + 2]) << 8
8381
W[t] |= UInt32(block[index + 3])
84-
default:
82+
}
83+
84+
// Extend to 64 words.
85+
for t in 16..<64 {
8586
let σ1 = W[t-2].rotateRight(by: 17) ^ W[t-2].rotateRight(by: 19) ^ (W[t-2] >> 10)
8687
let σ0 = W[t-15].rotateRight(by: 7) ^ W[t-15].rotateRight(by: 18) ^ (W[t-15] >> 3)
8788
W[t] = σ1 &+ W[t-7] &+ σ0 &+ W[t-16]
8889
}
89-
}
9090

91-
var a = hash[0]
92-
var b = hash[1]
93-
var c = hash[2]
94-
var d = hash[3]
95-
var e = hash[4]
96-
var f = hash[5]
97-
var g = hash[6]
98-
var h = hash[7]
99-
100-
// Run the main algorithm.
101-
for t in 0..<_konstants.count {
102-
let Σ1 = e.rotateRight(by: 6) ^ e.rotateRight(by: 11) ^ e.rotateRight(by: 25)
103-
let ch = (e & f) ^ (~e & g)
104-
let t1 = h &+ Σ1 &+ ch &+ _konstants[t] &+ W[t]
105-
106-
let Σ0 = a.rotateRight(by: 2) ^ a.rotateRight(by: 13) ^ a.rotateRight(by: 22)
107-
let maj = (a & b) ^ (a & c) ^ (b & c)
108-
let t2 = Σ0 &+ maj
109-
110-
h = g
111-
g = f
112-
f = e
113-
e = d &+ t1
114-
d = c
115-
c = b
116-
b = a
117-
a = t1 &+ t2
118-
}
91+
var a = hash[0]
92+
var b = hash[1]
93+
var c = hash[2]
94+
var d = hash[3]
95+
var e = hash[4]
96+
var f = hash[5]
97+
var g = hash[6]
98+
var h = hash[7]
99+
100+
// Run the main algorithm.
101+
for t in 0..<_konstants.count {
102+
let Σ1 = e.rotateRight(by: 6) ^ e.rotateRight(by: 11) ^ e.rotateRight(by: 25)
103+
let ch = (e & f) ^ (~e & g)
104+
let t1 = h &+ Σ1 &+ ch &+ _konstants[t] &+ W[t]
105+
106+
let Σ0 = a.rotateRight(by: 2) ^ a.rotateRight(by: 13) ^ a.rotateRight(by: 22)
107+
let maj = (a & b) ^ (a & c) ^ (b & c)
108+
let t2 = Σ0 &+ maj
109+
110+
h = g
111+
g = f
112+
f = e
113+
e = d &+ t1
114+
d = c
115+
c = b
116+
b = a
117+
a = t1 &+ t2
118+
}
119119

120-
hash[0] = a &+ hash[0]
121-
hash[1] = b &+ hash[1]
122-
hash[2] = c &+ hash[2]
123-
hash[3] = d &+ hash[3]
124-
hash[4] = e &+ hash[4]
125-
hash[5] = f &+ hash[5]
126-
hash[6] = g &+ hash[6]
127-
hash[7] = h &+ hash[7]
120+
hash[0] = a &+ hash[0]
121+
hash[1] = b &+ hash[1]
122+
hash[2] = c &+ hash[2]
123+
hash[3] = d &+ hash[3]
124+
hash[4] = e &+ hash[4]
125+
hash[5] = f &+ hash[5]
126+
hash[6] = g &+ hash[6]
127+
hash[7] = h &+ hash[7]
128+
}
128129
}
129130

130131
/// Pad the given byte array to be a multiple of 512 bits.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2014–2026 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
#if canImport(CryptoKit)
12+
13+
import CryptoKit
14+
@testable import Testing
15+
16+
@Suite
17+
struct SHA256Tests {
18+
@Test(arguments: [
19+
[],
20+
withUnsafeBytes(of: UInt64.random(in: 0 ..< .max), Array.init),
21+
Array(0..<20),
22+
Array("Hello, world".utf8),
23+
Array(#"{"key": "value", "key2": 123, "key3": null}"#.utf8),
24+
(0..<1_024).map { _ in .random(in: 0 ..< .max) }
25+
])
26+
func matchesCryptoKit(data: [UInt8]) {
27+
let expected = CryptoKit::SHA256.hash(data: data)
28+
let ours = Testing::SHA256.hash(data)
29+
30+
#expect(expected == ours, "Data \(data) did not hash to the same value")
31+
}
32+
}
33+
34+
#endif

Tests/TestingTests/Test.Case.Argument.IDTests.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ struct Test_Case_Argument_IDTests {
2323
let arguments = try #require(testCase.arguments)
2424
#expect(arguments.count == 1)
2525
let argument = try #require(arguments.first)
26-
#expect(String(decoding: argument.id.bytes, as: UTF8.self) == "123")
26+
#expect(argument.id.bytes == SHA256.hash("123".utf8))
2727
}
2828

2929
@Test("One CustomTestArgumentEncodable parameter")
3030
func oneCustomParameter() async throws {
31+
let argumentValue = MyCustomTestArgument(x: 123, y: "abc")
3132
let test = Test(
32-
arguments: [MyCustomTestArgument(x: 123, y: "abc")],
33+
arguments: [argumentValue],
3334
parameters: [Test.Parameter(index: 0, firstName: "value", type: MyCustomTestArgument.self)]
3435
) { _ in }
3536
let testCases = try #require(test.testCases)
@@ -38,10 +39,9 @@ struct Test_Case_Argument_IDTests {
3839
#expect(arguments.count == 1)
3940
let argument = try #require(arguments.first)
4041
#if canImport(Foundation)
41-
let decodedArgument = try argument.id.bytes.withUnsafeBufferPointer { argumentID in
42-
try JSON.decode(MyCustomTestArgument.self, from: .init(argumentID))
42+
try JSON.withEncoding(of: CustomArgumentWrapper(rawValue: argumentValue)) { data in
43+
#expect(argument.id.bytes == SHA256.hash(data))
4344
}
44-
#expect(decodedArgument == MyCustomTestArgument(x: 123, y: "abc"))
4545
#endif
4646
}
4747

@@ -56,7 +56,7 @@ struct Test_Case_Argument_IDTests {
5656
let arguments = try #require(testCase.arguments)
5757
#expect(arguments.count == 1)
5858
let argument = try #require(arguments.first)
59-
#expect(String(decoding: argument.id.bytes, as: UTF8.self) == #""abc""#)
59+
#expect(argument.id.bytes == SHA256.hash(#""abc""#.utf8))
6060
}
6161

6262
@Test("One RawRepresentable parameter")
@@ -70,7 +70,7 @@ struct Test_Case_Argument_IDTests {
7070
let arguments = try #require(testCase.arguments)
7171
#expect(arguments.count == 1)
7272
let argument = try #require(arguments.first)
73-
#expect(String(decoding: argument.id.bytes, as: UTF8.self) == #""abc""#)
73+
#expect(argument.id.bytes == SHA256.hash(#""abc""#.utf8))
7474
}
7575
}
7676

0 commit comments

Comments
 (0)