Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229package social.grain
import androidx.camera.core.SurfaceRequestimport androidx.lifecycle.Lifecycleimport androidx.lifecycle.LifecycleOwnerimport androidx.lifecycle.LifecycleRegistryimport androidx.test.ext.junit.runners.AndroidJUnit4import kotlinx.coroutines.test.runTestimport org.junit.Assert.assertEqualsimport org.junit.Assert.assertFalseimport org.junit.Assert.assertNotNullimport org.junit.Assert.assertNullimport org.junit.Assert.assertTrueimport org.junit.Ruleimport org.junit.Testimport org.junit.rules.TemporaryFolderimport org.junit.runner.RunWithimport social.grain.feature.stories.CameraBackendimport social.grain.feature.stories.CameraStatusimport social.grain.feature.stories.Lensimport social.grain.feature.stories.OpenedCameraimport social.grain.feature.stories.StoryCameraimport social.grain.feature.stories.labelSummaryimport social.grain.feature.stories.pruneStaleCapturesimport java.io.File
/** * The story camera's state machine, driven through a fake backend. * * What these pin is that every path ends in a state the capture stage knows * how to draw, that nothing tries to shoot from the wrong one, and that a * lens switch which fails leaves the previous lens up rather than a dark * viewfinder. */@RunWith(AndroidJUnit4::class)class StoryCameraTest {
@get:Rule val folder = TemporaryFolder()
private val owner = object : LifecycleOwner { override val lifecycle = LifecycleRegistry.createUnsafe(this).apply { currentState = Lifecycle.State.RESUMED } }
/** A device with the lenses in [lenses]; the front one has no flash, as on most phones. */ private class FakeBackend( private val lenses: Set<Lens> = setOf(Lens.Back, Lens.Front), private val failCapture: Boolean = false, ) : CameraBackend { val opened = mutableListOf<Lens>() var closed = 0 var lastFlash: Boolean? = null
override suspend fun open( owner: LifecycleOwner, lens: Lens, onSurface: (SurfaceRequest) -> Unit, ): OpenedCamera? { if (lens !in lenses) return null opened += lens return object : OpenedCamera { override val hasFlash = lens == Lens.Back override suspend fun capture(flash: Boolean, into: File) { if (failCapture) error("shutter jammed") lastFlash = flash into.writeBytes(byteArrayOf(1, 2, 3)) } override fun close() { closed += 1 } } } }
private fun camera( backend: CameraBackend = FakeBackend(), access: Boolean = true, ) = StoryCamera(backend, requestAccess = { access }, captureDir = folder.root)
@Test fun deniedAccessLeavesTheCameraUnauthorized() = runTest { val backend = FakeBackend() val camera = camera(backend, access = false)
camera.start(owner)
assertEquals(CameraStatus.Unauthorized, camera.status) assertTrue("nothing should be bound without permission", backend.opened.isEmpty()) assertNull("there is nothing to shoot with", camera.capture()) }
@Test fun grantedAccessWithoutACameraIsUnavailable() = runTest { val camera = camera(FakeBackend(lenses = emptySet()))
camera.start(owner)
assertEquals(CameraStatus.Unavailable, camera.status) assertNull(camera.capture()) }
@Test fun theCameraStartsOutStartingOnTheBackLensWithFlashOff() { val camera = camera()
assertEquals(CameraStatus.Starting, camera.status) assertEquals(Lens.Back, camera.lens) assertFalse(camera.flashOn) assertFalse("flash is only offered once a lens reports having one", camera.hasFlash) assertFalse(camera.isCapturing) assertNull(camera.surfaceRequest) }
@Test fun aWorkingBackLensComesUpReadyWithItsFlash() = runTest { val backend = FakeBackend() val camera = camera(backend)
camera.start(owner)
assertEquals(CameraStatus.Ready, camera.status) assertEquals(listOf(Lens.Back), backend.opened) assertTrue(camera.hasFlash) }
@Test fun aShotLandsInTheCaptureDirectory() = runTest { val backend = FakeBackend() val camera = camera(backend) camera.start(owner)
val shot = camera.capture()
assertNotNull(shot) assertEquals(folder.root, shot!!.parentFile) assertTrue(shot.length() > 0) assertFalse("the shutter is free again afterwards", camera.isCapturing) }
@Test fun flashOnlyFiresWhenToggledOnAndTheLensHasOne() = runTest { val backend = FakeBackend() val camera = camera(backend) camera.start(owner)
camera.capture() assertEquals(false, backend.lastFlash)
camera.toggleFlash() camera.capture() assertEquals(true, backend.lastFlash)
// The front lens has no flash, so the toggle being on must not ask for it. camera.flip() camera.capture() assertEquals(false, backend.lastFlash) }
@Test fun flippingSwitchesLensesAndTheirFlash() = runTest { val backend = FakeBackend() val camera = camera(backend) camera.start(owner)
camera.flip()
assertEquals(Lens.Front, camera.lens) assertFalse(camera.hasFlash) assertEquals(listOf(Lens.Back, Lens.Front), backend.opened) assertEquals(CameraStatus.Ready, camera.status) }
/** Opening replaces the binding, so a flip that fails has to reopen the old lens. */ @Test fun aFlipToAMissingLensPutsThePreviousOneBack() = runTest { val backend = FakeBackend(lenses = setOf(Lens.Back)) val camera = camera(backend) camera.start(owner)
camera.flip()
assertEquals(Lens.Back, camera.lens) assertEquals(CameraStatus.Ready, camera.status) assertEquals("back, then back again after the front failed", listOf(Lens.Back, Lens.Back), backend.opened) }
@Test fun aShotThatFailsReportsNothingRatherThanCrashing() = runTest { val camera = camera(FakeBackend(failCapture = true)) camera.start(owner)
assertNull(camera.capture()) assertFalse(camera.isCapturing) assertEquals("still usable for another try", CameraStatus.Ready, camera.status) }
@Test fun stoppingClosesTheLensAndDropsThePreview() = runTest { val backend = FakeBackend() val camera = camera(backend) camera.start(owner)
camera.stop()
assertEquals(1, backend.closed) assertNull(camera.surfaceRequest) assertNull("nothing to shoot with once stopped", camera.capture()) }
/** Recreating the composer on rotation used to wipe the shot it was showing. */ @Test fun pruningKeepsAFreshShotAndDropsAStaleOne() { val now = System.currentTimeMillis() val fresh = folder.newFile("fresh.jpg").apply { setLastModified(now - 5_000) } val stale = folder.newFile("stale.jpg").apply { setLastModified(now - 2 * 60 * 60 * 1000) }
pruneStaleCaptures(folder.root, now)
assertTrue(fresh.exists()) assertFalse(stale.exists()) }
@Test fun theLabelChipNamesWhatIsSetInTheOrderTheyAreOffered() { assertNull(labelSummary(emptySet())) assertEquals("Nudity, Adult", labelSummary(setOf("porn", "nudity"))) }}