diff --git a/buildSrc/src/main/kotlin/config-kotlin.gradle.kts b/buildSrc/src/main/kotlin/config-kotlin.gradle.kts index e8e4e8e0e..d21258b9c 100644 --- a/buildSrc/src/main/kotlin/config-kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/config-kotlin.gradle.kts @@ -68,6 +68,7 @@ testing { useKotlinTest(embeddedKotlinVersion) dependencies { implementation("org.junit.jupiter:junit-jupiter-engine:5.10.1") + implementation("org.junit.jupiter:junit-jupiter-params:5.10.1") implementation("org.junit.platform:junit-platform-launcher:1.10.1") } diff --git a/paperweight-core/src/main/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApply.kt b/paperweight-core/src/main/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApply.kt index 8df9780f7..a86b069b7 100644 --- a/paperweight-core/src/main/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApply.kt +++ b/paperweight-core/src/main/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApply.kt @@ -38,6 +38,19 @@ import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.options.Option +/** + * Patch roulette apply allows selection a set of patches from the remote patch roulette instance to work on. + * To control the amount/strategy of selecting these patches, the `--select` option can be passed. + * The following options are available: + * - `n`: Any positive integer number. + * Paperweight will select *up to* `n` patches from the current package the user is working in. + * If the package offers 0 patches, a new package will be chosen. + * If the package offers `m` patches, and `m < n`, only `m` patches will be returned. + * - `n!`: Any positive integer number followed by a `!`. + * Paperweight will select `n` patches, prioritizing patches in the current package. + * The only time less than `n` patches will be selected is if the entire patch roulette + * instance has less than `n` patches available, in which case all of them will be selected. + */ abstract class PatchRouletteApply : AbstractPatchRouletteTask() { @get:InputDirectory @@ -87,7 +100,7 @@ abstract class PatchRouletteApply : AbstractPatchRouletteTask() { } var tries = 5 - var patches = listOf() + var patches: List val patchSelectionStrategy = patchSelectionStrategy .map { PatchSelectionStrategy.parse(it) } .getOrElse(PatchSelectionStrategy.NumericInPackage(5)) @@ -166,14 +179,33 @@ abstract class PatchRouletteApply : AbstractPatchRouletteTask() { data class Config(val skip: List, val suggestedPackage: Path?, val currentPatches: List) sealed interface PatchSelectionStrategy { - data class NumericInPackage(val count: Int) : PatchSelectionStrategy { + data class NumericInPackage(val count: Int, val enforceCount: Boolean = false) : PatchSelectionStrategy { override fun select(config: Config, available: List): Pair> { + return this.select(config, available, this.count) + } + + fun select(config: Config, available: List, count: Int): Pair> { if (config.suggestedPackage != null) { val possiblePatches = available.filter { it.parent.equals(config.suggestedPackage) }.take(count) - if (possiblePatches.isNotEmpty()) return config to possiblePatches + if (possiblePatches.isNotEmpty()) { + if (!enforceCount) return config to possiblePatches + // The patches we found satisfy the count param or the entire available set simply does not offer enough patches. + if (possiblePatches.size >= count || possiblePatches.size == available.size) return config to possiblePatches + + // The patches found in the package do not satisfy the requested count *and* the strategy was configured to enforce the + // count. Re-select from a new package and different patch set, add them to our already fetched patches and update the + // config, as the last suggested package is the one to suggest in potentially next runs. + val additionalPatches = select( + config.copy(suggestedPackage = null), + available.filter { !possiblePatches.contains(it) }, + count - possiblePatches.size + ) + + return additionalPatches.first to possiblePatches + additionalPatches.second + } } - return select(config.copy(suggestedPackage = available.first().parent), available) + return select(config.copy(suggestedPackage = available.first().parent), available, count) } } @@ -182,7 +214,10 @@ abstract class PatchRouletteApply : AbstractPatchRouletteTask() { companion object { fun parse(input: String): PatchSelectionStrategy { try { - return NumericInPackage(input.toInt()) + return when { + input.endsWith("!") -> NumericInPackage(input.substring(0, input.length - 1).toInt(), true) + else -> NumericInPackage(input.toInt()) + } } catch (e: Exception) { throw PaperweightException("Failed to parse patch selection strategy $input", e) } diff --git a/paperweight-core/src/test/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApplyTest.kt b/paperweight-core/src/test/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApplyTest.kt new file mode 100644 index 000000000..dca09e16e --- /dev/null +++ b/paperweight-core/src/test/kotlin/io/papermc/paperweight/core/tasks/patchroulette/PatchRouletteApplyTest.kt @@ -0,0 +1,150 @@ +/* + * paperweight is a Gradle plugin for the PaperMC project. + * + * Copyright (c) 2023 Kyle Wood (DenWav) + * Contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 only, no later versions. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +package io.papermc.paperweight.core.tasks.patchroulette + +import kotlin.io.path.* +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +class PatchRouletteApplyTest { + + @Test + fun `test patch strategy parsing non enforced`() { + val strategy = PatchRouletteApply.PatchSelectionStrategy.parse("10") + when (strategy) { + is PatchRouletteApply.PatchSelectionStrategy.NumericInPackage -> { + assertEquals(10, strategy.count) + assertFalse(strategy.enforceCount) + } + } + } + + @Test + fun `test patch strategy parsing enforced`() { + val strategy = PatchRouletteApply.PatchSelectionStrategy.parse("20!") + when (strategy) { + is PatchRouletteApply.PatchSelectionStrategy.NumericInPackage -> { + assertEquals(20, strategy.count) + assertTrue(strategy.enforceCount) + } + } + } + + @ParameterizedTest + @MethodSource("testPatchSelectionSource") + fun `test patch selection`( + strategy: PatchRouletteApply.PatchSelectionStrategy, + allPatches: List, + expectedPatchBatches: List> + ) { + var config = PatchRouletteApply.Config(listOf(), null, listOf()) + var availablePatches = allPatches.map { Path(it) } + for (batch in expectedPatchBatches) { + val selectionResult = strategy.select(config, availablePatches) + assertEquals(batch.map { Path(it) }, selectionResult.second) + + config = selectionResult.first + availablePatches = availablePatches - selectionResult.second + } + + assertTrue(availablePatches.isEmpty(), "Patches remained after exhausting expected batches") + } + + companion object { + @JvmStatic + fun testPatchSelectionSource(): Collection = listOf( + Arguments.of( + PatchRouletteApply.PatchSelectionStrategy.NumericInPackage(2), + mockAvailablePatches(), + listOf( + listOf("io/papermc/paper/block/Block.java", "io/papermc/paper/block/BlockData.java"), + listOf("io/papermc/paper/block/BlockState.java"), + listOf("io/papermc/paper/entity/Entity.java") + ) + ), + Arguments.of( + PatchRouletteApply.PatchSelectionStrategy.NumericInPackage(5), + mockAvailablePatches(), + listOf( + listOf("io/papermc/paper/block/Block.java", "io/papermc/paper/block/BlockData.java", "io/papermc/paper/block/BlockState.java"), + listOf("io/papermc/paper/entity/Entity.java") + ) + ), + Arguments.of( + PatchRouletteApply.PatchSelectionStrategy.NumericInPackage(2, true), + mockAvailablePatches(), + listOf( + listOf("io/papermc/paper/block/Block.java", "io/papermc/paper/block/BlockData.java"), + listOf("io/papermc/paper/block/BlockState.java", "io/papermc/paper/entity/Entity.java"), + ) + ), + Arguments.of( + PatchRouletteApply.PatchSelectionStrategy.NumericInPackage(5, true), + mockAvailablePatches(), + listOf( + listOf( + "io/papermc/paper/block/Block.java", + "io/papermc/paper/block/BlockData.java", + "io/papermc/paper/block/BlockState.java", + "io/papermc/paper/entity/Entity.java" + ), + ) + ), + Arguments.of( + PatchRouletteApply.PatchSelectionStrategy.NumericInPackage(4, true), + listOf( + "io/papermc/paper/block/Block.java", + "io/papermc/paper/block/BlockData.java", + "io/papermc/paper/block/BlockState.java", + "io/papermc/paper/entity/Entity.java", + "io/papermc/paper/entity/Entity2.java", + "io/papermc/paper/entity/Entity3.java" + ), + listOf( + listOf( + "io/papermc/paper/block/Block.java", + "io/papermc/paper/block/BlockData.java", + "io/papermc/paper/block/BlockState.java", + "io/papermc/paper/entity/Entity.java", + ), + listOf( + "io/papermc/paper/entity/Entity2.java", + "io/papermc/paper/entity/Entity3.java" + ) + ) + ) + ) + + fun mockAvailablePatches() = listOf( + "io/papermc/paper/block/Block.java", + "io/papermc/paper/block/BlockData.java", + "io/papermc/paper/block/BlockState.java", + "io/papermc/paper/entity/Entity.java" + ) + } +}