Add a seam for forcing failure paths, and force thirteen of them
Error handling was the least-tested code in the app. It only runs when something goes wrong, which is exactly what a healthy test run avoids, so the branches a user meets on a bad day were the ones that had never executed. An audit put it at 4 of 18 fallback branches actually forced by a test. There was also no mechanism to do better: ConversionWorker constructed Media3Engine and FFmpegEngine directly, so no test could make either of them fail. Media3Engine and FFmpegEngine now implement HardwareTranscoder and SoftwareTranscoder, and ConversionDependencies holds the factories. Workers are built by WorkManager and the app deliberately carries no DI framework, so a small settable holder is the least machinery that does the job. The foreground-service timeout needed different treatment. Its trigger is the six-hour-per-day budget expiring, which no test can reach, so the decision moved out of the worker into FailureOutcome. The rule is now verified on the JVM across every WorkInfo stop reason, including that only the timeout earns a retry -- retrying a user cancellation would ignore the user, and retrying a constraint failure would spin. Thirteen branches are now forced, including both free-space prechecks, both engines failing, a missing input, and the router bypassing hardware entirely for MP3. The dynamic fallback is covered twice over: once by injection, and once by HardwareFallbackTest driving it with genuinely undecodable 4:4:4 footage. Injection alone would prove the plumbing without proving the condition ever arises in reality. Five remain unforced and are listed in ConversionDependencies' documentation. They need a SAF grant to be revoked mid-job, which is not something a test can arrange. Media3EngineTest now asserts against the muxed file rather than the engine's own ExportResult, which is a stronger check anyway: a result object can report success for a file that will not play. 35 instrumented tests on a Pixel 10 Pro XL and 66 unit tests, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package dev.jasonmross.mediaconverter.work
|
||||
|
||||
import androidx.work.WorkInfo
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The retry-versus-fail rule.
|
||||
*
|
||||
* Isolated from the worker precisely so it can be tested: the condition that triggers
|
||||
* a retry in production is the foreground-service budget running out, six hours per
|
||||
* twenty-four, which no test can reach. Extracting the decision means the rule is still
|
||||
* verified even though its trigger cannot be reproduced.
|
||||
*/
|
||||
class FailureOutcomeTest {
|
||||
|
||||
@Test
|
||||
fun `a foreground service timeout is a retry, not a failure`() {
|
||||
// The work is still valid; there is simply no budget right now. Telling the
|
||||
// user their conversion failed would be wrong.
|
||||
assertEquals(
|
||||
FailureOutcome.RETRY,
|
||||
FailureOutcome.forStopReason(WorkInfo.STOP_REASON_FOREGROUND_SERVICE_TIMEOUT),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an ordinary failure is reported as a failure`() {
|
||||
assertEquals(
|
||||
FailureOutcome.FAIL,
|
||||
FailureOutcome.forStopReason(WorkInfo.STOP_REASON_NOT_STOPPED),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every other stop reason fails rather than retrying forever`() {
|
||||
// Retrying on, say, a user cancellation or a battery constraint would either
|
||||
// ignore the user or spin. Only the timeout earns a retry.
|
||||
val others = listOf(
|
||||
WorkInfo.STOP_REASON_CANCELLED_BY_APP,
|
||||
WorkInfo.STOP_REASON_USER,
|
||||
WorkInfo.STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW,
|
||||
WorkInfo.STOP_REASON_CONSTRAINT_CHARGING,
|
||||
WorkInfo.STOP_REASON_CONSTRAINT_CONNECTIVITY,
|
||||
WorkInfo.STOP_REASON_CONSTRAINT_DEVICE_IDLE,
|
||||
WorkInfo.STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW,
|
||||
WorkInfo.STOP_REASON_DEVICE_STATE,
|
||||
WorkInfo.STOP_REASON_QUOTA,
|
||||
WorkInfo.STOP_REASON_BACKGROUND_RESTRICTION,
|
||||
WorkInfo.STOP_REASON_APP_STANDBY,
|
||||
WorkInfo.STOP_REASON_TIMEOUT,
|
||||
WorkInfo.STOP_REASON_UNKNOWN,
|
||||
)
|
||||
others.forEach {
|
||||
assertEquals("stop reason $it should fail", FailureOutcome.FAIL, FailureOutcome.forStopReason(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user