Tests de régression : données réelles hors dépôt (local-test-data gitignoré)
- RegressionUserCase{,2}Test chargent le backup réel depuis local-test-data/
(gitignoré) et sont IGNORÉS proprement (Assume) si le fichier est absent —
plus aucune donnée de santé personnelle embarquée dans le dépôt
- KDoc LabDialog : exemple 300.0 (au lieu d'une valeur réelle)
- script d'anonymisation /tmp/scrub.py prêt pour filter-branch (historique)
This commit is contained in:
parent
fdd3fee8f6
commit
851c185f69
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,3 +28,4 @@ Thumbs.db
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
local-test-data/
|
||||||
|
|||||||
@ -216,7 +216,7 @@ fun LabDialog(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatage d'une valeur de labo sans perdre de décimales significatives :
|
* Formatage d'une valeur de labo sans perdre de décimales significatives :
|
||||||
* 300.0 → "306", 0.45 → "0.45", 1.235 → "1.235" (contrairement à formatDose
|
* 300.0 → "300", 0.45 → "0.45", 1.235 → "1.235" (contrairement à formatDose
|
||||||
* qui arrondit à 1 décimale, pensé pour des doses en mg).
|
* qui arrondit à 1 décimale, pensé pour des doses en mg).
|
||||||
*/
|
*/
|
||||||
fun formatLabValue(v: Double): String =
|
fun formatLabValue(v: Double): String =
|
||||||
|
|||||||
@ -4,59 +4,43 @@ import com.google.gson.Gson
|
|||||||
import com.hormonetrack.data.backup.BackupData
|
import com.hormonetrack.data.backup.BackupData
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Assume.assumeTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Régression épinglée sur le **2ᵉ export réel** de l'utilisatrice (v1.2.0, 2026-09-05) :
|
* Régression épinglée sur le **2ᵉ export réel** de l'utilisatrice (v1.2.0) :
|
||||||
* 1 traitement EEn/ESE→**TFS** 5 mg (facteur stocké 0,72, fréquence 6 j), 9 doses
|
* 1 traitement EEn + TFS 5 mg (facteur stocké, fréquence 6 j), 9 doses tous les
|
||||||
* (~6-7 j d'écart, une à 8 mg), 8 labs (4× E2 pg/mL, T en ng/dL + **1 labo T saisi
|
* 6–7 j, 8 labs (E2 pg/mL, T ng/dL dont un saisi avec une mauvaise unité).
|
||||||
* par erreur en "pg/mL"** — signalé comme faute de frappe par l'utilisatrice, gardé
|
|
||||||
* tel quel dans le test pour vérifier qu'il ne casse plus l'axe T).
|
|
||||||
*
|
*
|
||||||
* Sert à valider v1.2.1 : courbe Home plausible, calibration PAR PÉRIODE D'ESTER
|
* ⚠️ Données de santé personnelles : le JSON réel vit HORS du dépôt dans
|
||||||
* (tout est sous EEn ici), lab T en unité aberrante neutralisé par la conversion.
|
* `local-test-data/backup-v1.2.0.json` (gitignoré) ; tests **ignorés** si absent.
|
||||||
|
*
|
||||||
|
* Valide v1.2.1+ : courbe Home en **état d'équilibre EEn** (t½ ≈ 6,7 j pour des
|
||||||
|
* doses tous les 6–7 j → accumulation ×2), calibration PAR PÉRIODE D'ESTER,
|
||||||
|
* lab T en unité aberrante neutralisé, prévision à 6 j.
|
||||||
*/
|
*/
|
||||||
class RegressionUserCase2Test {
|
class RegressionUserCase2Test {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val USER_JSON = """
|
/**
|
||||||
{"doseLogs":[
|
* Le workdir des tests unitaires Gradle est le dossier du module (app/) :
|
||||||
{"doseAmount":5.0,"id":11,"timestamp":1786104300000,"treatmentId":1},
|
* on cherche donc les données locales à plusieurs emplacements.
|
||||||
{"doseAmount":5.0,"id":10,"timestamp":1786721520000,"treatmentId":1},
|
*/
|
||||||
{"doseAmount":5.0,"id":9,"timestamp":1787337600000,"treatmentId":1},
|
private val DATA_FILE: File = listOf(
|
||||||
{"doseAmount":5.0,"id":8,"timestamp":1787911680000,"treatmentId":1},
|
File("../local-test-data/backup-v1.2.0.json"),
|
||||||
{"doseAmount":8.0,"id":7,"timestamp":1788583980000,"treatmentId":1},
|
File("local-test-data/backup-v1.2.0.json"),
|
||||||
{"doseAmount":5.0,"id":6,"timestamp":1789193820000,"treatmentId":1},
|
File("app/local-test-data/backup-v1.2.0.json")
|
||||||
{"doseAmount":5.0,"id":5,"timestamp":1789725060000,"treatmentId":1},
|
).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.2.0.json")
|
||||||
{"doseAmount":5.0,"id":4,"timestamp":1790264700000,"treatmentId":1},
|
|
||||||
{"doseAmount":5.0,"id":3,"timestamp":1790766120000,"treatmentId":1}],
|
|
||||||
"exportedAt":1791213894346,
|
|
||||||
"labResults":[
|
|
||||||
{"id":7,"marker":"E2","timestamp":1786704060000,"unit":"pg/mL","value":200.0},
|
|
||||||
{"id":8,"marker":"T","timestamp":1786704060000,"unit":"pg/mL","value":40.0},
|
|
||||||
{"id":5,"marker":"E2","timestamp":1788503580000,"unit":"pg/mL","value":175.0},
|
|
||||||
{"id":6,"marker":"T","timestamp":1788503580000,"unit":"ng/dL","value":47.0},
|
|
||||||
{"id":3,"marker":"E2","timestamp":1789028820000,"unit":"pg/mL","value":300.0},
|
|
||||||
{"id":4,"marker":"T","timestamp":1789028820000,"unit":"ng/dL","value":33.0},
|
|
||||||
{"id":1,"marker":"E2","timestamp":1790242440000,"unit":"pg/mL","value":250.0},
|
|
||||||
{"id":2,"marker":"T","timestamp":1790242440000,"unit":"ng/dL","value":44.0}],
|
|
||||||
"tConfig":{"base":6.0,"floor":0.2,"k":0.09},
|
|
||||||
"treatments":[{
|
|
||||||
"absorptionHours":152.0,"bioavailabilityFraction":1.0,"createdAt":1791206539840,
|
|
||||||
"doseAmount":5.0,"doseUnit":"mg","eliminationHalfLifeHours":150.0,"esterType":"EEN",
|
|
||||||
"forecastIntervalDays":6.0,"id":1,"isActive":true,"name":"Injection EEn 5mg 6d",
|
|
||||||
"pkModel":"TFS","reminderEnabled":false,"route":"INJECTION_IM","scaleFactor":0.72,
|
|
||||||
"type":"ESTRADIOL"}],
|
|
||||||
"version":1}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
private const val EXPORT_TIME = 1_791_213_894_346L
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
assumeTrue(
|
||||||
|
"local-test-data/backup-v1.2.0.json absent — tests ignorés (données locales)",
|
||||||
|
DATA_FILE.exists()
|
||||||
|
)
|
||||||
if (!PKProfileStore.hasProfile("EEN", "TFS")) {
|
if (!PKProfileStore.hasProfile("EEN", "TFS")) {
|
||||||
val file = listOf(
|
val file = listOf(
|
||||||
File("src/main/assets/pk_profiles.json"),
|
File("src/main/assets/pk_profiles.json"),
|
||||||
@ -67,7 +51,7 @@ class RegressionUserCase2Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importUserBackup(): BackupData =
|
private fun importUserBackup(): BackupData =
|
||||||
Gson().fromJson(USER_JSON, BackupData::class.java)
|
Gson().fromJson(DATA_FILE.readText(), BackupData::class.java)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `second user backup parses with forecast interval and stored scale factor`() {
|
fun `second user backup parses with forecast interval and stored scale factor`() {
|
||||||
@ -77,66 +61,58 @@ class RegressionUserCase2Test {
|
|||||||
val tr = data.treatments.single()
|
val tr = data.treatments.single()
|
||||||
assertEquals("TFS", tr.pkModel)
|
assertEquals("TFS", tr.pkModel)
|
||||||
assertEquals("EEN", tr.esterType)
|
assertEquals("EEN", tr.esterType)
|
||||||
assertEquals(0.72, tr.scaleFactor, 1e-9)
|
|
||||||
assertEquals(6.0, tr.forecastIntervalDays!!, 1e-9)
|
assertEquals(6.0, tr.forecastIntervalDays!!, 1e-9)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home 24h estimate is plausible with the stored scale factor`() {
|
fun `home 24h estimate is plausible in steady state with the stored scale factor`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
val curve = PharmacokineticEngine.computeCurve(
|
val curve = PharmacokineticEngine.computeCurve(
|
||||||
data.treatments, data.doseLogs,
|
data.treatments, data.doseLogs,
|
||||||
startMs = EXPORT_TIME - 24 * PharmacokineticEngine.HOUR_MS,
|
startMs = data.exportedAt - 24 * PharmacokineticEngine.HOUR_MS,
|
||||||
endMs = EXPORT_TIME,
|
endMs = data.exportedAt,
|
||||||
tConfig = data.tConfig
|
tConfig = data.tConfig
|
||||||
)
|
)
|
||||||
assertTrue(curve.isNotEmpty())
|
assertTrue(curve.isNotEmpty())
|
||||||
val last = curve.last()
|
val last = curve.last()
|
||||||
// ÉTAT D'ÉQUILIBRE enanthate : t½ ≈ 6,7 j pour des doses tous les 6–7 j
|
// État d'équilibre EEn : t½ ≈ 6,7 j, doses tous les 6–7 j → accumulation ×2 ;
|
||||||
// → accumulation ≈ ×2 (superposition des 9 doses). 5 mg × ~65 pg/mL/mg
|
// avec le facteur stocké (~0,7) : ~240–280 pg/mL, cohérent avec les labs
|
||||||
// (somme) × 0,72 ≈ 240–280 pg/mL — cohérent avec les labs de l'utilisatrice
|
|
||||||
// (300, 250). Sans calibration : 268/0,72 ≈ 365 = les « 370 » rapportés.
|
|
||||||
assertTrue("e2=${last.e2}", last.e2 in 200.0..350.0)
|
assertTrue("e2=${last.e2}", last.e2 in 200.0..350.0)
|
||||||
// k = 0,09 (calibré par l'utilisatrice) → T ≈ 0,43 ng/mL à e2 ≈ 268
|
|
||||||
assertTrue("t=${last.t}", last.t in 0.3..0.55)
|
assertTrue("t=${last.t}", last.t in 0.3..0.55)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `per-ester calibration produces a single EEN factor from the labs of that period`() {
|
fun `per-ester calibration produces a single factor from the labs of that period`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
val scales = PharmacokineticEngine.computeEsterScaleFactors(
|
val scales = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
data.treatments, data.doseLogs,
|
data.treatments, data.doseLogs,
|
||||||
data.labResults.filter { it.marker.equals("E2", true) }
|
data.labResults.filter { it.marker.equals("E2", true) }
|
||||||
)
|
)
|
||||||
// Toutes les doses sont EEn → un seul ester calibré, facteur plausible
|
// Toutes les doses sont du même ester → un seul facteur calibré
|
||||||
assertTrue("esters=${scales.keys}", scales.keys == setOf("EEN"))
|
assertTrue("esters=${scales.keys}", scales.keys == setOf("EEN"))
|
||||||
val sf = scales["EEN"]!!
|
assertTrue("sf=${scales["EEN"]}", scales["EEN"]!! in 0.2..2.0)
|
||||||
assertTrue("sf=$sf devrait être plausible (0,2–2)", sf in 0.2..2.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `mistyped T lab unit does not destroy the T axis`() {
|
fun `mistyped T lab unit does not destroy the T axis`() {
|
||||||
// Le lab T "40 pg/mL" (faute de frappe) doit être converti, pas renvoyé brut
|
// Un lab T avec une unité aberrante (pg/mL) doit être converti, pas brut
|
||||||
assertEquals(0.038, PharmacokineticEngine.convertTToNgMl(40.0, "pg/mL"), 1e-9)
|
assertEquals(0.04, PharmacokineticEngine.convertTToNgMl(40.0, "pg/mL"), 1e-9)
|
||||||
// Et un axe de chart restera lisible : max(T estimé ≈ 0,7, lab converti 0,038)
|
|
||||||
val tDataMax = maxOf(0.7, 0.038)
|
|
||||||
assertTrue(tDataMax < 1.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `forecast doses follow the 6 day interval configured by the user`() {
|
fun `forecast doses follow the configured interval`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
|
val intervalDays = data.treatments.single().forecastIntervalDays!!
|
||||||
val forecast = PharmacokineticEngine.generateForecastDoses(
|
val forecast = PharmacokineticEngine.generateForecastDoses(
|
||||||
data.treatments.single(), data.doseLogs,
|
data.treatments.single(), data.doseLogs,
|
||||||
toMs = EXPORT_TIME + 30 * 3_600_000L * 24,
|
toMs = data.exportedAt + 30 * 24 * 3_600_000L,
|
||||||
nowMs = EXPORT_TIME
|
nowMs = data.exportedAt
|
||||||
)
|
)
|
||||||
// 1ʳᵉ dose projetée = dernière réelle (Aug 31) + 6 j ; toutes espacées de 6 j
|
|
||||||
val lastReal = data.doseLogs.maxOf { it.timestamp }
|
val lastReal = data.doseLogs.maxOf { it.timestamp }
|
||||||
assertTrue(forecast.isNotEmpty())
|
assertTrue(forecast.isNotEmpty())
|
||||||
assertEquals(lastReal + 6L * 24 * 3_600_000L, forecast.first().timestamp)
|
assertEquals(lastReal + (intervalDays * 24 * 3_600_000L).toLong(), forecast.first().timestamp)
|
||||||
forecast.zipWithNext { prev, cur ->
|
forecast.zipWithNext { prev, cur ->
|
||||||
assertEquals(6L * 24 * 3_600_000L, cur.timestamp - prev.timestamp)
|
assertEquals((intervalDays * 24 * 3_600_000L).toLong(), cur.timestamp - prev.timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,11 +123,11 @@ class RegressionUserCase2Test {
|
|||||||
data.treatments, data.doseLogs, data.labResults, data.tConfig
|
data.treatments, data.doseLogs, data.labResults, data.tConfig
|
||||||
)
|
)
|
||||||
assertTrue(result.calibratedEsters == 1)
|
assertTrue(result.calibratedEsters == 1)
|
||||||
assertTrue(result.esterScales["EEN"]!! in 0.2..2.0)
|
assertTrue(result.esterScales.values.single() in 0.2..2.0)
|
||||||
// Tout est sous EEn → le k T recalibré vit dans la map par ester ;
|
// Un seul ester → le k T recalibré vit dans la map par ester ;
|
||||||
// le k stocké (0,09, calibré par l'utilisatrice) reste le fallback
|
// le k stocké reste le fallback (inchangé dans les réglages)
|
||||||
assertTrue("k=${result.tKPerEster["EEN"]}", result.tKPerEster["EEN"]!! in 0.01..1.0)
|
|
||||||
assertTrue(result.tRecalibrated)
|
assertTrue(result.tRecalibrated)
|
||||||
assertEquals(0.09, result.tConfig.k, 1e-9)
|
assertTrue(result.tKPerEster.values.single() in 0.01..1.0)
|
||||||
|
assertEquals(data.tConfig.k, result.tConfig.k, 1e-9)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,44 +3,41 @@ package com.hormonetrack.pk
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.hormonetrack.data.backup.BackupData
|
import com.hormonetrack.data.backup.BackupData
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Assume.assumeTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regression test pinned to the user's real exported data (v1.0.0 backup JSON):
|
* Régression épinglée sur le **1er export réel** de l'utilisatrice (v1.0.0).
|
||||||
* 1 EEn injection 5 mg (~5 days before export), 4 labs (2× E2 pg/mL, 2× T ng/dL),
|
*
|
||||||
* default T config. Original bug report: "charts don't generate".
|
* ⚠️ Données de santé personnelles : le JSON réel vit HORS du dépôt dans
|
||||||
|
* `local-test-data/backup-v1.0.0.json` (gitignoré). Si le fichier est absent
|
||||||
|
* (clone neuf, CI), les tests de cette classe sont **ignorés** — jamais échoués.
|
||||||
|
* Pour les lancer : exporter son backup depuis l'app et le déposer à cet emplacement.
|
||||||
*/
|
*/
|
||||||
class RegressionUserCaseTest {
|
class RegressionUserCaseTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Exact export provided by the user (formatted for readability)
|
/**
|
||||||
private val USER_JSON = """
|
* Le workdir des tests unitaires Gradle est le dossier du module (app/) :
|
||||||
{"doseLogs":[{"doseAmount":5.0,"id":3,"timestamp":1790766120000,"treatmentId":1}],
|
* on cherche donc les données locales à plusieurs emplacements.
|
||||||
"exportedAt":1791207257381,
|
*/
|
||||||
"labResults":[
|
private val DATA_FILE: File = listOf(
|
||||||
{"id":3,"marker":"E2","timestamp":1789028820000,"unit":"pg/mL","value":300.0},
|
File("../local-test-data/backup-v1.0.0.json"),
|
||||||
{"id":4,"marker":"T","timestamp":1789028820000,"unit":"ng/dL","value":33.0},
|
File("local-test-data/backup-v1.0.0.json"),
|
||||||
{"id":1,"marker":"E2","timestamp":1790242440000,"unit":"pg/mL","value":250.0},
|
File("app/local-test-data/backup-v1.0.0.json")
|
||||||
{"id":2,"marker":"T","timestamp":1790242440000,"unit":"ng/dL","value":44.0}],
|
).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.0.0.json")
|
||||||
"tConfig":{"base":6.0,"floor":0.2,"k":0.19},
|
|
||||||
"treatments":[{
|
|
||||||
"absorptionHours":152.0,"bioavailabilityFraction":1.0,"createdAt":1791206539840,
|
|
||||||
"doseAmount":5.0,"doseUnit":"mg","eliminationHalfLifeHours":150.0,"esterType":"EEN",
|
|
||||||
"id":1,"isActive":true,"name":"Injection EEn — Estrannaise","pkModel":"ESE",
|
|
||||||
"reminderEnabled":false,"route":"INJECTION_IM","scaleFactor":1.0,"type":"ESTRADIOL"}],
|
|
||||||
"version":1}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
private const val EXPORT_TIME = 1_791_207_257_381L
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
assumeTrue(
|
||||||
|
"local-test-data/backup-v1.0.0.json absent — tests ignorés (données locales)",
|
||||||
|
DATA_FILE.exists()
|
||||||
|
)
|
||||||
if (!PKProfileStore.hasProfile("EEN", "ESE")) {
|
if (!PKProfileStore.hasProfile("EEN", "ESE")) {
|
||||||
val file = listOf(
|
val file = listOf(
|
||||||
File("src/main/assets/pk_profiles.json"),
|
File("src/main/assets/pk_profiles.json"),
|
||||||
@ -51,38 +48,32 @@ class RegressionUserCaseTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importUserBackup(): BackupData =
|
private fun importUserBackup(): BackupData =
|
||||||
Gson().fromJson(USER_JSON, BackupData::class.java)
|
Gson().fromJson(DATA_FILE.readText(), BackupData::class.java)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `user backup JSON parses into the expected data`() {
|
fun `user backup parses into the expected data`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
assertEquals(1, data.treatments.size)
|
assertEquals(1, data.treatments.size)
|
||||||
assertEquals(1, data.doseLogs.size)
|
assertEquals(1, data.doseLogs.size)
|
||||||
assertEquals(4, data.labResults.size)
|
assertEquals(4, data.labResults.size)
|
||||||
assertEquals("EEN", data.treatments[0].esterType)
|
assertEquals("EEN", data.treatments[0].esterType)
|
||||||
assertEquals("ESE", data.treatments[0].pkModel)
|
assertEquals("ESE", data.treatments[0].pkModel)
|
||||||
assertEquals(1790766120000, data.doseLogs[0].timestamp)
|
assertEquals(4, data.labResults.size)
|
||||||
|
assertEquals(2, data.labResults.count { it.marker.equals("E2", true) })
|
||||||
|
assertEquals(2, data.labResults.count { it.marker.equals("T", true) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `charts generate for every range with the user data`() {
|
fun `charts generate for every range with the user data`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
val tConfig = data.tConfig
|
|
||||||
for (rangeHours in listOf(24L, 24L * 7, 24L * 30)) {
|
for (rangeHours in listOf(24L, 24L * 7, 24L * 30)) {
|
||||||
val curve = PharmacokineticEngine.computeCurve(
|
val curve = PharmacokineticEngine.computeCurve(
|
||||||
data.treatments, data.doseLogs,
|
data.treatments, data.doseLogs,
|
||||||
startMs = EXPORT_TIME - rangeHours * PharmacokineticEngine.HOUR_MS,
|
startMs = data.exportedAt - rangeHours * PharmacokineticEngine.HOUR_MS,
|
||||||
endMs = EXPORT_TIME,
|
endMs = data.exportedAt,
|
||||||
tConfig = tConfig
|
tConfig = data.tConfig
|
||||||
)
|
)
|
||||||
assertTrue("24h range=$rangeHours curve must not be empty", curve.isNotEmpty())
|
assertTrue("rangeHours=$rangeHours curve must not be empty", curve.isNotEmpty())
|
||||||
// grid is hour-aligned: the last point may land up to 1 h before endMs
|
|
||||||
assertTrue(
|
|
||||||
"last=${curve.last().timestamp} rangeHours=$rangeHours",
|
|
||||||
curve.last().timestamp in (EXPORT_TIME - rangeHours * PharmacokineticEngine.HOUR_MS)..EXPORT_TIME
|
|
||||||
)
|
|
||||||
// E2 must be positive and in a physiologically plausible band for
|
|
||||||
// 5 mg EEn at ~120 h (peak 31.4 pg/mL/mg around 152 h)
|
|
||||||
val last = curve.last()
|
val last = curve.last()
|
||||||
assertTrue("e2=${last.e2} at rangeHours=$rangeHours", last.e2 > 50.0 && last.e2 < 400.0)
|
assertTrue("e2=${last.e2} at rangeHours=$rangeHours", last.e2 > 50.0 && last.e2 < 400.0)
|
||||||
assertTrue("t=${last.t}", last.t > 0.0 && last.t < 1.0)
|
assertTrue("t=${last.t}", last.t > 0.0 && last.t < 1.0)
|
||||||
@ -92,8 +83,9 @@ class RegressionUserCaseTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `current level at export time is plausible`() {
|
fun `current level at export time is plausible`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
val point = PharmacokineticEngine.levelAt(data.treatments, data.doseLogs, EXPORT_TIME, data.tConfig)
|
val point = PharmacokineticEngine.levelAt(
|
||||||
// ~121 h after 5 mg EEn: profile ≈ 27-30 pg/mL/mg → 135-150 pg/mL
|
data.treatments, data.doseLogs, data.exportedAt, data.tConfig
|
||||||
|
)
|
||||||
assertTrue("e2=${point.e2}", point.e2 in 100.0..200.0)
|
assertTrue("e2=${point.e2}", point.e2 in 100.0..200.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,28 +101,29 @@ class RegressionUserCaseTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `T labs in ng per dL convert to ng per mL`() {
|
fun `T labs in ng per dL convert to ng per mL`() {
|
||||||
assertEquals(0.45, PharmacokineticEngine.convertTToNgMl(44.0, "ng/dL"), 1e-9)
|
// Constantes choisies idempotentes face au script d'anonymisation d'historique
|
||||||
assertEquals(0.32, PharmacokineticEngine.convertTToNgMl(33.0, "ng/dL"), 1e-9)
|
assertEquals(0.44, PharmacokineticEngine.convertTToNgMl(44.0, "ng/dL"), 1e-9)
|
||||||
|
assertEquals(0.33, PharmacokineticEngine.convertTToNgMl(33.0, "ng/dL"), 1e-9)
|
||||||
assertEquals(0.45, PharmacokineticEngine.convertTToNgMl(0.45, "ng/mL"), 1e-9)
|
assertEquals(0.45, PharmacokineticEngine.convertTToNgMl(0.45, "ng/mL"), 1e-9)
|
||||||
assertEquals(0.45, PharmacokineticEngine.convertTToNgMl(450.0, "ng/L"), 1e-9)
|
assertEquals(4.5, PharmacokineticEngine.convertTToNgMl(4500.0, "ng/L"), 1e-9)
|
||||||
assertEquals(0.45, PharmacokineticEngine.convertTToNgMl(0.45, ""), 1e-9)
|
assertEquals(0.45, PharmacokineticEngine.convertTToNgMl(0.45, ""), 1e-9)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `T calibration works with ng per dL labs`() {
|
fun `T calibration works with ng per dL labs`() {
|
||||||
val data = importUserBackup()
|
val data = importUserBackup()
|
||||||
// synthetic post-dose T labs expressed in ng/dL
|
// Lab T synthétique post-dose exprimé en ng/dL, k planté à 0,25
|
||||||
val cfg = data.tConfig
|
val cfg = data.tConfig
|
||||||
val e2 = PharmacokineticEngine.e2At(data.treatments, data.doseLogs, EXPORT_TIME)
|
val e2 = PharmacokineticEngine.e2At(data.treatments, data.doseLogs, data.exportedAt)
|
||||||
val trueK = 0.25
|
val trueK = 0.25
|
||||||
val tNgMl = cfg.floor + (cfg.base - cfg.floor) / (1.0 + trueK * e2)
|
val tNgMl = cfg.floor + (cfg.base - cfg.floor) / (1.0 + trueK * e2)
|
||||||
val lab = com.hormonetrack.data.model.LabResult(
|
val lab = com.hormonetrack.data.model.LabResult(
|
||||||
marker = "T", value = tNgMl * 100.0, unit = "ng/dL", timestamp = EXPORT_TIME
|
marker = "T", value = tNgMl * 100.0, unit = "ng/dL", timestamp = data.exportedAt
|
||||||
)
|
)
|
||||||
val calibrated = PharmacokineticEngine.computeTConfigCalibration(
|
val calibrated = PharmacokineticEngine.computeTConfigCalibration(
|
||||||
listOf(lab), data.treatments, data.doseLogs, cfg
|
listOf(lab), data.treatments, data.doseLogs, cfg
|
||||||
)
|
)
|
||||||
assertNotNull(calibrated)
|
assertTrue(calibrated != null)
|
||||||
assertEquals(trueK, calibrated!!.k, trueK * 0.15)
|
assertEquals(trueK, calibrated!!.k, trueK * 0.15)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user