mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
feat(lab): keep earlier Lab builds and list them in Updates
This commit is contained in:
1 parent
914c659f4a
commit
5f0519dfbf
5 files changed
+312
-28
No files matched your search
@@ -24,14 +24,22 @@ import java.io.File
|
||||
* loaded over the installed APK at startup so it can be tried without reinstalling or re-signing.
|
||||
*
|
||||
* `files/lab/next` holds a build waiting for the next start and `files/lab/active` the one in use,
|
||||
* each with `overlay.apk` (the build's dex files and linked resources) and `overlay.json`.
|
||||
* each with `overlay.apk` (the build's dex files and linked resources) and `overlay.json`. A build
|
||||
* that is replaced, turned off or refused moves to `files/lab/builds`, so it can be used again.
|
||||
*/
|
||||
object LabOverlay {
|
||||
data class Info(val label: String, val baseCommit: String, val baseApk: String)
|
||||
data class Info(val label: String, val baseCommit: String, val baseApk: String, val built: Long = 0)
|
||||
|
||||
enum class Reason { REPLACED, TURNED_OFF, OTHER_INSTALL, DID_NOT_START, UNREADABLE }
|
||||
|
||||
/** A build kept in `files/lab/builds`; [usable] when it was made for this install. */
|
||||
data class Saved(val dir: File, val info: Info?, val reason: Reason?, val usable: Boolean)
|
||||
|
||||
private const val TAG = "LabOverlay"
|
||||
private const val MAX_FAILED_STARTS = 2
|
||||
private const val STABLE_AFTER_MS = 10_000L
|
||||
private const val KEEP_BUILDS = 12
|
||||
const val SKIPPED_APP_IMAGE = "app image present"
|
||||
|
||||
/** The installed APK's commit; this class always comes from the APK, never from a Lab build. */
|
||||
val apkCommit: String = BuildConfig.SOURCE_COMMIT
|
||||
@@ -40,6 +48,9 @@ object LabOverlay {
|
||||
private set
|
||||
@Volatile var skipped: String? = null
|
||||
private set
|
||||
/** This start moved a newly staged build into use. */
|
||||
@Volatile var promoted = false
|
||||
private set
|
||||
|
||||
private var loader: Any? = null
|
||||
|
||||
@@ -47,6 +58,8 @@ object LabOverlay {
|
||||
internal var injectDex: (ClassLoader, File) -> Unit = ::prependDex
|
||||
@VisibleForTesting
|
||||
internal var openResources: (File) -> Any? = { if (Build.VERSION.SDK_INT >= 30) createLoader(it) else null }
|
||||
@VisibleForTesting
|
||||
internal var appImage: (Context) -> Boolean = ::hasAppImage
|
||||
|
||||
fun root(context: Context) = File(context.filesDir, "lab")
|
||||
|
||||
@@ -54,24 +67,23 @@ object LabOverlay {
|
||||
fun load(app: Application, mainProcess: Boolean) {
|
||||
active = null
|
||||
skipped = null
|
||||
promoted = false
|
||||
try {
|
||||
val root = root(app)
|
||||
if (mainProcess) promote(root)
|
||||
if (mainProcess) promoted = promote(root)
|
||||
val dir = File(root, "active")
|
||||
val apk = File(dir, "overlay.apk")
|
||||
val meta = File(dir, "overlay.json")
|
||||
if (!apk.isFile || !meta.isFile) return
|
||||
val info = parse(meta.readText()) ?: return refuse(root, mainProcess, "unreadable overlay.json")
|
||||
// The APK's path changes with every install and update of the app.
|
||||
if (info.baseCommit != apkCommit || info.baseApk != app.applicationInfo.sourceDir)
|
||||
return refuse(root, mainProcess, "built for another install")
|
||||
val info = parse(meta.readText()) ?: return refuse(root, mainProcess, Reason.UNREADABLE)
|
||||
if (!madeFor(app, info)) return refuse(root, mainProcess, Reason.OTHER_INSTALL)
|
||||
if (Build.VERSION.SDK_INT < 30) return skip("needs Android 11")
|
||||
// Classes in an app image are loaded before attachBaseContext and would ignore the overlay;
|
||||
// dd-lab-build recompiles the app without one (cmd package compile -m verify).
|
||||
if (hasAppImage(app)) return skip("app image present")
|
||||
if (appImage(app)) return skip(SKIPPED_APP_IMAGE)
|
||||
if (mainProcess) {
|
||||
val starts = failedStarts(root) + 1
|
||||
if (starts > MAX_FAILED_STARTS) return refuse(root, true, "did not start")
|
||||
if (starts > MAX_FAILED_STARTS) return refuse(root, true, Reason.DID_NOT_START)
|
||||
File(root, "starts").writeText(starts.toString())
|
||||
}
|
||||
apk.setReadOnly()
|
||||
@@ -86,12 +98,55 @@ object LabOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
/** The APK's path changes with every install and update of the app, so it pins a build to one install. */
|
||||
fun madeFor(context: Context, info: Info) = info.baseCommit == apkCommit && info.baseApk == context.applicationInfo.sourceDir
|
||||
|
||||
/** The build in `active`, whether or not this start loaded it. */
|
||||
fun current(context: Context): Info? = read(File(root(context), "active"))
|
||||
|
||||
/** A build staged for the next start. */
|
||||
fun pending(context: Context): Info? = read(File(root(context), "next"))
|
||||
|
||||
/** Kept builds, newest first. */
|
||||
fun saved(context: Context): List<Saved> = File(root(context), "builds").listFiles().orEmpty()
|
||||
.filter { File(it, "overlay.apk").isFile }
|
||||
.sortedByDescending { it.name.toLongOrNull() ?: 0L }
|
||||
.map { dir ->
|
||||
val info = read(dir)
|
||||
val reason = File(dir, "reason").takeIf { it.isFile }?.readText()?.trim()?.let { r -> Reason.entries.firstOrNull { it.name == r } }
|
||||
Saved(dir, info, reason, info != null && madeFor(context, info))
|
||||
}
|
||||
|
||||
/** Leaves the Lab build and restarts the app on the installed APK. */
|
||||
fun turnOff(context: Context) {
|
||||
val root = root(context)
|
||||
File(root, "active").renameTo(File(root, "off-${System.currentTimeMillis()}"))
|
||||
archive(root, File(root, "active"), Reason.TURNED_OFF)
|
||||
File(root, "next").deleteRecursively()
|
||||
File(root, "starts").delete()
|
||||
restart(context)
|
||||
}
|
||||
|
||||
/** Stages [build] for the next start; the caller restarts the app. */
|
||||
fun stage(context: Context, build: Saved) {
|
||||
val root = root(context)
|
||||
File(root, "next").deleteRecursively()
|
||||
File(build.dir, "reason").delete()
|
||||
if (!build.dir.renameTo(File(root, "next"))) error("could not stage ${build.dir.name}")
|
||||
}
|
||||
|
||||
fun delete(build: Saved) {
|
||||
build.dir.deleteRecursively()
|
||||
}
|
||||
|
||||
/**
|
||||
* Android's background optimization can give the APK an app image again, which keeps a Lab
|
||||
* build from loading. Recompiling without one needs a shell, which the caller provides.
|
||||
*/
|
||||
fun needsRecompile(context: Context) = appImage(context)
|
||||
|
||||
fun recompileCommand(context: Context) = "cmd package compile -m verify -f ${context.packageName}"
|
||||
|
||||
fun restart(context: Context) {
|
||||
val launch = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
||||
?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
if (launch != null) context.startActivity(launch)
|
||||
@@ -101,28 +156,46 @@ object LabOverlay {
|
||||
@VisibleForTesting
|
||||
internal fun parse(json: String): Info? = runCatching {
|
||||
val o = JSONObject(json)
|
||||
Info(o.getString("label"), o.getString("baseCommit"), o.getString("baseApk"))
|
||||
Info(o.getString("label"), o.getString("baseCommit"), o.getString("baseApk"), o.optLong("built"))
|
||||
}.getOrNull()
|
||||
|
||||
private fun read(dir: File): Info? = File(dir, "overlay.json").takeIf { it.isFile && File(dir, "overlay.apk").isFile }?.let { parse(it.readText()) }
|
||||
|
||||
/** Moves `next` into use and keeps the build it replaces. Returns whether there was one to move. */
|
||||
@VisibleForTesting
|
||||
internal fun promote(root: File) {
|
||||
internal fun promote(root: File): Boolean {
|
||||
val next = File(root, "next")
|
||||
if (!File(next, "overlay.apk").isFile || !File(next, "overlay.json").isFile) return
|
||||
if (!File(next, "overlay.apk").isFile || !File(next, "overlay.json").isFile) return false
|
||||
val active = File(root, "active")
|
||||
archive(root, active, Reason.REPLACED)
|
||||
active.deleteRecursively()
|
||||
if (next.renameTo(active)) File(root, "starts").delete()
|
||||
if (!next.renameTo(active)) return false
|
||||
File(root, "starts").delete()
|
||||
return true
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun archive(root: File, dir: File, reason: Reason) {
|
||||
if (!File(dir, "overlay.apk").isFile) return
|
||||
val builds = File(root, "builds").apply { mkdirs() }
|
||||
var stamp = System.currentTimeMillis()
|
||||
while (File(builds, stamp.toString()).exists()) stamp++
|
||||
val target = File(builds, stamp.toString())
|
||||
if (!dir.renameTo(target)) return
|
||||
File(target, "reason").writeText(reason.name)
|
||||
builds.listFiles().orEmpty().sortedByDescending { it.name.toLongOrNull() ?: 0L }.drop(KEEP_BUILDS).forEach { it.deleteRecursively() }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun failedStarts(root: File) = File(root, "starts").takeIf { it.isFile }?.readText()?.trim()?.toIntOrNull() ?: 0
|
||||
|
||||
private fun refuse(root: File, mainProcess: Boolean, reason: String) {
|
||||
skipped = reason
|
||||
private fun refuse(root: File, mainProcess: Boolean, reason: Reason) {
|
||||
skipped = reason.name
|
||||
Log.w(TAG, "Lab build turned off: $reason")
|
||||
if (!mainProcess) return
|
||||
File(root, "active").renameTo(File(root, "off-${System.currentTimeMillis()}"))
|
||||
archive(root, File(root, "active"), reason)
|
||||
File(root, "starts").delete()
|
||||
File(root, "last-off-reason").writeText(reason)
|
||||
File(root, "last-off-reason").writeText(reason.name)
|
||||
}
|
||||
|
||||
private fun skip(reason: String) {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.WirelessAdbFix
|
||||
import com.droiddeck.launcher.lab.LabOverlay
|
||||
|
||||
/** Lab builds on this device: the one in use, one waiting for a restart, and the kept ones. */
|
||||
@Composable
|
||||
internal fun LabBuilds() {
|
||||
val context = LocalContext.current
|
||||
var version by remember { mutableIntStateOf(0) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
val current = remember(version) { LabOverlay.current(context) }
|
||||
val pending = remember(version) { LabOverlay.pending(context) }
|
||||
val saved = remember(version) { LabOverlay.saved(context) }
|
||||
if (current == null && pending == null && saved.isEmpty()) return
|
||||
val active = LabOverlay.active
|
||||
val paused = active == null && current != null && LabOverlay.skipped == LabOverlay.SKIPPED_APP_IMAGE
|
||||
val restartFailed = stringResource(R.string.lab_restart_failed)
|
||||
|
||||
/** Android may have given the APK an app image again, which a Lab build cannot load over. */
|
||||
fun restart() {
|
||||
busy = true
|
||||
error = null
|
||||
Thread({
|
||||
val result = runCatching {
|
||||
if (LabOverlay.needsRecompile(context)) WirelessAdbFix.shell(context, LabOverlay.recompileCommand(context))
|
||||
}
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||
busy = false
|
||||
if (result.isSuccess) LabOverlay.restart(context)
|
||||
else error = restartFailed.format(result.exceptionOrNull()?.message.orEmpty())
|
||||
}
|
||||
}, "lab-restart").start()
|
||||
}
|
||||
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Column(Modifier.fillMaxWidth().padding(top = 18.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.lab_builds), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp,
|
||||
color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 2.dp, top = 2.dp, bottom = 10.dp),
|
||||
)
|
||||
Column(Modifier.fillMaxWidth().clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14)) {
|
||||
val rows = mutableListOf<@Composable () -> Unit>()
|
||||
if (current != null) rows += {
|
||||
BuildRow(
|
||||
current, true,
|
||||
when {
|
||||
active != null -> stringResource(R.string.lab_in_use)
|
||||
paused -> stringResource(R.string.lab_paused_row)
|
||||
else -> stringResource(R.string.lab_not_loaded, LabOverlay.skipped.orEmpty())
|
||||
},
|
||||
warn = active == null,
|
||||
) {
|
||||
if (paused) PrimaryButton(stringResource(R.string.lab_restart), compact = true, enabled = !busy) { restart() }
|
||||
SecondaryButton(stringResource(R.string.lab_turn_off), compact = true, enabled = !busy) { LabOverlay.turnOff(context) }
|
||||
}
|
||||
}
|
||||
if (pending != null) rows += {
|
||||
BuildRow(pending, false, stringResource(R.string.lab_waiting), warn = false) {
|
||||
PrimaryButton(stringResource(R.string.lab_restart), compact = true, enabled = !busy) { restart() }
|
||||
}
|
||||
}
|
||||
for (build in saved) rows += {
|
||||
var armed by remember(build.dir) { mutableStateOf(false) }
|
||||
val info = build.info
|
||||
BuildRow(
|
||||
info, false,
|
||||
when {
|
||||
armed -> stringResource(R.string.lab_delete_warning)
|
||||
!build.usable -> stringResource(R.string.lab_other_install)
|
||||
build.reason == LabOverlay.Reason.DID_NOT_START -> stringResource(R.string.lab_off_did_not_start)
|
||||
else -> stringResource(R.string.lab_kept)
|
||||
},
|
||||
warn = armed || !build.usable || build.reason == LabOverlay.Reason.DID_NOT_START,
|
||||
) {
|
||||
if (build.usable && !armed) SecondaryButton(stringResource(R.string.lab_use), compact = true, enabled = !busy) {
|
||||
runCatching { LabOverlay.stage(context, build) }
|
||||
.onSuccess { restart() }
|
||||
.onFailure { error = it.message }
|
||||
}
|
||||
SecondaryButton(stringResource(if (armed) R.string.lab_delete_confirm else R.string.lab_delete), compact = true, enabled = !busy) {
|
||||
if (armed) {
|
||||
LabOverlay.delete(build)
|
||||
version++
|
||||
} else armed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
rows.forEachIndexed { index, row ->
|
||||
row()
|
||||
if (index < rows.lastIndex) Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
}
|
||||
error?.let { Text(it, fontSize = 12.5.sp, color = AttentionAmber, modifier = Modifier.padding(start = 2.dp, top = 8.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BuildRow(info: LabOverlay.Info?, current: Boolean, status: String, warn: Boolean, actions: @Composable () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 60.dp).padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(34.dp).clip(Shape12)
|
||||
.background(if (current) pal.signal.copy(alpha = 0.18f) else colors.surfaceVariant)) {
|
||||
Icon(Icons.Outlined.Code, null, tint = if (current) pal.signal else colors.onSurfaceVariant, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(info?.label ?: "?", fontSize = 14.5.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
val built = info?.built?.takeIf { it > 0 }?.let { DateUtils.getRelativeTimeSpanString(it * 1000, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS).toString() }
|
||||
Text(listOfNotNull(status, built).joinToString(" · "), fontSize = 12.5.sp, color = if (warn) AttentionAmber else colors.onSurfaceVariant,
|
||||
maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { actions() }
|
||||
}
|
||||
}
|
||||
@@ -280,17 +280,12 @@ private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) {
|
||||
if (tests.isEmpty()) stringResource(R.string.upd_tests_none) else pluralStringResource(R.plurals.upd_tests_count, tests.size, tests.size),
|
||||
u.follow.channel == Channel.TEST, enabled = tests.isNotEmpty() || u.follow.channel == Channel.TEST,
|
||||
) { tests.firstOrNull()?.let { ua.onFollow(Follow(Channel.TEST, it.pr)) } }
|
||||
com.droiddeck.launcher.lab.LabOverlay.active?.let { lab ->
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
ChannelCard(Icons.Outlined.Code, stringResource(R.string.upd_lab), lab.label, stringResource(R.string.upd_lab_off), true) {
|
||||
com.droiddeck.launcher.lab.LabOverlay.turnOff(context)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(u.follow.channel == Channel.TEST && tests.isNotEmpty(), enter = expandVertically(Motion.sp(1f)) + fadeIn(Motion.sp(1f)), exit = shrinkVertically(Motion.sp(1f)) + fadeOut(Motion.sp(1f))) {
|
||||
Column(Modifier.padding(start = 18.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
tests.forEach { t -> TestRow(t, u.follow.pr == t.pr) { ua.onFollow(Follow(Channel.TEST, t.pr)) } }
|
||||
}
|
||||
}
|
||||
LabBuilds()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1119,7 +1119,27 @@
|
||||
<string name="upd_tests_hint">Try a fix before it\'s released</string>
|
||||
<string name="upd_tests_none">None right now</string>
|
||||
<string name="upd_lab">Lab build</string>
|
||||
<string name="upd_lab_off">Tap to go back to the installed version</string>
|
||||
<string name="lab_builds">Lab builds</string>
|
||||
<string name="lab_in_use">In use</string>
|
||||
<string name="lab_paused_row">Paused after Android re-optimized DroidDeck</string>
|
||||
<string name="lab_not_loaded">Not loaded: %1$s</string>
|
||||
<string name="lab_waiting">Waiting for a restart</string>
|
||||
<string name="lab_kept">Kept</string>
|
||||
<string name="lab_other_install">Built for another version of DroidDeck</string>
|
||||
<string name="lab_delete_warning">Deletes this build from the device</string>
|
||||
<string name="lab_restart">Restart on it</string>
|
||||
<string name="lab_turn_off">Turn off</string>
|
||||
<string name="lab_use">Use</string>
|
||||
<string name="lab_delete">Delete</string>
|
||||
<string name="lab_delete_confirm">Delete now</string>
|
||||
<string name="lab_restart_failed">Could not prepare the restart: %1$s. Pair wireless debugging in Setup and try again.</string>
|
||||
<string name="lab_installed_title">Lab build installed</string>
|
||||
<string name="lab_installed_text">DroidDeck restarted on %1$s</string>
|
||||
<string name="lab_paused_title">Lab build paused</string>
|
||||
<string name="lab_paused_text">Android re-optimized DroidDeck, so %1$s is not loaded. Open Updates to restart on it.</string>
|
||||
<string name="lab_off_title">Lab build turned off</string>
|
||||
<string name="lab_off_other_install">It was built for another version of DroidDeck. It is kept in Updates.</string>
|
||||
<string name="lab_off_did_not_start">DroidDeck did not start on it twice</string>
|
||||
<string name="upd_pr">PR #%1$d</string>
|
||||
<string name="upd_not_built">Not built for this copy of DroidDeck</string>
|
||||
<string name="upd_updated">Updated %1$s</string>
|
||||
|
||||
@@ -69,14 +69,14 @@ class LabOverlayTest {
|
||||
assertNull(LabOverlay.active)
|
||||
assertTrue(injected.isEmpty())
|
||||
assertFalse(File(root, "active").exists())
|
||||
assertEquals("built for another install", File(root, "last-off-reason").readText())
|
||||
assertEquals("OTHER_INSTALL", File(root, "last-off-reason").readText())
|
||||
}
|
||||
|
||||
@Test fun aBuildForAnEarlierInstallOfTheSameCommitIsTurnedOff() {
|
||||
stage("active", apk = "/data/app/old/base.apk")
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
assertNull(LabOverlay.active)
|
||||
assertEquals("built for another install", File(root, "last-off-reason").readText())
|
||||
assertEquals("OTHER_INSTALL", File(root, "last-off-reason").readText())
|
||||
}
|
||||
|
||||
@Test fun aBuildThatKeepsFailingToStartIsTurnedOff() {
|
||||
@@ -84,7 +84,7 @@ class LabOverlayTest {
|
||||
File(root, "starts").writeText("2")
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
assertNull(LabOverlay.active)
|
||||
assertEquals("did not start", File(root, "last-off-reason").readText())
|
||||
assertEquals("DID_NOT_START", File(root, "last-off-reason").readText())
|
||||
}
|
||||
|
||||
@Test fun otherProcessesLoadWithoutCountingStarts() {
|
||||
@@ -98,5 +98,46 @@ class LabOverlayTest {
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
assertNull(LabOverlay.active)
|
||||
assertTrue(injected.isEmpty())
|
||||
assertFalse(LabOverlay.promoted)
|
||||
}
|
||||
|
||||
@Test fun aNewBuildKeepsTheOneItReplaces() {
|
||||
stage("active")
|
||||
stage("next")
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
assertTrue(LabOverlay.promoted)
|
||||
val saved = LabOverlay.saved(app).single()
|
||||
assertEquals(LabOverlay.Reason.REPLACED, saved.reason)
|
||||
assertTrue(saved.usable)
|
||||
}
|
||||
|
||||
@Test fun aRefusedBuildIsKeptButNotUsable() {
|
||||
stage("active", commit = "0000000")
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
val saved = LabOverlay.saved(app).single()
|
||||
assertEquals(LabOverlay.Reason.OTHER_INSTALL, saved.reason)
|
||||
assertFalse(saved.usable)
|
||||
}
|
||||
|
||||
@Test fun aKeptBuildCanBeStagedAgain() {
|
||||
stage("active", commit = "0000000")
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
stage("active")
|
||||
LabOverlay.archive(root, File(root, "active"), LabOverlay.Reason.TURNED_OFF)
|
||||
val usable = LabOverlay.saved(app).first { it.usable }
|
||||
LabOverlay.stage(app, usable)
|
||||
assertNotNull(LabOverlay.pending(app))
|
||||
assertEquals(1, LabOverlay.saved(app).size)
|
||||
LabOverlay.load(app, mainProcess = true)
|
||||
assertTrue(LabOverlay.promoted)
|
||||
assertNotNull(LabOverlay.active)
|
||||
}
|
||||
|
||||
@Test fun onlyTheNewestBuildsAreKept() {
|
||||
repeat(15) {
|
||||
stage("active")
|
||||
LabOverlay.archive(root, File(root, "active"), LabOverlay.Reason.TURNED_OFF)
|
||||
}
|
||||
assertEquals(12, LabOverlay.saved(app).size)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user