mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
fix(components): auto updates follow DroidDeck-Proton only (#2190)
This commit is contained in:
1 parent
250f80fbb5
commit
f33d5b9e09
5 files changed
+227
-9
No files matched your search
@@ -13,6 +13,8 @@ object ComponentUpdater {
|
||||
private const val TAG = "ComponentUpdater"
|
||||
private const val CHECK_EVERY_MS = 3_600_000L
|
||||
private const val BOOTED_KEPT = 200
|
||||
private const val HANDED_BACK = "handedBack"
|
||||
internal const val HAND_BACK_FILES = "handBackFiles"
|
||||
private val lock = Any()
|
||||
private val io = Any()
|
||||
|
||||
@@ -57,7 +59,8 @@ object ComponentUpdater {
|
||||
|
||||
fun settle(context: Context) {
|
||||
val owned = load(context).optJSONObject("lines")?.let { lines -> lines.keys().asSequence().flatMap { lines.optJSONObject(it).strings("owned") }.toSet() } ?: emptySet()
|
||||
val choices = ComponentsManager.activeChoices(context).values.flatMap { it.entries }
|
||||
val own = (runCatching { ComponentsManager.protons(context) }.getOrNull() ?: return).filter(ComponentUpdates::follows).mapTo(HashSet()) { it.id }
|
||||
val choices = ComponentsManager.activeChoices(context).filterKeys { it in own }.values.flatMap { it.entries }
|
||||
for (comp in ComponentsManager.COMPONENTS) {
|
||||
if (SessionPrefs.componentModeChosen(context, comp)) continue
|
||||
val manual = choices.any { (c, choice) -> c == comp && !choice.auto && choice.file !in owned && !choice.file.startsWith("original ") }
|
||||
@@ -80,7 +83,7 @@ object ComponentUpdater {
|
||||
logLines(context).mapNotNull { STAMP.find(it)?.groupValues?.get(1) }.maxOrNull() ?: "0"
|
||||
|
||||
private fun lineOf(context: Context, state: JSONObject, p: ComponentsManager.Proton, comp: String): ComponentUpdates.Line? {
|
||||
if (!ComponentsManager.supports(p, comp)) return null
|
||||
if (!ComponentUpdates.follows(p) || !ComponentsManager.supports(p, comp)) return null
|
||||
if (comp == "fex") return ComponentUpdates.line("fex", false)
|
||||
val key = "${p.id}|${p.version}"
|
||||
val flavors = state.obj("flavors").obj(key)
|
||||
@@ -93,7 +96,9 @@ object ComponentUpdater {
|
||||
settle(context)
|
||||
val state = load(context)
|
||||
val notes = ArrayList<String>()
|
||||
val protons = runCatching { ComponentsManager.protons(context) }.getOrDefault(emptyList())
|
||||
val found = runCatching { ComponentsManager.protons(context) }.getOrNull()
|
||||
val protons = found ?: emptyList()
|
||||
if (found != null) handBack(context, state, found)
|
||||
judge(context, state, protons, notes)
|
||||
val due = force || System.currentTimeMillis() - state.optLong("checkedAt") >= CHECK_EVERY_MS
|
||||
val wanted = ComponentsManager.COMPONENTS.filter { managed(context, it) }
|
||||
@@ -137,8 +142,26 @@ object ComponentUpdater {
|
||||
complete = false
|
||||
continue
|
||||
}
|
||||
val current = ls.optString("current")
|
||||
val missing = current.isNotEmpty() && !ComponentsManager.isStored(context, current)
|
||||
val relisted = if (missing) catalog.firstOrNull { it.file == current } else null
|
||||
if (relisted != null) {
|
||||
if (holdBack) {
|
||||
state.put("note", context.getString(R.string.comp_auto_waits_unmetered))
|
||||
complete = false
|
||||
continue
|
||||
}
|
||||
val got = runCatching { ComponentsManager.download(context, relisted) {} }
|
||||
if (got.isFailure) {
|
||||
Log.w(TAG, "download ${relisted.file} again", got.exceptionOrNull())
|
||||
state.put("note", context.getString(R.string.comp_auto_check_failed))
|
||||
complete = false
|
||||
continue
|
||||
}
|
||||
ls.put("owned", JSONArray(ls.strings("owned") + current))
|
||||
}
|
||||
val target = ComponentUpdates.pick(catalog, line, ls.strings("bad"))
|
||||
if (target == null || ls.optString("current") == target.file) continue
|
||||
if (target == null || current == target.file) continue
|
||||
val stored = ComponentsManager.isStored(context, target.file)
|
||||
if (!stored) {
|
||||
if (holdBack) {
|
||||
@@ -161,7 +184,7 @@ object ComponentUpdater {
|
||||
if (!stored) runCatching { ComponentsManager.deletePackage(context, target.file) }
|
||||
continue
|
||||
}
|
||||
ls.put("previous", ls.optString("current").ifEmpty { ComponentUpdates.ORIGINAL })
|
||||
ls.put("previous", if (missing && relisted == null) ComponentUpdates.ORIGINAL else current.ifEmpty { ComponentUpdates.ORIGINAL })
|
||||
.put("current", target.file).put("since", latestStamp(context)).put("failed", JSONObject())
|
||||
if (!stored) ls.put("owned", JSONArray(ls.strings("owned") + target.file))
|
||||
ls.remove("note")
|
||||
@@ -210,6 +233,33 @@ object ComponentUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun placed(state: JSONObject): Set<String> {
|
||||
val remembered = state.strings(HAND_BACK_FILES)
|
||||
val lines = state.optJSONObject("lines") ?: return remembered
|
||||
return remembered + lines.keys().asSequence().flatMap { k ->
|
||||
val ls = lines.optJSONObject(k) ?: return@flatMap emptySequence()
|
||||
(ls.strings("owned") + ls.strings("bad") + ls.optString("current") + ls.optString("previous")).asSequence()
|
||||
}.filter { it.isNotEmpty() }.toSet()
|
||||
}
|
||||
|
||||
private fun handBack(context: Context, state: JSONObject, protons: List<ComponentsManager.Proton>) {
|
||||
if (state.optBoolean(HANDED_BACK)) return
|
||||
val placed = placed(state)
|
||||
state.put(HAND_BACK_FILES, JSONArray(placed.sorted()))
|
||||
val own = protons.filter(ComponentUpdates::follows).mapTo(HashSet()) { it.id }
|
||||
runCatching { ComponentsManager.dropQueuedPackages(context, placed) { it in own || ComponentUpdates.ownId(it) } }.onFailure { Log.w(TAG, "drop queued Auto builds", it) }.getOrNull() ?: return
|
||||
val strays = ComponentUpdates.strays(protons, ComponentsManager.activeChoices(context), placed)
|
||||
for ((p, comp) in strays) {
|
||||
runCatching { ComponentsManager.restoreUnlessQueued(context, p.id, comp, ComponentsManager.safeName(p.version)) }
|
||||
.onFailure { Log.w(TAG, "hand back ${p.name} $comp", it) }
|
||||
}
|
||||
val after = ComponentsManager.activeChoices(context)
|
||||
if (ComponentUpdates.strays(protons, after, placed).isEmpty() && !ComponentUpdates.unseen(protons, after, placed)) {
|
||||
state.put(HANDED_BACK, true)
|
||||
state.remove(HAND_BACK_FILES)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun dropRemoved(state: JSONObject, protons: List<ComponentsManager.Proton>) {
|
||||
val installed = protons.mapTo(HashSet()) { it.id }
|
||||
val lines = state.optJSONObject("lines") ?: return
|
||||
|
||||
@@ -23,6 +23,30 @@ object ComponentUpdates {
|
||||
Line("fex", "fex", "DD-FEXCore-"),
|
||||
)
|
||||
|
||||
private val OWN_PREFIX = ProtonExtras.tools.first { it.id == ProtonExtras.OWN }.prefix
|
||||
|
||||
fun follows(p: ComponentsManager.Proton): Boolean = !p.valve && p.dir.name.startsWith(OWN_PREFIX)
|
||||
|
||||
fun ownId(protonId: String): Boolean = protonId.startsWith(ComponentsManager.safeName(OWN_PREFIX))
|
||||
|
||||
fun strays(
|
||||
protons: List<ComponentsManager.Proton>,
|
||||
choices: Map<String, Map<String, ComponentsManager.Choice>>,
|
||||
placed: Set<String>,
|
||||
): List<Pair<ComponentsManager.Proton, String>> =
|
||||
protons.filterNot(::follows).flatMap { p ->
|
||||
choices[p.id].orEmpty().filter { (_, c) -> !c.auto && c.protonVersion == p.version && c.file in placed }.keys.map { p to it }
|
||||
}
|
||||
|
||||
fun unseen(
|
||||
protons: List<ComponentsManager.Proton>,
|
||||
choices: Map<String, Map<String, ComponentsManager.Choice>>,
|
||||
placed: Set<String>,
|
||||
): Boolean {
|
||||
val found = protons.mapTo(HashSet()) { it.id }
|
||||
return choices.any { (id, comps) -> id !in found && comps.values.any { !it.auto && it.file in placed } }
|
||||
}
|
||||
|
||||
fun line(comp: String, arm64ec: Boolean): Line = LINES.first {
|
||||
it.comp == comp && (comp == "fex" || it.key.endsWith("-arm64ec") == arm64ec)
|
||||
}
|
||||
|
||||
@@ -700,6 +700,12 @@ object ComponentsManager {
|
||||
context.getString(R.string.cmgr_restore_done, LABEL[comp], p.name, protonVersion)
|
||||
}
|
||||
|
||||
fun restoreUnlessQueued(context: Context, protonId: String, comp: String, protonVersion: String): Boolean = synchronized(lock) {
|
||||
if (loadState(context).optJSONObject("queued")?.optJSONObject(protonId)?.has(comp) == true) return false
|
||||
restore(context, protonId, comp, protonVersion)
|
||||
true
|
||||
}
|
||||
|
||||
private fun applyOriginal(context: Context, p: Proton, comp: String, protonVersion: String, wcp: File, state: JSONObject) {
|
||||
val current = protonVersion == safeName(p.version)
|
||||
if (!current) ensureOriginal(context, p, comp, state)
|
||||
@@ -750,6 +756,25 @@ object ComponentsManager {
|
||||
saveState(context, state)
|
||||
}
|
||||
|
||||
fun dropQueuedPackages(context: Context, files: Set<String>, keep: (String) -> Boolean): Int = synchronized(lock) {
|
||||
val state = loadState(context)
|
||||
val queued = state.sub("queued")
|
||||
var dropped = 0
|
||||
for (pid in queued.keys().asSequence().toList()) {
|
||||
if (keep(pid)) continue
|
||||
val comps = queued.optJSONObject(pid) ?: continue
|
||||
for (comp in comps.keys().asSequence().toList()) {
|
||||
val entry = comps.optJSONObject(comp) ?: continue
|
||||
if (entry.optString("kind") != "package" || entry.optBoolean("auto") || entry.optString("file") !in files) continue
|
||||
comps.remove(comp)
|
||||
dropped++
|
||||
}
|
||||
if (comps.length() == 0) queued.remove(pid)
|
||||
}
|
||||
if (dropped > 0) saveState(context, state)
|
||||
dropped
|
||||
}
|
||||
|
||||
class StoredPackage(val pkg: Package, val inUse: Boolean)
|
||||
class StoredOriginal(val protonId: String, val protonName: String?, val comp: String, val protonVersion: String, val size: Long, val kept: Boolean)
|
||||
class Stored(val packages: List<StoredPackage>, val originals: List<StoredOriginal>)
|
||||
@@ -968,9 +993,16 @@ object ComponentsManager {
|
||||
if (!force && cache.isFile) runCatching { return parseCatalog(JSONObject(cache.readText())) }
|
||||
if (!force) return Catalog(emptyList(), 0)
|
||||
val items = JSONArray()
|
||||
val previous = if (cache.isFile) runCatching { JSONObject(cache.readText()).optJSONArray("items") }.getOrNull() else null
|
||||
var fetched = 0
|
||||
for ((repo, tag) in SOURCES) {
|
||||
val body = Downloader.downloadString("https://api.github.com/repos/$repo/releases/tags/$tag") ?: continue
|
||||
val assets = runCatching { JSONObject(body).optJSONArray("assets") }.getOrNull() ?: continue
|
||||
val body = Downloader.downloadString("https://api.github.com/repos/$repo/releases/tags/$tag")
|
||||
val assets = body?.let { runCatching { JSONObject(it).optJSONArray("assets") }.getOrNull() }
|
||||
if (assets == null) {
|
||||
keepSource(previous, tag, items)
|
||||
continue
|
||||
}
|
||||
fetched++
|
||||
for (i in 0 until assets.length()) {
|
||||
val a = assets.getJSONObject(i)
|
||||
val name = a.optString("name")
|
||||
@@ -981,13 +1013,18 @@ object ComponentsManager {
|
||||
.put("url", a.optString("browser_download_url")).put("size", a.optLong("size")).put("digest", a.optString("digest")))
|
||||
}
|
||||
}
|
||||
if (items.length() == 0 && cache.isFile) runCatching { return parseCatalog(JSONObject(cache.readText())) }
|
||||
if (fetched == 0 && cache.isFile) runCatching { return parseCatalog(JSONObject(cache.readText())) }
|
||||
val out = JSONObject().put("items", items).put("fetchedAt", System.currentTimeMillis() / 1000)
|
||||
val tmp = File(cache.parentFile, cache.name + ".tmp")
|
||||
tmp.writeText(out.toString()); tmp.renameTo(cache)
|
||||
return parseCatalog(out)
|
||||
}
|
||||
|
||||
internal fun keepSource(previous: JSONArray?, tag: String, into: JSONArray) {
|
||||
previous ?: return
|
||||
for (i in 0 until previous.length()) previous.optJSONObject(i)?.takeIf { it.optString("release") == tag }?.let(into::put)
|
||||
}
|
||||
|
||||
private fun parseCatalog(o: JSONObject): Catalog {
|
||||
val arr = o.optJSONArray("items") ?: JSONArray()
|
||||
val items = (0 until arr.length()).map { arr.getJSONObject(it) }.map {
|
||||
|
||||
@@ -528,7 +528,8 @@ fun ComponentsPage(
|
||||
else -> stringResource(R.string.comp_nightlies_count, availableRows(l, catalog, snapshot).size)
|
||||
}
|
||||
}
|
||||
val moded: (String) -> Boolean = { it in compModes && it !in compAutoHeld && !ComponentsManager.runtimeManaged(it, gpu.systemPowerVr) }
|
||||
val followed = view?.proton?.let(ComponentUpdates::follows) == true
|
||||
val moded: (String) -> Boolean = { followed && it in compModes && it !in compAutoHeld && !ComponentsManager.runtimeManaged(it, gpu.systemPowerVr) }
|
||||
val automatic: (String) -> Boolean = { moded(it) && compModes[it] == ComponentUpdates.AUTO }
|
||||
val installedRows: @Composable ColumnScope.(String, Boolean) -> Unit = { l, right ->
|
||||
when (l) {
|
||||
|
||||
@@ -236,6 +236,112 @@ class ComponentUpdatesTest {
|
||||
assertEquals("DD-FEXCore-2611+2-nightly.wcp", fex.getJSONArray("bad").getString(0))
|
||||
}
|
||||
|
||||
private fun proton(dir: String, valve: Boolean = false, version: String = "$dir-1") =
|
||||
ComponentsManager.Proton(dir, dir, File("/steam/compatibilitytools.d/$dir"), "/p/$dir", version, valve)
|
||||
|
||||
@Test fun autoFollowsOnlyDroidDeckProton() {
|
||||
assertTrue(ComponentUpdates.follows(proton("DroidDeck-Proton-10.0-7")))
|
||||
assertFalse(ComponentUpdates.follows(proton("DroidDeck-Proton-10.0-7", valve = true)))
|
||||
assertFalse(ComponentUpdates.follows(proton("droiddeck-proton-arm64")))
|
||||
assertFalse(ComponentUpdates.follows(proton("droiddeck-proton-11-arm64")))
|
||||
assertFalse(ComponentUpdates.follows(proton("GE-Proton10-25")))
|
||||
assertFalse(ComponentUpdates.follows(proton("proton-cachyos-10.0-20251101-slr-arm64")))
|
||||
}
|
||||
|
||||
@Test fun onlyAutosOwnBuildsInOtherProtonsAreHandedBack() {
|
||||
val dd = proton("DroidDeck-Proton-10.0-7")
|
||||
val ge = proton("GE-Proton10-25")
|
||||
val valve = proton("Proton - Experimental", valve = true)
|
||||
val choices = mapOf(
|
||||
dd.id to mapOf("dxvk" to ComponentsManager.Choice("DD-dxvk-3.1.1+190-nightly.wcp", false, dd.version)),
|
||||
ge.id to mapOf(
|
||||
"dxvk" to ComponentsManager.Choice("DD-dxvk-3.1.1+190-nightly.wcp", false, ge.version),
|
||||
"vkd3d" to ComponentsManager.Choice("vkd3d-proton-3.0-mine.wcp", false, ge.version),
|
||||
"fex" to ComponentsManager.Choice("DD-FEXCore-2611+3-nightly.wcp", false, "GE-Proton10-24"),
|
||||
),
|
||||
valve.id to mapOf(
|
||||
"fex" to ComponentsManager.Choice("DD-FEXCore-2611+3-nightly.wcp", false, valve.version),
|
||||
"dxvk" to ComponentsManager.Choice("DD-dxvk-3.1.1+190-nightly.wcp", true, valve.version),
|
||||
),
|
||||
)
|
||||
val placed = setOf("DD-dxvk-3.1.1+190-nightly.wcp", "DD-FEXCore-2611+3-nightly.wcp")
|
||||
assertEquals(
|
||||
setOf(ge.id to "dxvk", valve.id to "fex"),
|
||||
ComponentUpdates.strays(listOf(dd, ge, valve), choices, placed).map { (p, c) -> p.id to c }.toSet(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun aProtonMissingFromDiscoveryKeepsTheHandBackPending() {
|
||||
val dd = proton("DroidDeck-Proton-10.0-7")
|
||||
val ge = proton("GE-Proton10-25")
|
||||
val placed = setOf("DD-dxvk-3.1.1+190-nightly.wcp")
|
||||
val choices = mapOf(ge.id to mapOf("dxvk" to ComponentsManager.Choice("DD-dxvk-3.1.1+190-nightly.wcp", false, ge.version)))
|
||||
assertTrue(ComponentUpdates.unseen(listOf(dd), choices, placed))
|
||||
assertFalse(ComponentUpdates.unseen(listOf(dd, ge), choices, placed))
|
||||
assertFalse(ComponentUpdates.unseen(listOf(dd), choices, emptySet()))
|
||||
}
|
||||
|
||||
@Test fun queuedAutoBuildsInOtherProtonsAreDroppedAndEverythingElseStays() {
|
||||
org.junit.Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows"))
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val state = File(context.filesDir, "components/state.json").apply { parentFile?.mkdirs() }
|
||||
fun pkg(file: String, auto: Boolean = false) = JSONObject().put("kind", "package").put("file", file).put("label", file).apply { if (auto) put("auto", true) }
|
||||
state.writeText(JSONObject().put("queued", JSONObject()
|
||||
.put("GE-Proton10-25", JSONObject().put("dxvk", pkg("DD-dxvk-3.1.1+190-nightly.wcp")).put("vkd3d", pkg("vkd3d-proton-3.0-mine.wcp"))
|
||||
.put("fex", pkg("DD-FEXCore-2611+3-nightly.wcp", auto = true)))
|
||||
.put("proton-cachyos-10.0-arm64", JSONObject().put("dxvk", pkg("DD-dxvk-3.1.1+190-nightly.wcp")))
|
||||
.put("DroidDeck-Proton-10.0-7", JSONObject().put("dxvk", pkg("DD-dxvk-3.1.1+190-nightly.wcp")))).toString())
|
||||
val placed = setOf("DD-dxvk-3.1.1+190-nightly.wcp", "DD-FEXCore-2611+3-nightly.wcp")
|
||||
assertEquals(2, ComponentsManager.dropQueuedPackages(context, placed, ComponentUpdates::ownId))
|
||||
val queued = JSONObject(state.readText()).getJSONObject("queued")
|
||||
assertEquals(setOf("vkd3d", "fex"), queued.getJSONObject("GE-Proton10-25").keys().asSequence().toSet())
|
||||
assertFalse(queued.has("proton-cachyos-10.0-arm64"))
|
||||
assertTrue(queued.getJSONObject("DroidDeck-Proton-10.0-7").has("dxvk"))
|
||||
}
|
||||
|
||||
@Test fun aQueuedSwapIsNotReplacedByTheHandBack() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val state = File(context.filesDir, "components/state.json").apply { parentFile?.mkdirs() }
|
||||
val queued = JSONObject().put("kind", "package").put("file", "vkd3d-proton-3.0-mine.wcp").put("label", "3.0")
|
||||
state.writeText(JSONObject().put("queued", JSONObject().put("GE-Proton10-25", JSONObject().put("vkd3d", queued))).toString())
|
||||
assertFalse(ComponentsManager.restoreUnlessQueued(context, "GE-Proton10-25", "vkd3d", "GE-Proton10-25"))
|
||||
val after = JSONObject(state.readText()).getJSONObject("queued").getJSONObject("GE-Proton10-25").getJSONObject("vkd3d")
|
||||
assertEquals("vkd3d-proton-3.0-mine.wcp", after.getString("file"))
|
||||
}
|
||||
|
||||
@Test fun aSourceThatFailsToListKeepsItsPreviousPackages() {
|
||||
val previous = JSONArray()
|
||||
.put(JSONObject().put("file", "DD-dxvk-arm64ec-3.1.1+198-nightly.wcp").put("release", ComponentsManager.DD_NIGHTLY))
|
||||
.put(JSONObject().put("file", "dxvk-3.1.1-linux.wcp").put("release", "Dxvk-Linux"))
|
||||
val into = JSONArray()
|
||||
ComponentsManager.keepSource(previous, ComponentsManager.DD_NIGHTLY, into)
|
||||
assertEquals(1, into.length())
|
||||
assertEquals("DD-dxvk-arm64ec-3.1.1+198-nightly.wcp", into.getJSONObject(0).getString("file"))
|
||||
ComponentsManager.keepSource(null, ComponentsManager.DD_NIGHTLY, into)
|
||||
assertEquals(1, into.length())
|
||||
}
|
||||
|
||||
@Test fun everyBuildAutoEverPlacedCountsAsItsOwn() {
|
||||
val state = JSONObject().put("lines", JSONObject()
|
||||
.put("dxvk", JSONObject().put("current", "b.wcp").put("previous", ComponentUpdates.ORIGINAL)
|
||||
.put("owned", JSONArray().put("a.wcp").put("b.wcp")).put("bad", JSONArray().put("c.wcp")))
|
||||
.put("fex", JSONObject().put("current", "f2.wcp").put("previous", "f1.wcp")))
|
||||
assertEquals(setOf("a.wcp", "b.wcp", "c.wcp", "f1.wcp", "f2.wcp"), ComponentUpdater.placed(state))
|
||||
assertEquals(emptySet<String>(), ComponentUpdater.placed(JSONObject()))
|
||||
}
|
||||
|
||||
@Test fun aBuildAutoMovedPastStillCountsUntilTheHandBackCompletes() {
|
||||
val first = JSONObject().put("lines", JSONObject().put("dxvk", JSONObject().put("current", "a.wcp")))
|
||||
val remembered = ComponentUpdater.placed(first)
|
||||
val later = JSONObject().put(ComponentUpdater.HAND_BACK_FILES, JSONArray(remembered.sorted()))
|
||||
.put("lines", JSONObject().put("dxvk", JSONObject().put("current", "c.wcp").put("previous", "b.wcp")))
|
||||
val placed = ComponentUpdater.placed(later)
|
||||
assertEquals(setOf("a.wcp", "b.wcp", "c.wcp"), placed)
|
||||
val proton = proton("GE-Proton10-25")
|
||||
val choices = mapOf(proton.id to mapOf("dxvk" to ComponentsManager.Choice("a.wcp", false, proton.version)))
|
||||
assertTrue(ComponentUpdates.unseen(emptyList(), choices, placed))
|
||||
}
|
||||
|
||||
@Test fun onlyTheCurrentAndThePreviousBuildAndUserSelectionsAreKept() {
|
||||
assertEquals(setOf("b.wcp", "a.wcp", "mine.wcp"), ComponentUpdates.keep("b.wcp", "a.wcp", setOf("mine.wcp")))
|
||||
assertEquals(setOf("b.wcp"), ComponentUpdates.keep("b.wcp", ComponentUpdates.ORIGINAL, emptySet()))
|
||||
|
||||
Reference in new issue
Block a user