From d4b0451863aff0c52199cc3442bdbb6ad3f5e9c2 Mon Sep 17 00:00:00 2001 From: Patrice Haltaya Date: Mon, 9 Nov 2026 17:45:29 +0100 Subject: [PATCH 1/3] build(deps): AGP 9.5.0, Gradle 9.8.2, SQLCipher 4.20.0, Markdown 0.7.18; DataStore removed Brings the open Dependabot updates together: #46 (AGP 9.5.0), #35 (Gradle wrapper 9.8.2), #47 (SQLCipher 4.20.0), #32 (org.jetbrains:markdown 0.7.18). #34 (DataStore 1.3.0) is not taken: the dependency goes instead. - AGP 9.5.0 needs Gradle 9.6.0 or later: 9.8.2 here. The wrapper's distributionSha256Sum is Gradle's published one, and 9.8.2 is in F-Droid's gradle-transparency-log with the same sum (F-Droid builds Notes Tech from source). Only the properties are taken from #35: its gradlew.bat change was line endings. - SQLCipher 4.17 to 4.20: SQLite 3.54.1, compileSdk 37 (ours), 16 KB page linker flag. 4.19.1 changes what getColumnType returns for NULL: nothing here reads it, Room goes through isNull. cipher_compatibility = 4 is still set at opening, secure_delete is still set by the app and checked on device (EffacementDansLaBaseTest). - Markdown 0.7.15 to 0.7.18: ==text== becomes a GFM highlight node, and the > of quote continuation lines become BLOCK_QUOTE tokens. The preview already draws an unknown node as its text and a BLOCK_QUOTE token as nothing; a test pins the highlight shown as written, as 2.0.9 shows it. - DataStore was never imported: D-015 kept the settings in the Flutter preferences file, and the dependency had stayed "for phase 7", which did without it. R8 removed its code, but every APK still shipped lib//libdatastore_shared_counter.so. Removed, with Okio, which only it pulled in. THIRD_PARTY_NOTICES read again from the resolved releaseRuntimeClasspath: SQLCipher 4.20.0, Markdown 0.7.18, androidx.sqlite 2.7.0 (was written 2.6.2), DataStore and Okio gone, DocumentFile and Transition no longer resolved. README stack: SQLCipher 4.20, no DataStore, Gradle 9.8.2. Local gate (assembleDebug, 551 unit tests, lintDebug, detekt, ktlintCheck, androidTest compile) and assembleRelease green. Release APKs signed with the release key for a trial only: same certificate and permissions as the published 3.3.0, no INTERNET. On the S9, installed over the published 3.3.0: notes kept, full-text search finds a note written before the update and a 2.0.9 one, archive by month and permanent deletion work, no crash. Co-Authored-By: Claude Opus 5.5 --- README.fr.md | 4 ++-- README.md | 4 ++-- THIRD_PARTY_NOTICES.md | 18 ++++++++---------- app/build.gradle.kts | 2 -- .../data/local/NotesDatabaseFactory.kt | 4 ++-- .../markdown/MarkdownPreviewReaderTest.kt | 10 ++++++++++ docs/01-DECISIONS.md | 5 +++++ gradle/libs.versions.toml | 16 ++++++++-------- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 9 files changed, 39 insertions(+), 28 deletions(-) diff --git a/README.fr.md b/README.fr.md index 24f81044..2c5a7bd4 100644 --- a/README.fr.md +++ b/README.fr.md @@ -238,7 +238,7 @@ Pas de Play Store : aucun compte n'est nécessaire pour installer. ## Construction locale Prérequis : JDK 17, SDK Android 37, NDK `27.0.12077973` et CMake 3.22.1 (épinglés dans -`app/build.gradle.kts`). Gradle 9.8.1 arrive avec le wrapper. +`app/build.gradle.kts`). Gradle 9.8.2 arrive avec le wrapper. ```bash ./gradlew testDebugUnitTest lintDebug @@ -256,7 +256,7 @@ et l'APK universel `versionCode × 10`. ## Technique -- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room sur **SQLCipher** 4.16, DataStore +- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room sur **SQLCipher** 4.20 - Bouncy Castle (Argon2id), Keystore Android, AES-256-GCM de la plateforme - `org.jetbrains:markdown` (aperçu), whisper.cpp par JNI (dictée) - **Aucune bibliothèque réseau** diff --git a/README.md b/README.md index 44fd3924..5bd4214f 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ No Play Store: no account is needed to install. ## Local build Requirements: JDK 17, Android SDK 37, NDK `27.0.12077973` and CMake 3.22.1 (pinned in -`app/build.gradle.kts`). Gradle 9.8.1 comes with the wrapper. +`app/build.gradle.kts`). Gradle 9.8.2 comes with the wrapper. ```bash ./gradlew testDebugUnitTest lintDebug @@ -243,7 +243,7 @@ and the universal APK `versionCode × 10`. ## Stack -- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room on **SQLCipher** 4.16, DataStore +- Kotlin 2.4, Jetpack Compose (Material 3), Hilt, Room on **SQLCipher** 4.20 - Bouncy Castle (Argon2id), Android Keystore, AES-256-GCM from the platform - `org.jetbrains:markdown` (preview), whisper.cpp through JNI (dictation) - **No network library** diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c968c059..c63eb79a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,10 +1,10 @@ # Third-party notices — Notes Tech -Notes Tech 3.1 (`com.filestech.notes_tech`) is written in Kotlin with Jetpack Compose and is licensed +Notes Tech 3 (`com.filestech.notes_tech`) is written in Kotlin with Jetpack Compose and is licensed under the Apache License 2.0 ([LICENSE](LICENSE)). Everything it ships that it did not write is listed -here. The list was read on 2026-10-10 from the resolved `releaseRuntimeClasspath` of the `:app` module -for 3.1.0, after the move to AGP 9 (earlier versions: see this file at their tag, e.g. `v3.0.0`), and -each licence from the POM that the library publishes — not from memory. +here. The list was read on 2026-11-09 from the resolved `releaseRuntimeClasspath` of the `:app` module, +built with AGP 9.5.0 (earlier versions: see this file at their tag, e.g. `v3.0.0`), and each licence +from the POM that the library publishes — not from memory. **No Google Play Services, no ML Kit, no Firebase, no analytics SDK, no crash reporter, no network library.** The app does not hold the `INTERNET` permission. @@ -13,21 +13,19 @@ library.** The app does not hold the `INTERNET` permission. | Library | Version | Publisher | Licence | |---|---|---|---| -| AndroidX: Core, Activity, AppCompat, Lifecycle, Navigation, SavedState, Fragment, Emoji2, Window, Startup, Tracing, Profile Installer, DocumentFile, Biometric, Transition and their support modules | various | Google / AOSP | Apache 2.0 | +| AndroidX: Core, Activity, AppCompat, Lifecycle, Navigation, SavedState, Fragment, Emoji2, Window, Startup, Tracing, Profile Installer, Biometric and their support modules | various | Google / AOSP | Apache 2.0 | | Jetpack Compose: UI, Foundation, Animation, Runtime, Material 3, Material Icons | BOM 2026.09.00 (UI 1.12.1, Material 3 1.4.0) | Google / AOSP | Apache 2.0 | -| Room (`androidx.room`) and `androidx.sqlite` | 2.8.5 / 2.6.2 | Google / AOSP | Apache 2.0 | -| DataStore Preferences | 1.1.1 | Google / AOSP | Apache 2.0 | +| Room (`androidx.room`) and `androidx.sqlite` | 2.8.5 / 2.7.0 | Google / AOSP | Apache 2.0 | | Hilt and Dagger (`com.google.dagger`, `androidx.hilt`) | 2.60.1 / 1.4.0 | Google | Apache 2.0 | | Guava `listenablefuture`, JSR-305 annotations | 1.0 / 3.0.2 | Google | Apache 2.0 | | Kotlin standard library | 2.4.10 | JetBrains | Apache 2.0 | | kotlinx.coroutines | 1.11.0 | JetBrains | Apache 2.0 | -| `org.jetbrains:markdown` — the Markdown parser behind the preview | 0.7.14 | JetBrains | Apache 2.0 | +| `org.jetbrains:markdown` — the Markdown parser behind the preview | 0.7.18 | JetBrains | Apache 2.0 | | JetBrains annotations, JSpecify | 23.0.0 / 1.0.0 | JetBrains / JSpecify | Apache 2.0 | | `javax.inject`, `jakarta.inject-api` | 1 / 2.0.1 | JSR-330 / Eclipse Foundation | Apache 2.0 | -| Okio (pulled in by DataStore) | 3.4.0 | Square | Apache 2.0 | | Timber | 5.0.1 | Jake Wharton | Apache 2.0 | | **Bouncy Castle** `bcprov-jdk18on` — **Argon2id**, the key derivation of the vaults | 1.86 | The Legion of the Bouncy Castle | **MIT** — full text below | -| **SQLCipher for Android** — the encrypted database | 4.16.0 | Zetetic LLC | **BSD-3-Clause** — full text below | +| **SQLCipher for Android** — the encrypted database | 4.20.0 | Zetetic LLC | **BSD-3-Clause** — full text below | SQLCipher bundles **SQLite** and **LibTomCrypt**, both released into the public domain by their authors. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7f59826..8a096f1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -396,8 +396,6 @@ dependencies { implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) - implementation(libs.androidx.datastore.preferences) - implementation(libs.hilt.android) ksp(libs.hilt.compiler) implementation(libs.hilt.navigation.compose) diff --git a/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt b/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt index cd61c4d6..078839c1 100644 --- a/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt +++ b/app/src/main/java/com/filestech/notes_tech/data/local/NotesDatabaseFactory.kt @@ -120,8 +120,8 @@ class NotesDatabaseFactory( /** * `PRAGMA cipher_compatibility = 4` avant le calage de la clé. * - * Défensif et assumé comme tel : la 4.16 utilise déjà le format v4 par défaut, donc ce pragma - * ne change rien **aujourd'hui**. Il protège d'une montée de version majeure de SQLCipher qui + * Défensif et assumé comme tel : toute la série 4.x (4.20 aujourd'hui) utilise déjà le format v4 + * par défaut, donc ce pragma ne change rien **aujourd'hui**. Il protège d'une montée de version majeure de SQLCipher qui * passerait au format v5 en silence — auquel cas les bases existantes deviendraient * illisibles sans le moindre avertissement. La version Flutter pose le même pragma pour la * même raison (`database.dart:474`). diff --git a/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt b/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt index affe2195..dc534da2 100644 --- a/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt +++ b/app/src/test/java/com/filestech/notes_tech/domain/markdown/MarkdownPreviewReaderTest.kt @@ -265,6 +265,16 @@ class MarkdownPreviewReaderTest { fun `inline HTML and inline math are shown as written`() { assertThat(runsOf("x \$a+b\$")).containsExactly(run("x \$a+b\$")) } + + /** + * `==…==` became a node of the GFM parser in 0.7.15 (highlighting). 2.0.9 has none: it stays + * text, delimiters included — a node the reader does not know must not take characters away. + */ + @Test + fun `highlighting is shown as written, as 2_0_9 shows it`() { + assertThat(runsOf("a ==b== c")).containsExactly(run("a ==b== c")) + assertThat(runsOf("**x ==y==**")).containsExactly(run("x ==y==", RunStyle.BOLD)) + } } @Nested diff --git a/docs/01-DECISIONS.md b/docs/01-DECISIONS.md index 9f905b6c..54bed585 100644 --- a/docs/01-DECISIONS.md +++ b/docs/01-DECISIONS.md @@ -419,6 +419,11 @@ vérité pour l'état d'une même application. **Écarté** : DataStore, qui est l'API moderne et que le projet embarque déjà. Le coût de la migration l'emporte sur le bénéfice, et la dépendance reste pour la phase 7. +**Dépendance retirée le 2026-11-09** : la phase 7 (dictée) s'est faite sans elle, et rien dans le code +ne l'a jamais importée. R8 en retirait le code Java, mais chaque APK embarquait encore sa bibliothèque +native (`lib//libdatastore_shared_counter.so`, lu dans la 3.3.0 publiée) et Dependabot en +proposait les montées de version (#34). Les réglages restent où ce choix les a mis. + ⚠️ **Les formats ont été LUS dans le greffon** (`shared_preferences_android-2.4.26`, version figée par `pubspec.lock`), pas supposés. Le piège cher : `setInt` écrit un **`putLong`**. Le délai d'auto-verrouillage des coffres est un `int` ; `getInt` aurait levé `ClassCastException` au premier diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 894d4fb2..6b024f5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -# Build toolchain — the one Agenda Tech and App Manager Tech run (AGP 9.4.1 / Kotlin 2.4.10 / KSP 2.3.12 -# / JDK 17). AGP 9 compiles Kotlin itself: there is no `org.jetbrains.kotlin.android` plugin any more. -agp = "9.4.1" +# Build toolchain — AGP 9.5.0 (as SMS Tech since 2026-11-05; needs Gradle 9.6.0 or later) / Kotlin 2.4.10 / +# KSP 2.3.12 / JDK 17. AGP 9 compiles Kotlin itself: there is no `org.jetbrains.kotlin.android` plugin any more. +agp = "9.5.0" # Capped below 2.4.20: CodeQL 2.27 refuses to compile from there ("currently supports versions below # 2.4.20", measured on Agenda Tech). This is the EXACT compiler version: the root build.gradle.kts pins # the Kotlin Gradle Plugin to it with `strictly`, since AGP 9 would otherwise bring its own. @@ -15,7 +15,6 @@ activity-compose = "1.13.0" lifecycle = "2.11.0" navigation-compose = "2.10.2" room = "2.8.5" -datastore = "1.1.1" splashscreen = "1.2.0" # The app lock's fingerprint / face unlock (D-023). Same version as Agenda Tech and SMS Tech: 1.1.0 is # the last stable release, and the two portfolio apps that ship it have measured its pitfalls @@ -30,8 +29,10 @@ coroutines = "1.11.0" # SQLCipher — même artefact que celui embarqué par `sqflite_sqlcipher` côté Flutter, qui est en # 4.10.0. Le format de fichier est stable sur toute la série 4.x et `PRAGMA cipher_compatibility = 4` -# est posé explicitement à l'ouverture : une base écrite par la 4.10 s'ouvre avec la 4.16. -sqlcipher = "4.16.0" +# est posé explicitement à l'ouverture : une base écrite par la 4.10 s'ouvre avec la 4.20 +# (`LegacyDatabaseOpeningTest`, instrumenté). 4.19.1 change ce que rend `getColumnType` pour NULL : +# rien ici ne le lit, Room passe par `isNull`. +sqlcipher = "4.20.0" # Bouncy Castle — uniquement pour `Argon2BytesGenerator` (RFC 9106), qui n'a pas d'équivalent # dans la plateforme. On utilise l'API bas niveau `org.bouncycastle.crypto.*`, PAS le fournisseur @@ -43,7 +44,7 @@ timber = "5.0.1" # The Markdown preview's parser (D-024): pure Kotlin, Apache 2.0, a single dependency on # kotlin-stdlib. Only the GFM parser is used; the rendering is written here, in Compose. -jetbrains-markdown = "0.7.14" +jetbrains-markdown = "0.7.18" # Désucrage des bibliothèques de base — `java.time` n'est natif qu'à partir de l'API 26, or le # plancher est à 24 (cf. `app/build.gradle.kts`). Sans ça, tout formatage de date écrirait du @@ -91,7 +92,6 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } -androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a78c88e5..5907d39c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f -distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip +distributionSha256Sum=21bd5da2de49b642904077f38c0efdc7778e92b3ed0889392dca61b97e389607 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From f5697e94947ae4de0c18e12deb028d04b5cb9b65 Mon Sep 17 00:00:00 2001 From: Patrice Haltaya Date: Mon, 9 Nov 2026 17:45:29 +0100 Subject: [PATCH 2/3] fix(home): no Java type mismatch in the saver of the gestures `?.let(::ArrayList)` resolves to the Java constructor taking a `Collection`, which Kotlin 2.4 flags as a Java type mismatch, four times, in code added by 3.3.0. A lambda builds the same ArrayList. Co-Authored-By: Claude Opus 5.5 --- .../notes_tech/ui/home/GestesDeNoteEnCours.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt b/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt index 3d3748ee..f36bfb7a 100644 --- a/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt +++ b/app/src/main/java/com/filestech/notes_tech/ui/home/GestesDeNoteEnCours.kt @@ -166,11 +166,13 @@ internal class GestesDeNoteEnCours( /** What a rotation, or the process dying, must not lose: the questions open, by id. */ val Sauvegarde: Saver = listSaver( save = { etat -> + // A lambda, not `::ArrayList`: that reference resolves to the Java constructor taking a + // `Collection`, which Kotlin 2.4 flags as a Java type mismatch. listOf( - etat.suppression?.let(::ArrayList), - etat.deplacement?.let(::ArrayList), - etat.couleur?.let(::ArrayList), - etat.sortie?.ids?.let(::ArrayList), + etat.suppression?.let { ArrayList(it) }, + etat.deplacement?.let { ArrayList(it) }, + etat.couleur?.let { ArrayList(it) }, + etat.sortie?.ids?.let { ArrayList(it) }, etat.sortie?.dossierId, etat.sortieNombre, ) From 78b73acaf5e5454f52821d82c0ecff5fa7cf2b62 Mon Sep 17 00:00:00 2001 From: Patrice Haltaya Date: Mon, 9 Nov 2026 18:07:38 +0100 Subject: [PATCH 3/3] ci(instrumented): read the reason of a skip as AGP 9.5 writes it Up to AGP 9.4, an assumption that does not hold came out of the instrumented run as a carrying the AssumptionViolatedException. AGP 9.5 writes it as a proper , the exception and its stack in the element's text. The judge read only , and gave every the same reasonless text: on this PR's first run it refused the eleven admitted skips (software-only Keystore, no fingerprint, no dictation model) although each carried its admitted reason. 557 passed, the hardware Keystore witness failed as it must. The reason is now read from the text. A skip without an AssumptionViolatedException (an @Ignore, an empty skip) is still never admitted, even when its text names an admitted reason. Four self-test scenarios cover the new form; the judge accepts this PR's real report. Co-Authored-By: Claude Opus 5.5 --- tools/check-instrumented-results.py | 51 +++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/tools/check-instrumented-results.py b/tools/check-instrumented-results.py index cd96ee55..8ebfeede 100644 --- a/tools/check-instrumented-results.py +++ b/tools/check-instrumented-results.py @@ -14,9 +14,12 @@ Trois pièges que ce script évite -------------------------------- -1. **Une hypothèse non remplie est comptée comme un ÉCHEC dans le XML d'AGP**, pas comme un test - ignoré (mesuré le 2026-10-29 : `skipped="0"`, et les `AssumptionViolatedException` sous - ``). Il faut donc lire la cause de chaque échec ; le total ne dit rien. +1. **Une hypothèse non remplie n'a pas la même forme selon la version d'AGP.** Jusqu'à AGP 9.4, un + ÉCHEC (mesuré le 2026-10-29 : `skipped="0"`, et les `AssumptionViolatedException` sous + ``) ; depuis AGP 9.5, un `` dont le TEXTE porte l'exception et sa pile (mesuré + le 2026-11-09, PR #51 — ce script, qui ne lisait que ``, a alors jugé « sans raison » les + onze sauts admis). Les deux formes sont lues, et dans les deux la cause se lit ; le total ne dit + rien. Un `` sans hypothèse levée — un `@Ignore`, un saut sans raison — n'est jamais admis. 2. **Un test ignoré n'est pas un test vert** (`docs/04-PIEGES.md` §45). Seules les raisons de [RAISONS_ADMISES] passent ; toute autre hypothèse levée fait échouer le contrôle — une nouvelle raison s'ajoute ici, en revue, jamais par défaut. @@ -42,6 +45,7 @@ import sys import tempfile import xml.etree.ElementTree as ET +from xml.sax.saxutils import escape # 501 cas le 2026-10-29. Un plancher, pas un compte exact : retirer des tests le fait baisser ici, # dans le même commit. @@ -73,12 +77,26 @@ def lire(dossier: pathlib.Path) -> list[tuple[str, str, str | None]]: if echec is None: echec = element.find("error") texte = None if echec is None else (echec.get("message") or "") + "\n" + (echec.text or "") - if element.find("skipped") is not None: - texte = "AssumptionViolatedException: " + sautee = element.find("skipped") + if sautee is not None: + texte = raison_du_saut(sautee) cas.append((element.get("classname", ""), element.get("name", ""), texte)) return cas +def raison_du_saut(sautee: ET.Element) -> str: + """Pourquoi un test a été sauté, tel que le XML le dit — cf. le premier piège. + + AGP 9.5 met l'hypothèse levée, exception et pile, dans le texte du ``. Un saut qui n'en + porte pas n'a rien à juger : il est rendu sans le mot `AssumptionViolatedException`, donc compté + comme un échec, même si son texte cite une raison admise. + """ + raison = ((sautee.get("message") or "") + "\n" + (sautee.text or "")).strip() + if "AssumptionViolatedException" in raison: + return raison + return f" sans hypothèse levée : {raison or 'aucune raison'}" + + def juger(cas: list[tuple[str, str, str | None]]) -> list[str]: """Les défauts, vide si les résultats sont acceptables.""" defauts = [] @@ -116,13 +134,20 @@ def resumer(cas: list[tuple[str, str, str | None]]) -> str: # ── Contrôle négatif ──────────────────────────────────────────────────────────────────────────── +class Saut(str): + """Dans l'auto-contrôle, un cas écrit en `` (AGP 9.5) au lieu de `` (AGP 9.4).""" + + def _xml(cas: list[tuple[str, str, str | None]]) -> str: lignes = ['', ""] for classe, test, texte in cas: if texte is None: lignes.append(f'') else: - lignes.append(f'{texte}') + balise = "skipped" if isinstance(texte, Saut) else "failure" + lignes.append( + f'<{balise}>{escape(texte)}', + ) lignes.append("") return "\n".join(lignes) @@ -142,6 +167,20 @@ def auto_controle() -> int: False, ), "trop peu de tests": (ordinaires[:10] + [temoin_ok], False), + # AGP 9.5 : l'hypothèse dans le texte d'un , suivie de sa pile. + "AGP 9.5 : raison admise dans un ": ( + ordinaires + [temoin_ok, ("com.x.VoixTest", "v", Saut(admis[2] + "\n\tat org.junit.Assume.assumeTrue"))], + True, + ), + "AGP 9.5 : raison non admise dans un ": ( + ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut("org.junit.AssumptionViolatedException: autre"))], + False, + ), + "AGP 9.5 : vide": (ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut(""))], False), + "AGP 9.5 : @Ignore qui cite une raison admise": ( + ordinaires + [temoin_ok, ("com.x.ATest", "i", Saut("no biometric enrolled on this device"))], + False, + ), } rate = [] with tempfile.TemporaryDirectory() as dossier: