DepotModelesTest.kt
198 lignes · 6781 octets
package fr.ebii.card2vcf.modeles import android.content.Context import androidx.test.core.app.ApplicationProvider import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okio.Buffer import org.junit.After import org.junit.Before import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.io.ByteArrayOutputStream import java.io.File import java.security.MessageDigest import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertTrue @RunWith(RobolectricTestRunner::class) @Config(sdk = [31]) class DepotModelesTest { private lateinit var serveur: MockWebServer private lateinit var context: Context private lateinit var prefs: PreferencesModeles private lateinit var depot: DepotModeles private val contenuFra = "traineddata factice pour les tests".repeat(400).toByteArray() private fun sha256(octets: ByteArray): String = MessageDigest.getInstance("SHA-256").digest(octets).joinToString("") { "%02x".format(it) } private fun modeleFra() = ModeleLocal( id = "fra", type = TypeModele.TESSDATA, tailleOctets = contenuFra.size.toLong(), sha256 = sha256(contenuFra), ) private fun corpsBinaire(octets: ByteArray) = MockResponse().setBody(Buffer().write(octets)) @Before fun setUp() { serveur = MockWebServer().apply { start() } context = ApplicationProvider.getApplicationContext() context.filesDir.deleteRecursively() prefs = PreferencesModeles(context).apply { setMiroir(serveur.url("/").toString().trimEnd('/')) } depot = DepotModeles(context, prefs) } @After fun tearDown() { serveur.shutdown() } private fun fichierTessdata(langue: String) = File(context.filesDir, "tesseract/tessdata/$langue.traineddata") @Test fun `un modele absent est signale absent`() { assertIs<EtatModele.Absent>(depot.etat(modeleFra())) } @Test fun `telechargement nominal ecrit le traineddata et passe a present`() = runTest { serveur.enqueue(corpsBinaire(contenuFra)) depot.telecharger(modeleFra()) val cible = fichierTessdata("fra") assertTrue(cible.isFile, "le traineddata doit être écrit") assertEquals(contenuFra.size.toLong(), cible.length()) val etat = depot.etat(modeleFra()) assertIs<EtatModele.Present>(etat) assertEquals(contenuFra.size.toLong(), etat.tailleOctets) } @Test fun `telechargement pose le stamp external pour proteger du recouvrement par les assets`() = runTest { serveur.enqueue(corpsBinaire(contenuFra)) depot.telecharger(modeleFra()) val stamp = File(context.filesDir, "tesseract/tessdata/.card2vcf-tessdata-variant") assertTrue(stamp.isFile, "le stamp doit exister") assertEquals("external", stamp.readText().trim()) } @Test fun `le miroir configure est bien celui qui est interroge`() = runTest { serveur.enqueue(corpsBinaire(contenuFra)) depot.telecharger(modeleFra()) assertEquals("/fra.traineddata", serveur.takeRequest().path) } @Test fun `une empreinte invalide laisse le modele absent et ne laisse aucun fichier partiel`() = runTest { serveur.enqueue(corpsBinaire("contenu corrompu".toByteArray())) depot.telecharger(modeleFra()) val etat = depot.etat(modeleFra()) assertIs<EtatModele.Echec>(etat) assertFalse(fichierTessdata("fra").exists(), "aucun traineddata ne doit subsister") assertFalse( File(context.filesDir, "tesseract/tessdata/fra.traineddata.part").exists(), "le fichier partiel doit être nettoyé", ) } @Test fun `une erreur reseau est rapportee en echec sans fichier residuel`() = runTest { serveur.enqueue(MockResponse().setResponseCode(404)) depot.telecharger(modeleFra()) assertIs<EtatModele.Echec>(depot.etat(modeleFra())) assertFalse(fichierTessdata("fra").exists()) assertFalse(File(context.filesDir, "tesseract/tessdata/fra.traineddata.part").exists()) } @Test fun `un echec n'empeche pas une relance reussie`() = runTest { serveur.enqueue(MockResponse().setResponseCode(500)) depot.telecharger(modeleFra()) assertIs<EtatModele.Echec>(depot.etat(modeleFra())) serveur.enqueue(corpsBinaire(contenuFra)) depot.telecharger(modeleFra()) assertIs<EtatModele.Present>(depot.etat(modeleFra())) } @Test fun `supprimer un modele le ramene a absent`() = runTest { serveur.enqueue(corpsBinaire(contenuFra)) depot.telecharger(modeleFra()) assertIs<EtatModele.Present>(depot.etat(modeleFra())) depot.supprimer(modeleFra()) assertIs<EtatModele.Absent>(depot.etat(modeleFra())) assertFalse(fichierTessdata("fra").exists()) } @Test fun `languesPretes ne retient que les langues selectionnees et presentes`() = runTest { serveur.enqueue(corpsBinaire(contenuFra)) depot.telecharger(modeleFra()) prefs.setLanguesSelectionnees(setOf("fra", "deu")) assertEquals(listOf("fra"), depot.languesPretes()) } @Test fun `le modele vocal est decompresse dans filesDir`() = runTest { val zip = zipDeTest() val modele = ModeleLocal( id = "vosk-model-small-fr-0.22", type = TypeModele.VOSK, tailleOctets = zip.size.toLong(), sha256 = sha256(zip), ) serveur.enqueue(corpsBinaire(zip)) depot.telecharger(modele) val racine = File(context.filesDir, "vosk-model-small-fr-0.22") assertTrue(racine.isDirectory, "le modèle doit être décompressé") assertEquals("bonjour", File(racine, "am/final.mdl").readText()) assertIs<EtatModele.Present>(depot.etat(modele)) assertFalse( File(context.filesDir, "vosk-model-small-fr-0.22.zip.part").exists(), "l'archive téléchargée doit être nettoyée", ) } /** Archive minimale imitant la structure d'un modèle Vosk. */ private fun zipDeTest(): ByteArray { val sortie = ByteArrayOutputStream() ZipOutputStream(sortie).use { zos -> zos.putNextEntry(ZipEntry("vosk-model-small-fr-0.22/am/final.mdl")) zos.write("bonjour".toByteArray()) zos.closeEntry() } return sortie.toByteArray() } }
GitRust