Skip to content

Commit 70f8b43

Browse files
committed
Add SnapshotFormat option for audio file vs MD5 checksum snapshots
For repositories with many audio snapshot tests, .caf files can bloat the repo. This adds a SnapshotFormat enum (.audio / .checksum) so users can opt into lightweight 32-byte .md5 checksum files instead of full ALAC audio snapshots. Default is .audio, preserving full backward compatibility.
1 parent 363f2be commit 70f8b43

8 files changed

Lines changed: 292 additions & 156 deletions

File tree

Sources/AudioSnapshotTesting/Core/AudioSnapshotTesting.swift

Lines changed: 209 additions & 153 deletions
Large diffs are not rendered by default.

Sources/AudioSnapshotTesting/Core/AudioSnapshotTrait.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,22 @@ public struct AudioSnapshotTrait: TestTrait, SuiteTrait, TestScoping {
2020
/// The bit depth for ALAC encoding. Defaults to 16-bit.
2121
public let bitDepth: AudioBitDepth
2222

23+
/// The format used for storing snapshot artifacts.
24+
public let format: SnapshotFormat
25+
2326
/// Creates a new audio snapshot trait.
2427
/// - Parameters:
2528
/// - record: Whether to record new snapshots. Defaults to `false`.
2629
/// - strategy: The snapshot strategy for failure visualization. Defaults to `nil`.
2730
/// - autoOpen: Whether to automatically open visualizations. Defaults to `false`.
2831
/// - bitDepth: The bit depth for ALAC encoding. Defaults to `.bits16`.
29-
public init(record: Bool = false, strategy: VisualisationStrategy? = nil, autoOpen: Bool = false, bitDepth: AudioBitDepth = .bits16) {
32+
/// - format: The format for storing snapshot artifacts. Defaults to `.audio`.
33+
public init(record: Bool = false, strategy: VisualisationStrategy? = nil, autoOpen: Bool = false, bitDepth: AudioBitDepth = .bits16, format: SnapshotFormat = .audio) {
3034
self.record = record
3135
self.strategy = strategy
3236
self.autoOpen = autoOpen
3337
self.bitDepth = bitDepth
38+
self.format = format
3439
}
3540

3641
/// Called by Swift Testing to set up the test scope.
@@ -48,14 +53,16 @@ extension Trait where Self == AudioSnapshotTrait {
4853
/// - strategy: The snapshot strategy for failure visualization. Defaults to `nil`.
4954
/// - autoOpen: Whether to automatically open visualizations. Defaults to `false`.
5055
/// - bitDepth: The bit depth for ALAC encoding. Defaults to `.bits16`.
56+
/// - format: The format for storing snapshot artifacts. Defaults to `.audio`.
5157
/// - Returns: An `AudioSnapshotTrait` configured with the specified options.
5258
public static func audioSnapshot(
5359
record: Bool = false,
5460
strategy: VisualisationStrategy? = nil,
5561
autoOpen: Bool = false,
56-
bitDepth: AudioBitDepth = .bits16
62+
bitDepth: AudioBitDepth = .bits16,
63+
format: SnapshotFormat = .audio
5764
) -> AudioSnapshotTrait {
58-
AudioSnapshotTrait(record: record, strategy: strategy, autoOpen: autoOpen, bitDepth: bitDepth)
65+
AudioSnapshotTrait(record: record, strategy: strategy, autoOpen: autoOpen, bitDepth: bitDepth, format: format)
5966
}
6067
}
6168

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// The format used for storing audio snapshot artifacts.
2+
public enum SnapshotFormat: Sendable {
3+
/// Full ALAC-encoded .caf audio file (current behavior).
4+
case audio
5+
/// Lightweight MD5 checksum stored in a .md5 text file.
6+
case checksum
7+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import CryptoKit
2+
import Foundation
3+
4+
/// Handles MD5 checksum computation and file I/O for checksum-based snapshots.
5+
enum AudioChecksumWriter {
6+
/// Computes the MD5 checksum of a file at the given URL.
7+
/// - Parameter url: The file URL to hash.
8+
/// - Returns: A 32-character lowercase hex string.
9+
static func computeChecksum(of url: URL) throws -> String {
10+
let data = try Data(contentsOf: url)
11+
let digest = Insecure.MD5.hash(data: data)
12+
return digest.map { String(format: "%02x", $0) }.joined()
13+
}
14+
15+
/// Writes a checksum string to a `.md5` file.
16+
/// - Parameters:
17+
/// - checksum: The 32-character hex checksum string.
18+
/// - url: The destination file URL.
19+
static func writeChecksum(_ checksum: String, to url: URL) throws {
20+
try checksum.write(to: url, atomically: true, encoding: .utf8)
21+
}
22+
23+
/// Reads a checksum string from a `.md5` file.
24+
/// - Parameter url: The file URL to read from.
25+
/// - Returns: The checksum string (trimmed of whitespace).
26+
static func readChecksum(from url: URL) throws -> String {
27+
try String(contentsOf: url, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
28+
}
29+
}

Tests/AudioSnapshotTestingTests/AudioSnapshotTestingTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,40 @@ func multiChannelComparison() async throws {
169169
await assertAudioSnapshot(of: buffer, named: "multiChannelComparison.4ch")
170170
}
171171

172+
@Test(
173+
"Checksum snapshot records and verifies a deterministic buffer",
174+
.audioSnapshot(record: false, format: .checksum)
175+
)
176+
func checksumRoundTrip() async throws {
177+
let signal = synthesizeSignal(
178+
frequencyAmplitudePairs: [(440, 0.5)],
179+
count: 4410
180+
)
181+
let buffer = createBuffer(from: signal)
182+
await assertAudioSnapshot(of: buffer, named: "checksumRoundTrip.440hz")
183+
}
184+
185+
@Test(
186+
"Checksum snapshot with multiple buffers uses indexed naming",
187+
.audioSnapshot(record: false, format: .checksum)
188+
)
189+
func checksumMultiBuffer() async throws {
190+
let signal1 = synthesizeSignal(
191+
frequencyAmplitudePairs: [(440, 0.5)],
192+
count: 4410
193+
)
194+
let signal2 = synthesizeSignal(
195+
frequencyAmplitudePairs: [(880, 0.3)],
196+
count: 4410
197+
)
198+
let buffer1 = createBuffer(from: signal1)
199+
let buffer2 = createBuffer(from: signal2)
200+
await assertAudioSnapshot(
201+
of: [buffer1, buffer2],
202+
named: "checksumMultiBuffer"
203+
)
204+
}
205+
172206
private func createBuffer(from samples: [Float], sampleRate: Double = 32768) -> AVAudioPCMBuffer {
173207
createBuffer(channels: [samples], sampleRate: sampleRate)
174208
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
60e4ed9c7ca9ae8b665aab56b5f09cd4
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
15bcba203f8d88260cd8d606496a45aa
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
60e4ed9c7ca9ae8b665aab56b5f09cd4

0 commit comments

Comments
 (0)