You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
76 lines
3.8 KiB
76 lines
3.8 KiB
import Foundation
|
|
import AVFoundation
|
|
import Testing
|
|
@testable import Music
|
|
|
|
// Locates the test bundle from a struct suite (struct suites don't have a Bundle.self,
|
|
// so we use a final class defined in the same file).
|
|
private final class BundleToken {}
|
|
|
|
// Verifies format routing and that writing tags round-trips through a real file
|
|
// without corrupting audio.
|
|
struct TagWriterTests {
|
|
|
|
// Step 1: Locate a resource file in the test bundle using BundleToken as the anchor.
|
|
private func fixtureURL(_ name: String, _ ext: String) -> URL? {
|
|
Bundle(for: BundleToken.self).url(forResource: name, withExtension: ext)
|
|
}
|
|
|
|
// Step 2: Copy the fixture to a temp path so the test can mutate it without
|
|
// modifying the bundle resource.
|
|
private func tempCopy(of url: URL) throws -> URL {
|
|
let dst = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent(UUID().uuidString + "." + url.pathExtension)
|
|
try FileManager.default.copyItem(at: url, to: dst)
|
|
return dst
|
|
}
|
|
|
|
// Step 3: Read the common "title" key from an audio file using AVFoundation metadata.
|
|
private func readCommonTitle(_ url: URL) async throws -> String? {
|
|
let md = try await AVURLAsset(url: url).load(.metadata)
|
|
let items = AVMetadataItem.metadataItems(from: md, withKey: AVMetadataKey.commonKeyTitle, keySpace: .common)
|
|
return try await items.first?.load(.stringValue)
|
|
}
|
|
|
|
// Verifies that TagWriterFactory routes ".mp3" → ID3TagWriter, ".m4a" → MP4TagWriter,
|
|
// and returns nil for unsupported formats.
|
|
@Test func factoryRoutesByExtension() {
|
|
#expect(TagWriterFactory.writer(for: URL(fileURLWithPath: "/a.mp3")) is ID3TagWriter)
|
|
#expect(TagWriterFactory.writer(for: URL(fileURLWithPath: "/a.m4a")) is MP4TagWriter)
|
|
#expect(TagWriterFactory.writer(for: URL(fileURLWithPath: "/a.flac")) == nil)
|
|
#expect(TagWriterFactory.writer(for: URL(fileURLWithPath: "/a.wav")) == nil)
|
|
}
|
|
|
|
// Step 1: Locate the sample.m4a fixture in the test bundle.
|
|
// Step 2: Copy it to a temp file so the bundle resource is not mutated.
|
|
// Step 3: Build EditableTrackFields with a specific title and artist.
|
|
// Step 4: Write the fields via MP4TagWriter.
|
|
// Step 5: Read the common title back via AVFoundation and assert it matches.
|
|
// Step 6: Assert the audio track is still present (file not corrupted).
|
|
@Test func m4aRoundTrips() async throws {
|
|
let src = try #require(fixtureURL("sample", "m4a"), "missing sample.m4a fixture")
|
|
let url = try tempCopy(of: src)
|
|
defer { try? FileManager.default.removeItem(at: url) }
|
|
var f = EditableTrackFields(from: .fixture())
|
|
f.title = "Round Trip"; f.artist = "The Verifier"
|
|
try MP4TagWriter().write(f, to: url)
|
|
#expect(try await readCommonTitle(url) == "Round Trip")
|
|
let tracks = try await AVURLAsset(url: url).loadTracks(withMediaType: .audio)
|
|
#expect(!tracks.isEmpty) // audio track survived the write
|
|
}
|
|
|
|
// Step 1: Check if sample.mp3 fixture is available; skip trivially if absent.
|
|
// Step 2: Copy it to a temp file.
|
|
// Step 3: Build EditableTrackFields with a specific title.
|
|
// Step 4: Write the fields via ID3TagWriter.
|
|
// Step 5: Read the common title back via AVFoundation and assert it matches.
|
|
@Test func mp3RoundTrips() async throws {
|
|
guard let src = fixtureURL("sample", "mp3") else { return } // no fixture → trivially pass
|
|
let url = try tempCopy(of: src)
|
|
defer { try? FileManager.default.removeItem(at: url) }
|
|
var f = EditableTrackFields(from: .fixture())
|
|
f.title = "ID3 Round Trip"; f.artist = "Tagger"
|
|
try ID3TagWriter().write(f, to: url)
|
|
#expect(try await readCommonTitle(url) == "ID3 Round Trip")
|
|
}
|
|
}
|
|
|