Fehlerbehandlung
Robuste Behandlung aller bekannten Fehlerfälle. - Speichermangel behandelt - Bildfehler behandelt - PDF-Fehler behandelt - Datenverlust verhindert
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package de.medapp.wunddoku.domain.error
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Reine JVM-Tests für die Fehlereinordnung (Speichermangel erkennen) und
|
||||
* [runCatchingCancellable].
|
||||
*/
|
||||
class FailureCategoryTest {
|
||||
|
||||
// --- Arbeitsspeicher -----------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun out_of_memory_error_wird_erkannt() {
|
||||
assertEquals(FailureCategory.OUT_OF_MEMORY, FailureCategory.of(OutOfMemoryError("Failed to allocate")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun out_of_memory_error_wird_auch_als_ursache_erkannt() {
|
||||
val wrapped = RuntimeException("Export fehlgeschlagen", OutOfMemoryError())
|
||||
assertEquals(FailureCategory.OUT_OF_MEMORY, FailureCategory.of(wrapped))
|
||||
}
|
||||
|
||||
// --- Speicherplatz -------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun enospc_ioexception_wird_als_speicher_voll_erkannt() {
|
||||
val e = IOException("write failed: ENOSPC (No space left on device)")
|
||||
assertEquals(FailureCategory.OUT_OF_STORAGE, FailureCategory.of(e))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sqlite_disk_full_meldung_wird_als_speicher_voll_erkannt() {
|
||||
val e = RuntimeException("database or disk is full (code 13 SQLITE_FULL)")
|
||||
assertEquals(FailureCategory.OUT_OF_STORAGE, FailureCategory.of(e))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun erkennung_ist_unabhaengig_von_gross_und_kleinschreibung() {
|
||||
assertEquals(FailureCategory.OUT_OF_STORAGE, FailureCategory.of(IOException("NO SPACE LEFT ON DEVICE")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun speicher_voll_in_tiefer_ursachenkette_wird_erkannt() {
|
||||
val root = IOException("No space left on device")
|
||||
val chain = IllegalStateException("a", RuntimeException("b", RuntimeException("c", root)))
|
||||
assertEquals(FailureCategory.OUT_OF_STORAGE, FailureCategory.of(chain))
|
||||
}
|
||||
|
||||
// --- Sonstige Fehler -----------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun gewoehnliche_fehler_sind_other() {
|
||||
assertEquals(FailureCategory.OTHER, FailureCategory.of(FileNotFoundException("weg")))
|
||||
assertEquals(FailureCategory.OTHER, FailureCategory.of(IllegalStateException()))
|
||||
assertEquals(FailureCategory.OTHER, FailureCategory.of(RuntimeException(null as String?)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun zyklische_ursachenkette_fuehrt_nicht_zur_endlosschleife() {
|
||||
val a = RuntimeException("a")
|
||||
val b = RuntimeException("b", a)
|
||||
a.initCause(b)
|
||||
assertEquals(FailureCategory.OTHER, FailureCategory.of(a))
|
||||
}
|
||||
|
||||
// --- runCatchingCancellable ----------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun runCatchingCancellable_liefert_erfolg_und_fehler_als_result() {
|
||||
assertEquals(42, runCatchingCancellable { 42 }.getOrNull())
|
||||
val failure = runCatchingCancellable<Int> { throw IOException("x") }
|
||||
assertTrue(failure.exceptionOrNull() is IOException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runCatchingCancellable_reicht_coroutine_abbruch_weiter() {
|
||||
try {
|
||||
runCatchingCancellable<Unit> { throw CancellationException("abgebrochen") }
|
||||
fail("CancellationException darf nicht verschluckt werden")
|
||||
} catch (expected: CancellationException) {
|
||||
assertEquals("abgebrochen", expected.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package de.medapp.wunddoku.media
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Reine JVM-Tests für [ImageDownsampler.computeInSampleSize] - der Kern der speicherschonenden
|
||||
* Bilddekodierung (Schutz vor OutOfMemoryError bei großen Kamerafotos) und der Robustheit
|
||||
* gegenüber ungültigen Abmessungen defekter Bilddateien.
|
||||
*/
|
||||
class ImageDownsamplerTest {
|
||||
|
||||
private val target = ImageDownsampler.MAX_STORED_DIMENSION_PX
|
||||
|
||||
// --- ungültige / defekte Abmessungen -------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun ungueltige_abmessungen_defekter_bilder_liefern_1_statt_zu_werfen() {
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(0, 0, target))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(-1, -1, target))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(4000, 0, target))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(0, 4000, target))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(4000, 3000, 0))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(4000, 3000, -5))
|
||||
}
|
||||
|
||||
// --- Bilder, die nicht verkleinert werden müssen -------------------------------------
|
||||
|
||||
@Test
|
||||
fun kleine_bilder_werden_nicht_heruntergerechnet() {
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(100, 100, target))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(target, target, target))
|
||||
assertEquals(1, ImageDownsampler.computeInSampleSize(1, 1, target))
|
||||
}
|
||||
|
||||
// --- Große Bilder (Speichermangel vermeiden) -----------------------------------------
|
||||
|
||||
@Test
|
||||
fun grosse_kamerafotos_werden_deutlich_heruntergerechnet() {
|
||||
// 48 MP ≈ 8000 x 6000
|
||||
val sample = ImageDownsampler.computeInSampleSize(8000, 6000, target)
|
||||
assertTrue("erwartet >= 2, war $sample", sample >= 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ergebnis_ist_immer_eine_zweierpotenz_und_haelt_die_zielgroesse_ein() {
|
||||
val sizes = listOf(1, 2, 99, 100, 1999, 2000, 2001, 3999, 4000, 4001, 6000, 8000, 12000, 20000, 65535)
|
||||
for (width in sizes) {
|
||||
for (height in sizes) {
|
||||
val sample = ImageDownsampler.computeInSampleSize(width, height, target)
|
||||
val largest = maxOf(width, height)
|
||||
|
||||
assertTrue("Zweierpotenz erwartet bei ${width}x$height, war $sample", sample >= 1 && sample and (sample - 1) == 0)
|
||||
if (largest > target) {
|
||||
// Nie unter die Zielgröße dekodieren ...
|
||||
assertTrue("${width}x$height: dekodiert wäre ${largest / sample}", largest / sample >= target)
|
||||
// ... aber auch nie mehr als doppelt so groß wie nötig (kein Speicher-Verschwenden).
|
||||
assertTrue("${width}x$height: dekodiert wäre ${largest / sample}", largest / sample < 2 * target)
|
||||
} else {
|
||||
assertEquals(1, sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extrem_grosse_werte_laufen_nicht_ueber() {
|
||||
val sample = ImageDownsampler.computeInSampleSize(Int.MAX_VALUE, Int.MAX_VALUE, target)
|
||||
assertTrue(sample >= 1)
|
||||
assertTrue(sample and (sample - 1) == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun thumbnail_ziel_ist_kleiner_als_speicherziel() {
|
||||
val full = ImageDownsampler.computeInSampleSize(2000, 1500, target)
|
||||
val thumb = ImageDownsampler.computeInSampleSize(2000, 1500, ImageDownsampler.THUMBNAIL_MAX_DIMENSION_PX)
|
||||
assertTrue(thumb > full)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Reine JVM-Tests für die Fehlermeldungen des PDF-Bereichs: jede Fehlerart hat eine
|
||||
* verständliche deutsche Meldung, und technische Details (Pfade, Exception-Texte - können
|
||||
* Patientennamen enthalten) gelangen nie in die Anzeige.
|
||||
*/
|
||||
class ErrorMessagesTest {
|
||||
|
||||
private val sensitive = "/data/user/0/de.medapp.wunddoku/files/pdf/2026-08-19_Max_Mustermann_1980-03-14.pdf"
|
||||
|
||||
private val exportErrors: List<PdfExportError> = listOf(
|
||||
PdfExportError.DocumentationNotFound,
|
||||
PdfExportError.DirectoryCreationFailed(sensitive),
|
||||
PdfExportError.WriteFailed(sensitive),
|
||||
PdfExportError.Unexpected(sensitive),
|
||||
PdfExportError.OutOfMemory,
|
||||
PdfExportError.InsufficientStorage,
|
||||
)
|
||||
|
||||
private val openFailures: List<PdfOpenResult.Failure> = listOf(
|
||||
PdfOpenResult.Failure.FileNotFound,
|
||||
PdfOpenResult.Failure.UriResolutionFailed(sensitive),
|
||||
PdfOpenResult.Failure.NoViewerAvailable,
|
||||
PdfOpenResult.Failure.Unexpected(sensitive),
|
||||
)
|
||||
|
||||
private val shareFailures: List<PdfShareResult.Failure> = listOf(
|
||||
PdfShareResult.Failure.UriResolutionFailed(sensitive),
|
||||
PdfShareResult.Failure.NoAppAvailable,
|
||||
PdfShareResult.Failure.Unexpected(sensitive),
|
||||
)
|
||||
|
||||
private val imageShareFailures: List<PdfImageShareResult.Failure> = listOf(
|
||||
PdfImageShareResult.Failure.NoImages,
|
||||
PdfImageShareResult.Failure.UriResolutionFailed(sensitive),
|
||||
PdfImageShareResult.Failure.NoAppAvailable,
|
||||
PdfImageShareResult.Failure.Unexpected(sensitive),
|
||||
)
|
||||
|
||||
private val imageConversionFailures: List<PdfToImagesResult.Failure> = listOf(
|
||||
PdfToImagesResult.Failure.FileNotFound,
|
||||
PdfToImagesResult.Failure.EmptyDocument,
|
||||
PdfToImagesResult.Failure.Unexpected(sensitive),
|
||||
PdfToImagesResult.Failure.OutOfMemory,
|
||||
PdfToImagesResult.Failure.InsufficientStorage,
|
||||
)
|
||||
|
||||
private fun assertSafe(message: String) {
|
||||
assertTrue("Meldung darf nicht leer sein", message.isNotBlank())
|
||||
assertFalse("Meldung enthält Pfad/Patientendaten: $message", message.contains("Mustermann"))
|
||||
assertFalse("Meldung enthält Pfad: $message", message.contains("/data/"))
|
||||
assertFalse("Meldung enthält Dateiendung/Pfad: $message", message.contains(".pdf"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alle_pdf_export_fehler_haben_eine_sichere_meldung() {
|
||||
exportErrors.forEach { assertSafe(it.toUserMessage()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alle_oeffnen_fehler_haben_eine_sichere_meldung() {
|
||||
openFailures.forEach { assertSafe(it.toUserMessage()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alle_pdf_teilen_fehler_haben_eine_sichere_meldung() {
|
||||
shareFailures.forEach { assertSafe(it.toUserMessage()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alle_bilder_teilen_fehler_haben_eine_sichere_meldung() {
|
||||
imageShareFailures.forEach { assertSafe(it.toUserMessage()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alle_bildkonvertierungs_fehler_haben_eine_sichere_meldung() {
|
||||
imageConversionFailures.forEach { assertSafe(it.toUserMessage()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun speichermangel_hat_eine_eigene_aussagekraeftige_meldung() {
|
||||
assertNotEquals(PdfExportError.OutOfMemory.toUserMessage(), PdfExportError.InsufficientStorage.toUserMessage())
|
||||
assertNotEquals(
|
||||
PdfExportError.OutOfMemory.toUserMessage(),
|
||||
PdfExportError.Unexpected("x").toUserMessage(),
|
||||
)
|
||||
assertNotEquals(
|
||||
PdfToImagesResult.Failure.OutOfMemory.toUserMessage(),
|
||||
PdfToImagesResult.Failure.InsufficientStorage.toUserMessage(),
|
||||
)
|
||||
assertTrue(PdfExportError.InsufficientStorage.toUserMessage().contains("Speicher"))
|
||||
assertTrue(PdfExportError.OutOfMemory.toUserMessage().contains("Arbeitsspeicher"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unterschiedliche_fehlerarten_erhalten_unterschiedliche_meldungen() {
|
||||
val messages = exportErrors.map { it.toUserMessage() }
|
||||
assertTrue(messages.toSet().size == messages.size)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user