Merge commit '8bc2a33' into scratch/integrate-d2-d3

This commit is contained in:
2026-08-22 19:09:09 -05:00
@@ -0,0 +1,134 @@
package org.libremediaconverter.convert
import android.app.Application
import android.net.Uri
import androidx.media3.common.util.UnstableApi
import androidx.work.WorkManager
import androidx.work.workDataOf
import kotlinx.coroutines.Dispatchers
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.libremediaconverter.join.JoinState
import org.libremediaconverter.join.JoinViewModel
import org.libremediaconverter.model.InputProbe
import org.libremediaconverter.work.ConcatWorker
import org.libremediaconverter.work.ConversionWorker
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import java.io.File
/**
* Where the two staging fixes meet: a result nobody started is still a result somebody owns.
*
* Reattachment hands the user back a job this ViewModel did not start, which means the
* ViewModel inherits the staged file along with it — and "Start over" on that file has to
* delete it exactly as it would for a conversion run in this process. Neither fix implies
* the other: cleanup only reaches a file whose handle was recorded, and reattachment only
* records one because it goes through the same `observe()` the normal success path does.
* That is a structural claim about one function, which is precisely the kind that a merge
* resolving the two changes into different places would quietly break. Hence a test rather
* than a comment.
*
* These run a real `WorkManager` and a real `OutputPublisher` against a real `cacheDir`, so
* what is asserted at the end is the filesystem.
*/
@UnstableApi
@RunWith(RobolectricTestRunner::class)
class ReattachedCleanupTest {
private lateinit var app: Application
private lateinit var publisher: RecordingPublisher
private lateinit var workManager: WorkManager
private lateinit var staged: File
@Before
fun setUp() {
app = RuntimeEnvironment.getApplication()
publisher = RecordingPublisher(app)
ConversionDependencies.publisher = { publisher }
ConversionDependencies.probe = { _, _ -> InputProbe() }
staged = publisher.createStagingFile("holiday_converted.mp4").apply { writeBytes(ByteArray(4096)) }
installTestWorkManager(app, workDataOf(ConversionWorker.KEY_OUTPUT_PATH to staged.absolutePath))
workManager = WorkManager.getInstance(app)
}
@After
fun tearDown() {
ConversionDependencies.reset()
}
/**
* The defect end to end, on the JVM: work that finished with no ViewModel left to see it,
* and a ViewModel created afterwards that finds it anyway.
*/
@Test
fun `a conversion that finished before this ViewModel existed is picked up`() {
finishAConversionWithNobodyWatching()
val viewModel = ConversionViewModel(app, Dispatchers.Unconfined)
val converted = awaitState(viewModel.state, "Converted") { it is ConversionState.Converted }
converted as ConversionState.Converted
assertEquals(staged.absolutePath, converted.staged.absolutePath)
// The name came back through the job's tags — the only channel WorkManager returns,
// since WorkInfo never carries the Data a request was enqueued with.
assertEquals("holiday.mp4", converted.input.displayName)
assertEquals(4_096L, converted.input.sizeBytes)
}
@Test
fun `start over on a reattached conversion deletes the staged file`() {
finishAConversionWithNobodyWatching()
val viewModel = ConversionViewModel(app, Dispatchers.Unconfined)
awaitState(viewModel.state, "Converted") { it is ConversionState.Converted }
assertTrue("the reattached result should still be on disk", staged.exists())
viewModel.reset()
assertEquals(ConversionState.Idle, viewModel.state.value)
assertEquals(
"a reattached result is still this ViewModel's to discard",
listOf(staged),
publisher.discarded,
)
assertFalse("Start over must not leave a full-size copy in cache", staged.exists())
}
@Test
fun `start over on a reattached join deletes the staged file`() {
workManager.enqueue(
ConcatWorker.request(
inputs = listOf(Uri.parse("content://test/one.mp4"), Uri.parse("content://test/two.mp4")),
totalBytes = 8_192L,
),
).result.get()
val viewModel = JoinViewModel(app, Dispatchers.Unconfined)
awaitState(viewModel.state, "Joined") { it is JoinState.Joined }
viewModel.reset()
assertEquals(listOf(staged), publisher.discarded)
assertFalse(staged.exists())
}
/**
* A job from a process that is gone: enqueued and finished before any ViewModel exists, so
* nothing observed it and nothing recorded its output.
*/
private fun finishAConversionWithNobodyWatching() {
workManager.enqueue(
ConversionWorker.request(
inputUri = Uri.parse("content://test/holiday.mp4"),
displayName = "holiday.mp4",
sizeBytes = 4_096L,
),
).result.get()
}
}