EcritureWavTest.swift 138 lignes · 6022 octets
import XCTest
@testable import Card2vcf

/// Preuve de rougeur (exigence du plan `docs/plans/rattrapage-ios-et-tests.md`, Lot 2a) :
/// En neutralisant l'écriture de l'en-tête (en commentant `handle.seek(toFileOffset: 0)` +
/// `handle.write(entete)` dans `EcritureWav.fermer()`), les tests échouent avec :
///
///   testEnteteRIFFValide — XCTAssertEqual failed: ("RIFF") is not equal to ("    ")
///   testSampleRateEt16Bits — XCTAssertEqual failed: ("16000") is not equal to ("0")
///
/// (Les 44 premiers octets restent à zéro ; les octets nuls sont rendus comme espaces
/// par la décodification ASCII, le sampleRate lu sur 4 octets nuls vaut 0.)
final class EcritureWavTest: XCTestCase {
    private var tempUrl: URL!

    override func setUp() {
        super.setUp()
        tempUrl = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
            .appendingPathExtension("wav")
    }

    override func tearDown() {
        super.tearDown()
        try? FileManager.default.removeItem(at: tempUrl)
    }

    // MARK: - En-tête RIFF

    func testEnteteRIFFValide() throws {
        let ecrivain = try EcritureWav(url: tempUrl)
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        XCTAssertGreaterThanOrEqual(data.count, 44, "Fichier doit faire au moins 44 octets")
        let riff = String(bytes: data[0..<4], encoding: .ascii) ?? ""
        XCTAssertEqual("RIFF", riff, "Les 4 premiers octets doivent être 'RIFF'")
        let wave = String(bytes: data[8..<12], encoding: .ascii) ?? ""
        XCTAssertEqual("WAVE", wave, "Les octets 8-11 doivent être 'WAVE'")
    }

    func testChunkFmtPresent() throws {
        let ecrivain = try EcritureWav(url: tempUrl)
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        let fmt = String(bytes: data[12..<16], encoding: .ascii) ?? ""
        XCTAssertEqual("fmt ", fmt, "Les octets 12-15 doivent être 'fmt '")
        let fmtSize = data[16...19].withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
        XCTAssertEqual(16, fmtSize, "taille du chunk fmt doit être 16 (PCM)")
        let audioFormat = data[20...21].withUnsafeBytes { $0.load(as: Int16.self).littleEndian }
        XCTAssertEqual(1, audioFormat, "audioFormat doit être 1 (PCM linéaire)")
    }

    func testSampleRateEt16Bits() throws {
        let ecrivain = try EcritureWav(url: tempUrl)
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        // sampleRate aux octets 24-27
        let sampleRate = data[24...27].withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
        XCTAssertEqual(16_000, sampleRate, "sampleRate doit être 16 000 Hz")
        // canaux aux octets 22-23
        let canaux = data[22...23].withUnsafeBytes { $0.load(as: Int16.self).littleEndian }
        XCTAssertEqual(1, canaux, "doit être mono (1 canal)")
        // bitsParEchantillon aux octets 34-35
        let bits = data[34...35].withUnsafeBytes { $0.load(as: Int16.self).littleEndian }
        XCTAssertEqual(16, bits, "doit être 16 bits par échantillon")
    }

    // MARK: - Taille cohérente avec les données

    func testTailleBloc_data_CohérenteAvecEchantillons() throws {
        let ecrivain = try EcritureWav(url: tempUrl)
        let echantillons: [Int16] = [0, 100, -100, 32767, -32768]
        ecrivain.ecrireEchantillons(echantillons)
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        // tag "data" aux octets 36-39
        let dataTag = String(bytes: data[36..<40], encoding: .ascii) ?? ""
        XCTAssertEqual("data", dataTag)
        // taille du bloc data aux octets 40-43
        let tailleData = data[40...43].withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
        XCTAssertEqual(Int32(echantillons.count * 2), tailleData,
                       "taille data = nb échantillons × 2 octets")
        // taille totale du fichier
        XCTAssertEqual(44 + echantillons.count * 2, data.count)
    }

    func testRIFFSizeCohérente() throws {
        let echantillons = [Int16](repeating: 42, count: 100)
        let ecrivain = try EcritureWav(url: tempUrl)
        ecrivain.ecrireEchantillons(echantillons)
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        // riffSize aux octets 4-7 = taille totale - 8
        let riffSize = data[4...7].withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
        let attendu = Int32(36 + echantillons.count * 2)
        XCTAssertEqual(attendu, riffSize, "riffSize doit valoir totalOctets - 8")
    }

    func testByteRateEtBlockAlign() throws {
        // byteRate = sampleRate × canaux × (bits/8) = 16000 × 1 × 2 = 32000
        // blockAlign = canaux × (bits/8) = 1 × 2 = 2
        let ecrivain = try EcritureWav(url: tempUrl)
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        let byteRate = data[28...31].withUnsafeBytes { $0.load(as: Int32.self).littleEndian }
        XCTAssertEqual(32_000, byteRate)
        let blockAlign = data[32...33].withUnsafeBytes { $0.load(as: Int16.self).littleEndian }
        XCTAssertEqual(2, blockAlign)
    }

    // MARK: - Cas limite

    func testFichierVideSansEchantillons() throws {
        let ecrivain = try EcritureWav(url: tempUrl)
        let octets = try ecrivain.fermer()

        XCTAssertEqual(0, octets)
        let data = try Data(contentsOf: tempUrl)
        XCTAssertEqual(44, data.count, "Fichier vide = 44 octets d'en-tête uniquement")
    }

    func testEchantillonsPCMEncodésLittleEndian() throws {
        let ecrivain = try EcritureWav(url: tempUrl)
        // 0x0100 en little-endian s'écrit [0x00, 0x01]
        ecrivain.ecrireEchantillons([0x0100])
        try ecrivain.fermer()

        let data = try Data(contentsOf: tempUrl)
        XCTAssertEqual(0x00, data[44], "octet bas d'abord (little-endian)")
        XCTAssertEqual(0x01, data[45], "octet haut ensuite")
    }
}