Skip to content

Commit 34e2a16

Browse files
committed
Factor out exit test ID construction to a separate function, just use a full 128 bits of hash
1 parent 9b7ac6d commit 34e2a16

File tree

4 files changed

+67
-56
lines changed

4 files changed

+67
-56
lines changed

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,10 @@ public struct ExitTest: Sendable, ~Copyable {
4545
/// same version of the testing library that encoded them.
4646
@_spi(ForToolsIntegrationOnly)
4747
public struct ID: Sendable, Equatable, Codable {
48-
/// The underlying UUID (stored as two `UInt64` values to avoid relying on
49-
/// `UUID` from Foundation or any platform-specific interfaces.)
48+
/// Storage for the underlying bits of the ID.
5049
///
5150
/// - Note: On Apple platforms, we deploy to OS versions that do not include
52-
/// support for `UInt128`.
51+
/// support for `UInt128`, so we use two `UInt64`s for storage instead.
5352
private var _lo: UInt64
5453
private var _hi: UInt64
5554

Sources/TestingMacros/ConditionMacro.swift

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -447,17 +447,7 @@ extension ExitTestConditionMacro {
447447
context.diagnose(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro))
448448
}
449449

450-
let exitTestID: (UInt64, UInt64)
451-
if let sourceLocation = context.location(of: macro, at: .afterLeadingTrivia, filePathMode: .fileID),
452-
var fileID = sourceLocation.file.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
453-
let line = sourceLocation.line.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue,
454-
let column = sourceLocation.column.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue {
455-
exitTestID.0 = fileID.withUTF8(make64BitHash)
456-
exitTestID.1 = (UInt64(line) << 32) | UInt64(column)
457-
} else {
458-
fatalError("Could not determine the source location of this exit test. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
459-
}
460-
let exitTestIDExpr: ExprSyntax = "(\(IntegerLiteralExprSyntax(exitTestID.0, radix: .hex)), \(IntegerLiteralExprSyntax(exitTestID.1, radix: .hex)))"
450+
let exitTestIDExpr = _makeExitTestIDExpr(for: macro, in: context)
461451

462452
var decls = [DeclSyntax]()
463453

@@ -550,6 +540,44 @@ extension ExitTestConditionMacro {
550540

551541
return try Base.expansion(of: macro, primaryExpression: bodyArgumentExpr, in: context)
552542
}
543+
544+
/// Make an expression representing an exit test ID that can be passed to the
545+
/// `ExitTest.__store()` function at runtime.
546+
///
547+
/// - Parameters:
548+
/// - macro: The exit test macro being inspected.
549+
/// - context: The macro context in which the expression is being parsed.
550+
///
551+
/// - Returns: An expression representing the exit test's unique ID.
552+
private static func _makeExitTestIDExpr(
553+
for macro: some FreestandingMacroExpansionSyntax,
554+
in context: some MacroExpansionContext
555+
) -> ExprSyntax {
556+
let exitTestID: (UInt64, UInt64)
557+
if let sourceLocation = context.location(of: macro, at: .afterLeadingTrivia, filePathMode: .fileID),
558+
let fileID = sourceLocation.file.as(StringLiteralExprSyntax.self)?.representedLiteralValue,
559+
let line = sourceLocation.line.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue,
560+
let column = sourceLocation.column.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue {
561+
// Hash the entire source location and store as many bits as possible in
562+
// the resulting ID.
563+
let stringValue = "\(fileID):\(line):\(column)"
564+
exitTestID = SHA256.hash(stringValue.utf8).withUnsafeBytes { sha256 in
565+
sha256.loadUnaligned(as: (UInt64, UInt64).self)
566+
}
567+
} else {
568+
// This branch is dead code in production, but is used when we expand a
569+
// macro in our own unit tests because the macro expansion context does
570+
// not have real source location information.
571+
exitTestID.0 = .random(in: 0 ... .max)
572+
exitTestID.1 = .random(in: 0 ... .max)
573+
}
574+
575+
// Return a tuple of integer literals (which is what the runtime __store()
576+
// function is expecting.)
577+
return """
578+
(\(IntegerLiteralExprSyntax(exitTestID.0, radix: .hex)), \(IntegerLiteralExprSyntax(exitTestID.1, radix: .hex)))
579+
"""
580+
}
553581
}
554582

555583
/// A type describing the expansion of the `#expect(exitsWith:)` macro.

Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ extension MacroExpansionContext {
6363
.tokens(viewMode: .fixedUp)
6464
.map(\.textWithoutBackticks)
6565
.joined()
66-
let hashValue = make64BitHash(identifierCharacters.utf8)
66+
let hashValue = SHA256.hash(identifierCharacters.utf8).withUnsafeBytes { sha256 in
67+
sha256.loadUnaligned(as: UInt64.self)
68+
}
6769
let suffix = String(hashValue, radix: 16, uppercase: false)
6870

6971
// If the caller did not specify a prefix and the hash value starts with a

Sources/TestingMacros/Support/SHA256.swift

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,20 @@
1313

1414
/// SHA-256 implementation from Secure Hash Algorithm 2 (SHA-2) set of
1515
/// cryptographic hash functions (FIPS PUB 180-2).
16-
struct SHA256 {
16+
enum SHA256 {
1717
/// The length of the output digest (in bits).
18-
private static let digestLength = 256
18+
private static let _digestLength = 256
1919

2020
/// The size of each blocks (in bits).
21-
private static let blockBitSize = 512
21+
private static let _blockBitSize = 512
2222

2323
/// The initial hash value.
24-
private static let initalHashValue: [UInt32] = [
24+
private static let _initialHashValue: [UInt32] = [
2525
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
2626
]
2727

2828
/// The constants in the algorithm (K).
29-
private static let konstants: [UInt32] = [
29+
private static let _konstants: [UInt32] = [
3030
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
3131
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
3232
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
@@ -37,28 +37,25 @@ struct SHA256 {
3737
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
3838
]
3939

40-
public init() {
41-
}
42-
43-
public func hash(_ bytes: [UInt8]) -> [UInt8] {
44-
var input = bytes
40+
public static func hash(_ bytes: some Sequence<UInt8>) -> [UInt8] {
41+
var input = Array(bytes)
4542

4643
// Pad the input.
47-
pad(&input)
44+
_pad(&input)
4845

4946
// Break the input into N 512-bit blocks.
50-
let messageBlocks = input.blocks(size: Self.blockBitSize / 8)
47+
let messageBlocks = input.blocks(size: _blockBitSize / 8)
5148

5249
/// The hash that is being computed.
53-
var hash = Self.initalHashValue
50+
var hash = _initialHashValue
5451

5552
// Process each block.
5653
for block in messageBlocks {
57-
process(block, hash: &hash)
54+
_process(block, hash: &hash)
5855
}
5956

6057
// Finally, compute the result.
61-
var result = [UInt8](repeating: 0, count: Self.digestLength / 8)
58+
var result = [UInt8](repeating: 0, count: _digestLength / 8)
6259
for (idx, element) in hash.enumerated() {
6360
let pos = idx * 4
6461
result[pos + 0] = UInt8((element >> 24) & 0xff)
@@ -71,10 +68,10 @@ struct SHA256 {
7168
}
7269

7370
/// Process and compute hash from a block.
74-
private func process(_ block: ArraySlice<UInt8>, hash: inout [UInt32]) {
71+
private static func _process(_ block: ArraySlice<UInt8>, hash: inout [UInt32]) {
7572

7673
// Compute message schedule.
77-
var W = [UInt32](repeating: 0, count: Self.konstants.count)
74+
var W = [UInt32](repeating: 0, count: _konstants.count)
7875
for t in 0..<W.count {
7976
switch t {
8077
case 0...15:
@@ -101,10 +98,10 @@ struct SHA256 {
10198
var h = hash[7]
10299

103100
// Run the main algorithm.
104-
for t in 0..<Self.konstants.count {
101+
for t in 0..<_konstants.count {
105102
let Σ1 = e.rotateRight(by: 6) ^ e.rotateRight(by: 11) ^ e.rotateRight(by: 25)
106103
let ch = (e & f) ^ (~e & g)
107-
let t1 = h &+ Σ1 &+ ch &+ Self.konstants[t] &+ W[t]
104+
let t1 = h &+ Σ1 &+ ch &+ _konstants[t] &+ W[t]
108105

109106
let Σ0 = a.rotateRight(by: 2) ^ a.rotateRight(by: 13) ^ a.rotateRight(by: 22)
110107
let maj = (a & b) ^ (a & c) ^ (b & c)
@@ -131,7 +128,7 @@ struct SHA256 {
131128
}
132129

133130
/// Pad the given byte array to be a multiple of 512 bits.
134-
private func pad(_ input: inout [UInt8]) {
131+
private static func _pad(_ input: inout [UInt8]) {
135132
// Find the bit count of input.
136133
let inputBitLength = input.count * 8
137134

@@ -157,24 +154,24 @@ struct SHA256 {
157154

158155
// MARK:- Helpers
159156

160-
private extension UInt64 {
157+
extension UInt64 {
161158
/// Converts the 64 bit integer into an array of single byte integers.
162-
func toByteArray() -> [UInt8] {
159+
fileprivate func toByteArray() -> [UInt8] {
163160
var value = self.littleEndian
164161
return withUnsafeBytes(of: &value, Array.init)
165162
}
166163
}
167164

168-
private extension UInt32 {
165+
extension UInt32 {
169166
/// Rotates self by given amount.
170-
func rotateRight(by amount: UInt32) -> UInt32 {
167+
fileprivate func rotateRight(by amount: UInt32) -> UInt32 {
171168
return (self >> amount) | (self << (32 - amount))
172169
}
173170
}
174171

175-
private extension Array {
172+
extension Array {
176173
/// Breaks the array into the given size.
177-
func blocks(size: Int) -> AnyIterator<ArraySlice<Element>> {
174+
fileprivate func blocks(size: Int) -> AnyIterator<ArraySlice<Element>> {
178175
var currentIndex = startIndex
179176
return AnyIterator {
180177
if let nextIndex = self.index(currentIndex, offsetBy: size, limitedBy: self.endIndex) {
@@ -185,18 +182,3 @@ private extension Array {
185182
}
186183
}
187184
}
188-
189-
// MARK: - 64-bit hash value
190-
191-
/// Compute a 64-bit hash value of a sequence of bytes by computing its SHA-256
192-
/// hash and truncating the result.
193-
///
194-
/// - Parameters:
195-
/// - bytes: The bytes for which a hash value should be computed.
196-
///
197-
/// - Returns: The hash value computed for `bytes`.
198-
func make64BitHash(_ bytes: some Sequence<UInt8>) -> UInt64 {
199-
SHA256().hash(Array(bytes)).withUnsafeBytes { hashBuffer in
200-
hashBuffer.loadUnaligned(as: UInt64.self)
201-
}
202-
}

0 commit comments

Comments
 (0)