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.
110 lines
6.0 KiB
110 lines
6.0 KiB
import Testing
|
|
import Foundation
|
|
@testable import Music
|
|
|
|
struct ScannerServiceTests {
|
|
// Creates a temp directory with audio and non-audio files, verifies only audio files are discovered.
|
|
// 1. Creates a unique temporary directory.
|
|
// 2. Creates files with audio extensions (mp3, m4a, flac, wav, aiff) and non-audio extensions (txt, png, json).
|
|
// 3. Creates a subdirectory with a nested mp3 file to test recursive discovery.
|
|
// 4. Calls ScannerService.discoverAudioFiles and checks that exactly 6 audio files are found.
|
|
// 5. Verifies all discovered files have audio-only extensions.
|
|
// 6. Cleans up the temp directory.
|
|
@Test func discoverAudioFiles() throws {
|
|
let tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: tmpDir) }
|
|
|
|
let audioFiles = ["song.mp3", "track.m4a", "audio.flac", "sound.wav", "music.aiff"]
|
|
let nonAudioFiles = ["readme.txt", "image.png", "data.json"]
|
|
|
|
for name in audioFiles + nonAudioFiles {
|
|
FileManager.default.createFile(atPath: tmpDir.appendingPathComponent(name).path, contents: Data())
|
|
}
|
|
|
|
let subDir = tmpDir.appendingPathComponent("subfolder")
|
|
try FileManager.default.createDirectory(at: subDir, withIntermediateDirectories: true)
|
|
FileManager.default.createFile(atPath: subDir.appendingPathComponent("nested.mp3").path, contents: Data())
|
|
|
|
let discovered = ScannerService.discoverAudioFiles(in: tmpDir)
|
|
#expect(discovered.count == 6)
|
|
|
|
let extensions = Set(discovered.map { $0.pathExtension.lowercased() })
|
|
#expect(extensions.isSubset(of: ["mp3", "m4a", "flac", "wav", "aiff", "aac", "alac"]))
|
|
}
|
|
|
|
// Verifies resolveBitrate uses the OS estimate when it is positive.
|
|
// 1. Passes a positive estimatedDataRate in bits/sec (320450).
|
|
// 2. Expects it rounded to kbps (320450/1000 = 320.45, which rounds to 320), ignoring size/duration.
|
|
@Test func resolveBitrateUsesEstimateWhenPositive() {
|
|
let kbps = ScannerService.resolveBitrate(estimatedDataRate: 320_450,
|
|
fileSizeBytes: 5_000_000,
|
|
durationSeconds: 200)
|
|
#expect(kbps == 320)
|
|
}
|
|
|
|
// Verifies the size/duration fallback when the OS estimate is 0 (the AVFoundation bug).
|
|
// 1. Passes estimatedDataRate 0 with a real file size and duration.
|
|
// 2. Expects 230_358_479 * 8 / 7198.54 s / 1000 -> ~256.0 -> 256 kbps (matches ffprobe).
|
|
@Test func resolveBitrateFallsBackToSizeAndDuration() {
|
|
let kbps = ScannerService.resolveBitrate(estimatedDataRate: 0,
|
|
fileSizeBytes: 230_358_479,
|
|
durationSeconds: 7198.5371428571425)
|
|
#expect(kbps == 256)
|
|
}
|
|
|
|
// Verifies nil (never 0) when the estimate is 0 and duration is unusable.
|
|
// 1. Zero duration cannot yield a value -> nil.
|
|
// 2. A NaN duration (CMTimeGetSeconds can return NaN) is also nil, not 0.
|
|
@Test func resolveBitrateReturnsNilWhenNoDuration() {
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: 0,
|
|
fileSizeBytes: 230_358_479,
|
|
durationSeconds: 0) == nil)
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: 0,
|
|
fileSizeBytes: 230_358_479,
|
|
durationSeconds: .nan) == nil)
|
|
}
|
|
|
|
// Verifies nil when the estimate is 0 and there is no file size.
|
|
// 1. Missing fileSizeBytes with estimate 0 -> nil (never 0).
|
|
@Test func resolveBitrateReturnsNilWhenNoFileSize() {
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: 0,
|
|
fileSizeBytes: nil,
|
|
durationSeconds: 200) == nil)
|
|
}
|
|
|
|
// Verifies the core invariant: no input combination ever yields 0.
|
|
// 1. All-zero inputs return nil so the UI renders "—" instead of "0 kbps".
|
|
@Test func resolveBitrateNeverReturnsZero() {
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: 0,
|
|
fileSizeBytes: 0,
|
|
durationSeconds: 0) == nil)
|
|
}
|
|
|
|
// Verifies the fallback never returns 0 for sub-kbps inputs (corrupt/truncated file).
|
|
// 1. Estimate 0, a 1-byte file with a 1-hour duration -> 8/3600/1000 ≈ 0 kbps.
|
|
// 2. Must return nil (never 0), upholding the "—" display invariant.
|
|
@Test func resolveBitrateFallbackBelowOneKbpsReturnsNil() {
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: 0,
|
|
fileSizeBytes: 1,
|
|
durationSeconds: 3600) == nil)
|
|
}
|
|
|
|
// Verifies the estimate branch never returns 0 for a sub-kbps estimate.
|
|
// 1. A tiny positive estimate (400 bits/s -> 0.4 kbps) rounds to 0.
|
|
// 2. With no size/duration to fall back on, the result must be nil, not 0.
|
|
@Test func resolveBitrateSubKbpsEstimateReturnsNil() {
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: 400,
|
|
fileSizeBytes: nil,
|
|
durationSeconds: nil) == nil)
|
|
}
|
|
|
|
// Verifies a (non-physical) negative estimate falls through to the formula.
|
|
// 1. estimatedDataRate -1 fails the `> 0` guard.
|
|
// 2. Falls back to size/duration -> 256 kbps, never a negative kbps.
|
|
@Test func resolveBitrateNegativeEstimateFallsBackToFormula() {
|
|
#expect(ScannerService.resolveBitrate(estimatedDataRate: -1,
|
|
fileSizeBytes: 230_358_479,
|
|
durationSeconds: 7198.5371428571425) == 256)
|
|
}
|
|
}
|
|
|