diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index bd5cb95b9..a38c7592e 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -35,10 +35,20 @@ private import _TestingInternals #endif public struct ExitTest: Sendable, ~Copyable { /// A type whose instances uniquely identify instances of ``ExitTest``. + /// + /// An instance of this type uniquely identifies an exit test within the + /// context of the current test target. You can get an exit test's unique + /// identifier from its ``id`` property. + /// + /// The encoded form of an instance of this type is subject to change over + /// time. Instances of this type are only guaranteed to be decodable by the + /// same version of the testing library that encoded them. @_spi(ForToolsIntegrationOnly) public struct ID: Sendable, Equatable, Codable { - /// An underlying UUID (stored as two `UInt64` values to avoid relying on - /// `UUID` from Foundation or any platform-specific interfaces.) + /// Storage for the underlying bits of the ID. + /// + /// - Note: On Apple platforms, we deploy to OS versions that do not include + /// support for `UInt128`, so we use two `UInt64`s for storage instead. private var _lo: UInt64 private var _hi: UInt64 diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index b0d809665..72184f94b 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -99,9 +99,9 @@ target_sources(TestingMacros PRIVATE Support/AvailabilityGuards.swift Support/CommentParsing.swift Support/ConditionArgumentParsing.swift - Support/CRC32.swift Support/DiagnosticMessage.swift Support/DiagnosticMessage+Diagnosing.swift + Support/SHA256.swift Support/SourceCodeCapturing.swift Support/SourceLocationGeneration.swift Support/TestContentGeneration.swift diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index f07fad91f..c82acd725 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -8,6 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +import SwiftParser public import SwiftSyntax import SwiftSyntaxBuilder public import SwiftSyntaxMacros @@ -446,9 +447,8 @@ extension ExitTestConditionMacro { context.diagnose(.captureClauseUnsupported(captureClause, in: closureExpr, inExitTest: macro)) } - // TODO: use UUID() here if we can link to Foundation - let exitTestID = (UInt64.random(in: 0 ... .max), UInt64.random(in: 0 ... .max)) - let exitTestIDExpr: ExprSyntax = "(\(literal: exitTestID.0), \(literal: exitTestID.1))" + // Generate a unique identifier for this exit test. + let idExpr = _makeExitTestIDExpr(for: macro, in: context) var decls = [DeclSyntax]() @@ -494,7 +494,7 @@ extension ExitTestConditionMacro { enum \(enumName) { private nonisolated static let accessor: Testing.__TestContentRecordAccessor = { outValue, type, hint, _ in Testing.ExitTest.__store( - \(exitTestIDExpr), + \(idExpr), \(bodyThunkName), into: outValue, asTypeAt: type, @@ -525,10 +525,7 @@ extension ExitTestConditionMacro { // Insert the exit test's ID as the first argument. Note that this will // invalidate all indices into `arguments`! arguments.insert( - Argument( - label: "identifiedBy", - expression: exitTestIDExpr - ), + Argument(label: "identifiedBy", expression: idExpr), at: arguments.startIndex ) @@ -541,6 +538,44 @@ extension ExitTestConditionMacro { return try Base.expansion(of: macro, primaryExpression: bodyArgumentExpr, in: context) } + + /// Make an expression representing an exit test ID that can be passed to the + /// `ExitTest.__store()` function at runtime. + /// + /// - Parameters: + /// - macro: The exit test macro being inspected. + /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: An expression representing the exit test's unique ID. + private static func _makeExitTestIDExpr( + for macro: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) -> ExprSyntax { + let exitTestID: (UInt64, UInt64) + if let sourceLocation = context.location(of: macro, at: .afterLeadingTrivia, filePathMode: .fileID), + let fileID = sourceLocation.file.as(StringLiteralExprSyntax.self)?.representedLiteralValue, + let line = sourceLocation.line.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue, + let column = sourceLocation.column.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue { + // Hash the entire source location and store as many bits as possible in + // the resulting ID. + let stringValue = "\(fileID):\(line):\(column)" + exitTestID = SHA256.hash(stringValue.utf8).withUnsafeBytes { sha256 in + sha256.loadUnaligned(as: (UInt64, UInt64).self) + } + } else { + // This branch is dead code in production, but is used when we expand a + // macro in our own unit tests because the macro expansion context does + // not have real source location information. + exitTestID.0 = .random(in: 0 ... .max) + exitTestID.1 = .random(in: 0 ... .max) + } + + // Return a tuple of integer literals (which is what the runtime __store() + // function is expecting.) + return """ + (\(IntegerLiteralExprSyntax(exitTestID.0, radix: .hex)), \(IntegerLiteralExprSyntax(exitTestID.1, radix: .hex))) + """ + } } /// A type describing the expansion of the `#expect(exitsWith:)` macro. diff --git a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift index 3b31caf72..322a84f3a 100644 --- a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift @@ -63,10 +63,12 @@ extension MacroExpansionContext { .tokens(viewMode: .fixedUp) .map(\.textWithoutBackticks) .joined() - let crcValue = crc32(identifierCharacters.utf8) - let suffix = String(crcValue, radix: 16, uppercase: false) + let hashValue = SHA256.hash(identifierCharacters.utf8).withUnsafeBytes { sha256 in + sha256.loadUnaligned(as: UInt64.self) + } + let suffix = String(hashValue, radix: 16, uppercase: false) - // If the caller did not specify a prefix and the CRC32 value starts with a + // If the caller did not specify a prefix and the hash value starts with a // digit, include a single-character prefix to ensure that Swift's name // demangling still works correctly. var prefix = prefix diff --git a/Sources/TestingMacros/Support/CRC32.swift b/Sources/TestingMacros/Support/CRC32.swift deleted file mode 100644 index e58c4c7f7..000000000 --- a/Sources/TestingMacros/Support/CRC32.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -/// The precomputed CRC-32 lookup table. -/// -/// This table is used by the ``crc32(_:)`` function below. It is borrowed from -/// the [Swift standard library](https://github.com/swiftlang/swift/blob/main/stdlib/public/Backtracing/Elf.swift). -private let _crc32Table: [UInt32] = [ - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, - 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, - 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, - 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, - 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, - 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, - 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, - 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, - 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, - 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, - 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, - 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, - 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, - 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, - 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, - 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, - 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, - 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, - 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, - 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, - 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, - 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d -] - -/// Compute the CRC-32 code for a sequence of bytes. -/// -/// - Parameters: -/// - bytes: The bytes for which a CRC-32 code should be computed. -/// -/// - Returns: The CRC-32 code computed for `bytes`. -/// -/// A starting value of `0` is assumed. This function is adapted from the -/// [Swift standard library](https://github.com/swiftlang/swift/blob/main/stdlib/public/Backtracing/Elf.swift). -func crc32(_ bytes: some Sequence) -> UInt32 { - ~bytes.reduce(~0) { crcValue, byte in - _crc32Table[Int(UInt8(truncatingIfNeeded: crcValue) ^ byte)] ^ (crcValue >> 8) - } -} diff --git a/Sources/TestingMacros/Support/SHA256.swift b/Sources/TestingMacros/Support/SHA256.swift new file mode 100644 index 000000000..a4d88a801 --- /dev/null +++ b/Sources/TestingMacros/Support/SHA256.swift @@ -0,0 +1,184 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014–2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// The contents of this file were copied more-or-less verbatim from +/// [swift-tools-support-core](https://github.com/swiftlang/swift-tools-support-core/blob/add9e1518ac37a8e52b7612d3eb2f009ae8f6ce8/Sources/TSCBasic/HashAlgorithms.swift). + +/// SHA-256 implementation from Secure Hash Algorithm 2 (SHA-2) set of +/// cryptographic hash functions (FIPS PUB 180-2). +enum SHA256 { + /// The length of the output digest (in bits). + private static let _digestLength = 256 + + /// The size of each blocks (in bits). + private static let _blockBitSize = 512 + + /// The initial hash value. + private static let _initialHashValue: [UInt32] = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + ] + + /// The constants in the algorithm (K). + private static let _konstants: [UInt32] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ] + + public static func hash(_ bytes: some Sequence) -> [UInt8] { + var input = Array(bytes) + + // Pad the input. + _pad(&input) + + // Break the input into N 512-bit blocks. + let messageBlocks = input.blocks(size: _blockBitSize / 8) + + /// The hash that is being computed. + var hash = _initialHashValue + + // Process each block. + for block in messageBlocks { + _process(block, hash: &hash) + } + + // Finally, compute the result. + var result = [UInt8](repeating: 0, count: _digestLength / 8) + for (idx, element) in hash.enumerated() { + let pos = idx * 4 + result[pos + 0] = UInt8((element >> 24) & 0xff) + result[pos + 1] = UInt8((element >> 16) & 0xff) + result[pos + 2] = UInt8((element >> 8) & 0xff) + result[pos + 3] = UInt8(element & 0xff) + } + + return result + } + + /// Process and compute hash from a block. + private static func _process(_ block: ArraySlice, hash: inout [UInt32]) { + + // Compute message schedule. + var W = [UInt32](repeating: 0, count: _konstants.count) + for t in 0..> 10) + let σ0 = W[t-15].rotateRight(by: 7) ^ W[t-15].rotateRight(by: 18) ^ (W[t-15] >> 3) + W[t] = σ1 &+ W[t-7] &+ σ0 &+ W[t-16] + } + } + + var a = hash[0] + var b = hash[1] + var c = hash[2] + var d = hash[3] + var e = hash[4] + var f = hash[5] + var g = hash[6] + var h = hash[7] + + // Run the main algorithm. + for t in 0..<_konstants.count { + let Σ1 = e.rotateRight(by: 6) ^ e.rotateRight(by: 11) ^ e.rotateRight(by: 25) + let ch = (e & f) ^ (~e & g) + let t1 = h &+ Σ1 &+ ch &+ _konstants[t] &+ W[t] + + let Σ0 = a.rotateRight(by: 2) ^ a.rotateRight(by: 13) ^ a.rotateRight(by: 22) + let maj = (a & b) ^ (a & c) ^ (b & c) + let t2 = Σ0 &+ maj + + h = g + g = f + f = e + e = d &+ t1 + d = c + c = b + b = a + a = t1 &+ t2 + } + + hash[0] = a &+ hash[0] + hash[1] = b &+ hash[1] + hash[2] = c &+ hash[2] + hash[3] = d &+ hash[3] + hash[4] = e &+ hash[4] + hash[5] = f &+ hash[5] + hash[6] = g &+ hash[6] + hash[7] = h &+ hash[7] + } + + /// Pad the given byte array to be a multiple of 512 bits. + private static func _pad(_ input: inout [UInt8]) { + // Find the bit count of input. + let inputBitLength = input.count * 8 + + // Append the bit 1 at end of input. + input.append(0x80) + + // Find the number of bits we need to append. + // + // inputBitLength + 1 + bitsToAppend ≡ 448 mod 512 + let mod = inputBitLength % 512 + let bitsToAppend = mod < 448 ? 448 - 1 - mod : 512 + 448 - mod - 1 + + // We already appended first 7 bits with 0x80 above. + input += [UInt8](repeating: 0, count: (bitsToAppend - 7) / 8) + + // We need to append 64 bits of input length. + for byte in UInt64(inputBitLength).toByteArray().lazy.reversed() { + input.append(byte) + } + assert((input.count * 8) % 512 == 0, "Expected padded length to be 512.") + } +} + +// MARK:- Helpers + +extension UInt64 { + /// Converts the 64 bit integer into an array of single byte integers. + fileprivate func toByteArray() -> [UInt8] { + var value = self.littleEndian + return withUnsafeBytes(of: &value, Array.init) + } +} + +extension UInt32 { + /// Rotates self by given amount. + fileprivate func rotateRight(by amount: UInt32) -> UInt32 { + return (self >> amount) | (self << (32 - amount)) + } +} + +extension Array { + /// Breaks the array into the given size. + fileprivate func blocks(size: Int) -> AnyIterator> { + var currentIndex = startIndex + return AnyIterator { + if let nextIndex = self.index(currentIndex, offsetBy: size, limitedBy: self.endIndex) { + defer { currentIndex = nextIndex } + return self[currentIndex..