importer chnges

This commit is contained in:
Mark Gregory
2026-11-04 16:10:32 +00:00
parent c35dd4b20e
commit 1f397685dd
10 changed files with 399 additions and 53 deletions
+114
View File
@@ -6,6 +6,118 @@ import UniformTypeIdentifiers
@testable import KJamsClone
final class KJamsCloneTests: XCTestCase {
@MainActor func testLargeImportReturnsImmediatelyAndReportsCompletion() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
let inputs = root.appendingPathComponent("Inputs");try FileManager.default.createDirectory(at:inputs,withIntermediateDirectories:true)
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
var urls = [URL]()
for index in 0..<48 { let url = inputs.appendingPathComponent("Track \(index).mp3");try FileManager.default.copyItem(at:fixture,to:url);urls.append(url) }
let model = AppModel(storageURL:root.appendingPathComponent("Library"))
model.importFiles(urls)
XCTAssertTrue(model.isImporting,"Import must return to the UI before processing the files")
XCTAssertTrue(model.songs.isEmpty)
var heartbeat = false
Task { @MainActor in heartbeat = true }
for _ in 0..<500 { if !model.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertTrue(heartbeat);XCTAssertFalse(model.isImporting)
XCTAssertEqual(model.importProgress?.completed,48);XCTAssertEqual(model.importProgress?.total,48)
XCTAssertEqual(model.importProgress?.finished,true);XCTAssertNil(model.importProgress?.failure)
XCTAssertEqual(model.songs.count,1,"Identical audio still deduplicates")
model.dismissImport();XCTAssertNil(model.importProgress)
}
func testImportCancellationReportsSavedPartialWork() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let db = try Library(url:root.appendingPathComponent("Library.sqlite"))
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
var cancel = false,latest = ImportProgress()
XCTAssertThrowsError(try Importer(media:root.appendingPathComponent("Media")).run([fixtures.appendingPathComponent("Demo.mp3"),fixtures.appendingPathComponent("TestTrack.mp3")],library:db,cancelled:{cancel},progress:{ latest = $0;if $0.completed == 1 { cancel = true } })) { XCTAssertTrue($0 is CancellationError) }
XCTAssertEqual(latest.completed,1);XCTAssertEqual(latest.total,2);XCTAssertEqual(try db.songs().count,1)
}
@MainActor func testImportFailureAndCancellationFinishTheDialog() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
let model = AppModel(storageURL:root)
model.importFiles([root.appendingPathComponent("missing.mp3")])
for _ in 0..<200 { if !model.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertFalse(model.isImporting);XCTAssertEqual(model.importProgress?.finished,true);XCTAssertNotNil(model.importProgress?.failure)
model.dismissImport()
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
model.importFiles([fixture]);model.cancelImport()
for _ in 0..<200 { if !model.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertFalse(model.isImporting);XCTAssertEqual(model.importProgress?.cancelled,true)
model.dismissImport();XCTAssertNil(model.importProgress)
}
func testImportHistoryPersistsAndSkipsUnchangedTracksAndRenamedDuplicates() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
let source = root.appendingPathComponent("Original.mp3");try FileManager.default.copyItem(at:fixture,to:source)
let database = root.appendingPathComponent("Library.sqlite"),media = root.appendingPathComponent("Media"),importer = Importer(media:media)
let db = try Library(url:database)
let first = try importer.run([source],library:db)
let id = try XCTUnwrap(first.first);try db.rate(id,5)
let stored = try XCTUnwrap(db.song(id));let modified = try FileManager.default.attributesOfItem(atPath:stored.audio)[.modificationDate] as? Date
let reopened = try Library(url:database);var progress = ImportProgress()
XCTAssertEqual(try importer.run([source],library:reopened,progress:{progress = $0}),first)
XCTAssertEqual(progress.completed,1);XCTAssertEqual(progress.skipped,1)
let renamed = root.appendingPathComponent("Renamed.mp3");try FileManager.default.copyItem(at:source,to:renamed)
XCTAssertEqual(try importer.run([renamed],library:reopened,progress:{progress = $0}),first)
XCTAssertEqual(progress.skipped,1);XCTAssertEqual(try reopened.songs().count,1)
XCTAssertEqual(try reopened.song(id)?.rating,5)
XCTAssertEqual(try FileManager.default.attributesOfItem(atPath:stored.audio)[.modificationDate] as? Date,modified)
try FileManager.default.removeItem(at:URL(fileURLWithPath:stored.audio))
_ = try importer.run([source],library:reopened,progress:{progress = $0})
XCTAssertEqual(progress.skipped,0);XCTAssertTrue(FileManager.default.fileExists(atPath:stored.audio))
}
func testUnchangedArchiveSkipsExtractionAndMissingGraphicsCanBeRestored() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.zip"))
let importer = Importer(media:root.appendingPathComponent("Media")),db = try Library(url:root.appendingPathComponent("Library.sqlite"))
let ids = try importer.run([fixture],library:db);var updates = [ImportProgress]()
XCTAssertEqual(try importer.run([fixture],library:db,progress:{updates.append($0)}),ids)
XCTAssertFalse(updates.contains{$0.current.hasPrefix("Extracting ")});XCTAssertEqual(updates.last?.skipped,1)
let cdg = try XCTUnwrap(try db.songs().first?.cdg);try FileManager.default.removeItem(atPath:cdg)
updates = [];_ = try importer.run([fixture],library:db,progress:{updates.append($0)})
XCTAssertTrue(updates.contains{$0.current.hasPrefix("Extracting ")});XCTAssertEqual(updates.last?.skipped,0)
XCTAssertTrue(FileManager.default.fileExists(atPath:try XCTUnwrap(try db.songs().first?.cdg)))
}
func testChangedAudioAndGraphicsAreRechecked() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
let audio = root.appendingPathComponent("Track.mp3"),cdg = root.appendingPathComponent("Track.cdg")
try FileManager.default.copyItem(at:fixtures.appendingPathComponent("Demo.mp3"),to:audio)
try FileManager.default.copyItem(at:fixtures.appendingPathComponent("Demo.cdg"),to:cdg)
let db = try Library(url:root.appendingPathComponent("Library.sqlite")),importer = Importer(media:root.appendingPathComponent("Media"))
let ids = try importer.run([audio],library:db)
let originalGraphics = try XCTUnwrap(try db.song(ids[0])?.cdg)
let changedGraphics = try Data(contentsOf:fixtures.appendingPathComponent("TestTrack.cdg"));try changedGraphics.write(to:cdg)
var progress = ImportProgress()
XCTAssertEqual(try importer.run([audio],library:db,progress:{progress = $0}),ids);XCTAssertEqual(progress.skipped,0)
let newGraphics = try XCTUnwrap(try db.song(ids[0])?.cdg)
XCTAssertNotEqual(originalGraphics,newGraphics);XCTAssertEqual(try Data(contentsOf:URL(fileURLWithPath:newGraphics)),changedGraphics)
try Data(contentsOf:fixtures.appendingPathComponent("TestTrack.mp3")).write(to:audio)
_ = try importer.run([audio],library:db,progress:{progress = $0});XCTAssertEqual(progress.skipped,0);XCTAssertEqual(try db.songs().count,2)
}
func testArchiveDeduplicationStillCreatesManagedMediaForReferencedAudio() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
let db = try Library(url:root.appendingPathComponent("Library.sqlite")),media = root.appendingPathComponent("Media"),importer = Importer(media:media)
let ids = try importer.run([fixtures.appendingPathComponent("Demo.mp3")],library:db,copyMedia:false)
_ = try importer.run([fixtures.appendingPathComponent("Demo.zip")],library:db,copyMedia:false)
XCTAssertEqual(try db.songs().count,1)
let song = try XCTUnwrap(try db.song(ids[0]));XCTAssertEqual(URL(fileURLWithPath:song.audio).deletingLastPathComponent().standardizedFileURL,media.standardizedFileURL)
XCTAssertTrue(FileManager.default.fileExists(atPath:song.audio));XCTAssertNotNil(song.cdg)
}
func packet(_ instruction: UInt8, _ data: [UInt8]) -> [UInt8] { [9,instruction,0,0] + Array((data + Array(repeating: 0,count:16)).prefix(16)) + [0,0,0,0] }
func testPresetTileXORAndInvalidCoordinates() {
var d = CDGDecoder()
@@ -219,6 +331,8 @@ final class KJamsCloneTests: XCTestCase {
_ = try model.createEntity(.singer,name:"Alex")
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
model.importFiles([fixtures.appendingPathComponent("Demo.zip")])
for _ in 0..<200 { if !model.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertFalse(model.isImporting);XCTAssertNil(model.importProgress?.failure)
let song = try XCTUnwrap(model.songs.first)
model.enqueue(song.id); model.enqueue(song.id); model.playQueueNext()
XCTAssertEqual(model.queue.count,1); XCTAssertEqual(model.history.count,1)