From 81803a29afcaef516defb7334726d761ba3c77ce Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 00:44:53 +0800 Subject: [PATCH 01/13] test: cover internal utilities --- .../internal/DefaultDependencyFilter.kt | 2 +- .../internal/MinimizeDependencyFilter.kt | 2 +- .../plugins/shadow/internal/R8Minimizer.kt | 12 +- .../shadow/internal/RelocatorRemapper.kt | 12 +- .../plugins/shadow/ShadowPropertiesTest.kt | 22 ++ .../shadow/internal/BytecodeRemappingTest.kt | 18 ++ .../shadow/internal/DependencyFiltersTest.kt | 120 ++++++++++ .../internal/DuplicatesStrategyCheckerTest.kt | 26 +++ .../shadow/internal/MinimizeSpecsTest.kt | 80 +++++++ .../shadow/internal/R8MinimizerTest.kt | 213 ++++++++++++++++++ .../internal/ReproduciblePropertiesTest.kt | 19 ++ .../shadow/internal/UnusedTrackerTest.kt | 108 +++++++++ .../shadow/internal/ZipEntryValidationTest.kt | 27 +++ 13 files changed, 646 insertions(+), 15 deletions(-) create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index c743b14b8..580213da4 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal class DefaultDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - override fun resolve( + public override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt index e22a33bff..8f9414079 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal open class MinimizeDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - override fun resolve( + public override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index ac661970b..65db4cc13 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -127,7 +127,7 @@ internal class R8Minimizer( } } - private fun createRules( + internal fun createRules( inputJar: File, r8Args: List, extractedRulesFile: File, @@ -151,14 +151,14 @@ internal class R8Minimizer( return rules.toList() } - private fun shouldDisableOptimization(r8Args: List): Boolean { + internal fun shouldDisableOptimization(r8Args: List): Boolean { return !r8Spec.optimizationEnabled.get() && (r8Spec.obfuscationEnabled.get() || DefaultR8Spec.NO_MINIFICATION_ARG in r8Args) } // Project classes are the public surface of the shadowed jar, even when nothing in the input jar // refers to every class directly. - private fun sourceKeepRules(inputJar: File): List { + internal fun sourceKeepRules(inputJar: File): List { val jarClasses = jarClassEntries(inputJar) return sourceSetsClassesDirs .asSequence() @@ -182,7 +182,7 @@ internal class R8Minimizer( // Keep dependencies users explicitly excluded from minimization, matching the existing // minimize { exclude(...) } contract for the default analyzer. - private fun keptDependencyRules(inputJar: File): List { + internal fun keptDependencyRules(inputJar: File): List { val jarClasses = jarClassEntries(inputJar) return keptDependencyFiles .asSequence() @@ -198,7 +198,7 @@ internal class R8Minimizer( // Service descriptors are usage edges for downstream ServiceLoader calls, so keep the service // interface and every listed provider even if R8 sees no direct references. - private fun serviceKeepRules(inputJar: File): List { + internal fun serviceKeepRules(inputJar: File): List { val rules = linkedSetOf() JarFile(inputJar).use { jarFile -> jarFile @@ -275,7 +275,7 @@ internal class R8Minimizer( // R8 writes a fresh jar, so rewrite it through Shadow's archive settings to preserve // reproducible ordering, timestamps, compression, zip64, and metadata charset behavior. - private fun normalizeJar(inputJar: File, outputJar: File) { + internal fun normalizeJar(inputJar: File, outputJar: File) { val entries = JarFile(inputJar).use { jarFile -> jarFile diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt index 3d239f543..c9daa0df9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt @@ -23,18 +23,16 @@ internal fun FileCopyDetails.remapClass(relocators: Set): ByteArray = // original class names. This is not a problem at runtime (because these entries in the constant // pool are never used), but confuses some tools such as Felix's maven-bundle-plugin that use // the constant pool to determine the dependencies of a class. - val cw = ClassWriter(0) - val cr = ClassReader(bytes) - val cv = ClassRemapper(cw, remapper) - try { + val cw = ClassWriter(0) + val cr = ClassReader(bytes) + val cv = ClassRemapper(cw, remapper) cr.accept(cv, ClassReader.EXPAND_FRAMES) + // If we didn't need to change anything, keep the original bytes as-is. + if (modified) cw.toByteArray() else bytes } catch (t: Throwable) { throw GradleException("Error in ASM processing class $path", t) } - - // If we didn't need to change anything, keep the original bytes as-is. - if (modified) cw.toByteArray() else bytes } private class RelocatorRemapper( diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 7ce1be2f8..236b15268 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -97,6 +97,28 @@ class ShadowPropertiesTest { ) } + @Test + fun inheritManifestFromMultipleSourcesWithMergeAction() = + with(project) { + plugins.apply(JavaPlugin::class.java) + val jar1 = tasks.register("manifestSource1", Jar::class.java) + val jar2 = tasks.register("manifestSource2", Jar::class.java) + jar1.configure { it.manifest.attributes["shared"] = "one" } + jar2.configure { it.manifest.attributes["shared"] = "two" } + + @Suppress("DEPRECATION") + tasks.shadowJar.configure { + it.manifest.inheritFrom(jar1.get().manifest, jar2.get().manifest) { merge -> + merge.eachEntry { details -> + if (details.key == "shared") details.exclude() + } + } + } + + assertThat(tasks.shadowJar.get().manifest.effectiveManifest.attributes) + .containsOnly("Manifest-Version" to "1.0") + } + @Test fun inheritManifestMainClassFromJar() = with(project) { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt index f3bc50921..d7840ada6 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt @@ -12,8 +12,10 @@ import java.nio.file.Path import kotlin.io.path.copyTo import kotlin.io.path.createParentDirectories import kotlin.reflect.KClass +import org.gradle.api.GradleException import org.gradle.api.file.FileCopyDetails import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -60,6 +62,22 @@ class BytecodeRemappingTest { assertThat(result).isEqualTo(details.file.readBytes()) } + @Test + fun asmFailureIsWrappedWithClassPath() { + val path = "broken/Example.class" + val file = tempDir.resolve("broken.class").toFile().apply { writeText("not bytecode") } + val details = + object : FileCopyDetails by noOpDelegate() { + override fun getPath(): String = path + + override fun getFile(): File = file + } + + val failure = assertThrows { details.remapClass(relocators) } + + assertThat(failure.message).isEqualTo("Error in ASM processing class $path") + } + @Test fun classNameIsRelocated() { val result = fixtureSubjectDetails.remapClass(relocators) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt new file mode 100644 index 000000000..fd6bdb9d6 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt @@ -0,0 +1,120 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.containsOnly +import assertk.assertions.isEmpty +import java.lang.reflect.Proxy +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.specs.Spec +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test + +class DependencyFiltersTest { + private val project = ProjectBuilder.builder().build() + + @Test + fun defaultFilterTraversesIncludedAndExcludedBranches() { + val includedChild = dependency("included-child") + val excludedChild = dependency("excluded-child") + val includedRoot = dependency("included-root", children = setOf(includedChild)) + val excludedRoot = dependency("excluded-root", children = setOf(excludedChild)) + val filter = DefaultDependencyFilter(project) + filter.exclude(named("excluded-root")) + val included = mutableSetOf() + val excluded = mutableSetOf() + + filter.resolve(setOf(includedRoot, excludedRoot), included, excluded) + + assertThat(included).containsOnly(includedRoot, includedChild, excludedChild) + assertThat(excluded).containsOnly(excludedRoot) + } + + @Test + fun includeRulesExcludeNonMatchingDependencies() { + val includedRoot = dependency("included") + val otherRoot = dependency("other") + val filter = DefaultDependencyFilter(project) + filter.include(named("included")) + val included = mutableSetOf() + val excluded = mutableSetOf() + + filter.resolve(setOf(includedRoot, otherRoot), included, excluded) + + assertThat(included).containsOnly(includedRoot) + assertThat(excluded).containsOnly(otherRoot) + } + + @Test + fun excludeWinsWhenDependencyMatchesIncludeAndExclude() { + val root = dependency("root") + val filter = DefaultDependencyFilter(project) + filter.include(named("root")) + filter.exclude(named("root")) + val included = mutableSetOf() + val excluded = mutableSetOf() + + filter.resolve(setOf(root), included, excluded) + + assertThat(included).isEmpty() + assertThat(excluded).containsOnly(root) + } + + @Test + fun minimizeFilterExcludesChildrenOfExcludedParent() { + lateinit var excludedRoot: ResolvedDependency + val child = dependency("child", parents = { setOf(excludedRoot) }) + excludedRoot = dependency("excluded-root", children = setOf(child)) + val filter = MinimizeDependencyFilter(project) + filter.exclude(named("excluded-root")) + val included = mutableSetOf() + val excluded = mutableSetOf() + + filter.resolve(setOf(excludedRoot), included, excluded) + + assertThat(included).isEmpty() + assertThat(excluded).containsOnly(excludedRoot, child) + } + + @Test + fun diamondGraphIsVisitedOnceAndIncluded() { + val leaf = dependency("leaf") + val left = dependency("left", children = setOf(leaf)) + val right = dependency("right", children = setOf(leaf)) + val filter = DefaultDependencyFilter(project) + val included = mutableSetOf() + val excluded = mutableSetOf() + + filter.resolve(setOf(left, right), included, excluded) + + assertThat(included).containsOnly(left, right, leaf) + assertThat(excluded).isEmpty() + } + + private fun named(name: String) = Spec { it.moduleName == name } + + private fun dependency( + name: String, + children: Set = emptySet(), + parents: () -> Set = { emptySet() }, + ): ResolvedDependency { + lateinit var proxy: ResolvedDependency + proxy = + Proxy.newProxyInstance( + javaClass.classLoader, + arrayOf(ResolvedDependency::class.java), + ) { _, method, args -> + when (method.name) { + "getModuleGroup" -> "test" + "getModuleName" -> name + "getModuleVersion" -> "1.0" + "getChildren" -> children + "getParents" -> parents() + "hashCode" -> System.identityHashCode(proxy) + "equals" -> proxy === args?.firstOrNull() + "toString" -> name + else -> null + } + } as ResolvedDependency + return proxy + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt index 257abe320..5ac45de99 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt @@ -5,6 +5,7 @@ import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.transformers.BaseTransformerTest.Companion.canTransformResource import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer.Companion.create +import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File import java.net.JarURLConnection @@ -16,6 +17,9 @@ import kotlin.io.path.isRegularFile import kotlin.io.path.toPath import kotlin.io.path.walk import kotlin.reflect.full.isSubclassOf +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileCopyDetails +import org.gradle.api.file.FileTreeElement import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -42,6 +46,28 @@ class DuplicatesStrategyCheckerTest { onCheckDupStrategyInvoked = null } } + + @Test + fun checkerHandlesEveryEarlyReturnAndDuplicatesStrategy() { + var invocationCount = 0 + onCheckDupStrategyInvoked = { invocationCount++ } + try { + ResourceTransformer.checkDupStrategy(false, noOpDelegate()) + ResourceTransformer.checkDupStrategy(true, noOpDelegate()) + DuplicatesStrategy.entries.forEach { strategy -> + val details = + object : FileCopyDetails by noOpDelegate() { + override fun getPath(): String = "duplicate.txt" + + override fun getDuplicatesStrategy(): DuplicatesStrategy = strategy + } + ResourceTransformer.checkDupStrategy(true, details) + } + assertThat(invocationCount).isEqualTo(2 + DuplicatesStrategy.entries.size) + } finally { + onCheckDupStrategyInvoked = null + } + } } private fun getTransformerClasses(): List> { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt new file mode 100644 index 000000000..afd087e31 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt @@ -0,0 +1,80 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNull +import assertk.assertions.isSameInstanceAs +import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.tasks.MinimizeTool +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test + +class MinimizeSpecsTest { + private val project = ProjectBuilder.builder().build() + + @Test + fun defaultMinimizeSpecUsesDependencyAnalyzer() { + val spec = project.objects.newInstance(DefaultMinimizeSpec::class.java, project) + + assertThat(spec.tool.get()).isEqualTo(MinimizeTool.DEPENDENCY_ANALYZER) + assertThat(spec.r8SpecForInputs).isNull() + } + + @Test + fun r8ConfiguresToolAndExposesSameSpecAsInput() { + val spec = project.objects.newInstance(DefaultMinimizeSpec::class.java, project) + lateinit var configured: Any + + spec.r8 { configured = it } + + assertThat(spec.tool.get()).isEqualTo(MinimizeTool.R8) + assertThat(spec.r8SpecForInputs).isSameInstanceAs(configured) + assertThat(spec.r8Spec).isSameInstanceAs(configured) + } + + @Test + fun defaultR8SpecIsShrinkOnly() { + val spec = project.objects.newInstance(DefaultR8Spec::class.java) + + assertThat(spec.args.get()).containsExactly(DefaultR8Spec.NO_MINIFICATION_ARG) + assertThat(spec.obfuscationEnabled.get()).isFalse() + assertThat(spec.optimizationEnabled.get()).isFalse() + assertThat(spec.keepRules.get()).isEmpty() + assertThat(spec.keepRuleFiles.files).isEmpty() + } + + @Test + fun enablingObfuscationRemovesDefaultArgument() { + val spec = project.objects.newInstance(DefaultR8Spec::class.java) + + spec.enableObfuscation() + + assertThat(spec.args.get()).isEmpty() + assertThat(spec.obfuscationEnabled.get()).isTrue() + assertThat(spec.optimizationEnabled.get()).isFalse() + } + + @Test + fun enablingOptimizationOnlyChangesOptimizationFlag() { + val spec = project.objects.newInstance(DefaultR8Spec::class.java) + + spec.enableOptimization() + + assertThat(spec.args.get()).containsExactly(DefaultR8Spec.NO_MINIFICATION_ARG) + assertThat(spec.obfuscationEnabled.get()).isFalse() + assertThat(spec.optimizationEnabled.get()).isTrue() + } + + @Test + fun explicitArgumentsTakePrecedenceOverChangedDefaults() { + val spec = project.objects.newInstance(DefaultR8Spec::class.java) + spec.args.set(listOf("--debug")) + + spec.enableObfuscation() + + assertThat(spec.args.get()).containsExactly("--debug") + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt new file mode 100644 index 000000000..fadfd5052 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt @@ -0,0 +1,213 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.containsOnly +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONSTANT_TIME_FOR_ZIP_ENTRIES +import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate +import java.io.File +import java.nio.file.Path +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.GradleException +import org.gradle.api.logging.Logging +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.gradle.jvm.toolchain.JavaLauncher +import org.gradle.process.ExecOperations +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir + +class R8MinimizerTest { + @TempDir lateinit var tempDir: Path + private val project = ProjectBuilder.builder().build() + private val r8Spec = project.objects.newInstance(DefaultR8Spec::class.java) + + @Test + fun minimizeRequiresR8Classpath() { + val failure = + assertThrows { + minimizer().minimize(tempDir.resolve("input.jar").toFile(), tempDir.toFile()) + } + + assertThat(failure.message) + .isEqualTo( + "R8 minimization requires a non-empty R8 classpath. Apply the Shadow plugin or configure the shadowR8 configuration." + ) + } + + @Test + fun optimizationRuleReflectsR8Options() { + val minimizer = minimizer() + + assertThat(minimizer.shouldDisableOptimization(r8Spec.args.get())).isTrue() + r8Spec.enableOptimization() + assertThat(minimizer.shouldDisableOptimization(r8Spec.args.get())).isFalse() + } + + @Test + fun sourceRulesRelocateFilterDeduplicateAndSortClasses() { + val source1 = tempDir.resolve("source1").toFile() + val source2 = tempDir.resolve("source2").toFile() + source1.resolve("old/B.class").writeBytesCreatingParents() + source1.resolve("old/A.class").writeBytesCreatingParents() + source1.resolve("module-info.class").writeBytesCreatingParents() + source2.resolve("old/A.class").writeBytesCreatingParents() + source2.resolve("old/Missing.class").writeBytesCreatingParents() + val input = tempDir.resolve("input.jar").toFile() + input.writeJar("new/A.class" to byteArrayOf(), "new/B.class" to byteArrayOf()) + val minimizer = + minimizer( + sourceDirs = listOf(source1, source2, tempDir.resolve("missing").toFile()), + relocators = listOf(SimpleRelocator("old", "new")), + ) + + assertThat(minimizer.sourceKeepRules(input)) + .containsExactly( + "-keep,includedescriptorclasses class new.A { *; }", + "-keep,includedescriptorclasses class new.B { *; }", + ) + } + + @Test + fun dependencyRulesReadDirectoriesAndJarsAndOnlyKeepClassesInInput() { + val directory = tempDir.resolve("dependency").toFile() + directory.resolve("dep/B.class").writeBytesCreatingParents() + directory.resolve("dep/Missing.class").writeBytesCreatingParents() + val dependencyJar = tempDir.resolve("dependency.jar").toFile() + dependencyJar.writeJar( + "dep/A.class" to byteArrayOf(), + "module-info.class" to byteArrayOf(), + "resource.txt" to byteArrayOf(), + ) + val input = tempDir.resolve("input.jar").toFile() + input.writeJar("dep/A.class" to byteArrayOf(), "dep/B.class" to byteArrayOf()) + + assertThat(minimizer(keptFiles = listOf(directory, dependencyJar)).keptDependencyRules(input)) + .containsExactly("-keep class dep.A { *; }", "-keep class dep.B { *; }") + } + + @Test + fun serviceRulesIgnoreCommentsMalformedNamesAndDuplicates() { + val input = tempDir.resolve("services.jar").toFile() + input.writeJar( + "META-INF/services/example.Service" to + "# heading\nexample.Provider # comment\ninvalid-name\nexample.Provider\n\n".toByteArray(), + "META-INF/services/invalid-name" to "example.Other\n".toByteArray(), + ) + + assertThat(minimizer().serviceKeepRules(input)) + .containsExactly( + "-keep class example.Service { *; }", + "-keep class example.Provider { *; }", + "-keep class example.Other { *; }", + ) + } + + @Test + fun createRulesCombinesSourcesInStableOrderAndDeduplicates() { + val input = tempDir.resolve("input.jar").toFile() + input.writeJar() + val extracted = + tempDir.resolve("extracted.pro").toFile().apply { + writeText("-keep class extracted.One\n-keep class duplicate.Rule\n") + } + val first = + tempDir.resolve("a.pro").toFile().apply { + writeText("-keep class duplicate.Rule\n-keep class file.One\n") + } + val second = tempDir.resolve("b.pro").toFile().apply { writeText("-keep class file.Two\n") } + r8Spec.keepRuleFiles.from(second, tempDir.toFile(), first) + r8Spec.keepRules.addAll("-keep class file.One", "-keep class inline.One") + + assertThat(minimizer().createRules(input, r8Spec.args.get(), extracted)) + .containsExactly( + DefaultR8Spec.DONT_OPTIMIZE_RULE, + "-keep class extracted.One", + "-keep class duplicate.Rule", + "-keep class file.One", + "-keep class file.Two", + "-keep class inline.One", + ) + } + + @Test + fun normalizeJarAddsParentsSortsAndNormalizesTimestamps() { + val input = tempDir.resolve("input.jar").toFile() + input.writeJar("z/B.txt" to "b".toByteArray(), "a/A.txt" to "a".toByteArray()) + val output = tempDir.resolve("output.jar").toFile() + + minimizer(reproducibleOrder = true, preserveTimestamps = false).normalizeJar(input, output) + + JarFile(output).use { jar -> + assertThat(jar.entries().toList().map { it.name }) + .containsExactly("a/", "a/A.txt", "z/", "z/B.txt") + assertThat(jar.entries().toList().map { it.time }.distinct()) + .containsOnly(CONSTANT_TIME_FOR_ZIP_ENTRIES) + assertThat(jar.getInputStream(jar.getJarEntry("a/A.txt")).readBytes().decodeToString()) + .isEqualTo("a") + } + } + + @Test + fun normalizeJarSupportsStoredEntriesAndPreservedTimes() { + val input = tempDir.resolve("input.jar").toFile() + input.writeJar("file.txt" to "content".toByteArray()) + val output = tempDir.resolve("output.jar").toFile() + + minimizer(compression = ZipEntryCompression.STORED, preserveTimestamps = true) + .normalizeJar(input, output) + + JarFile(output).use { jar -> + val entry = jar.getJarEntry("file.txt") + assertThat(entry.method).isEqualTo(ZipOutputStream.STORED) + assertThat(entry.time >= 0).isTrue() + } + } + + private fun minimizer( + sourceDirs: Iterable = emptyList(), + keptFiles: Iterable = emptyList(), + relocators: Iterable = emptyList(), + preserveTimestamps: Boolean = false, + reproducibleOrder: Boolean = true, + compression: ZipEntryCompression = ZipEntryCompression.DEFLATED, + ) = + R8Minimizer( + execOperations = noOpDelegate(), + logger = Logging.getLogger(R8MinimizerTest::class.java), + r8Classpath = project.files(), + r8Spec = r8Spec, + javaLauncher = project.objects.property(JavaLauncher::class.java), + sourceSetsClassesDirs = sourceDirs, + keptDependencyFiles = keptFiles, + relocators = relocators, + preserveFileTimestamps = preserveTimestamps, + reproducibleFileOrder = reproducibleOrder, + zip64 = false, + entryCompression = compression, + metadataCharset = null, + ) + + private fun File.writeBytesCreatingParents() { + parentFile.mkdirs() + writeBytes(byteArrayOf()) + } + + private fun File.writeJar(vararg entries: Pair) { + JarOutputStream(outputStream()).use { output -> + entries.forEach { (name, bytes) -> + output.putNextEntry(JarEntry(name)) + output.write(bytes) + output.closeEntry() + } + } + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ReproduciblePropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ReproduciblePropertiesTest.kt index 42b6b08eb..ea31df77a 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ReproduciblePropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ReproduciblePropertiesTest.kt @@ -76,6 +76,25 @@ class ReproduciblePropertiesTest { ) } + @ParameterizedTest + @MethodSource("generalCharsetsProvider") + fun escapesSpecialCharacters(charset: Charset) { + val output = + ReproducibleProperties() + .also { properties -> + properties[" leading:=#!"] = "line1\nline2\t\\" + } + .writeToString(charset) + + assertThat(output) + .isEqualTo( + """ + |\ leading\:\=\#\!=line1\nline2\t\\ + |""" + .trimMargin() + ) + } + private companion object Companion { @JvmStatic fun generalCharsetsProvider() = diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt new file mode 100644 index 000000000..5b451c77b --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt @@ -0,0 +1,108 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.containsOnly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isSameInstanceAs +import assertk.assertions.isTrue +import java.io.File +import java.nio.file.Path +import javax.tools.ToolProvider +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.LibraryElements +import org.gradle.api.attributes.Usage +import org.gradle.api.plugins.JavaLibraryPlugin +import org.gradle.api.plugins.JavaPlugin +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +class UnusedTrackerTest { + @TempDir lateinit var tempDir: Path + + @Test + fun getApiJarsIsEmptyWithoutApiConfiguration() { + val project = ProjectBuilder.builder().build() + + assertThat(project.getApiJars().get()).isEmpty() + } + + @Test + fun getApiJarsCreatesAndReusesResolvableApiView() { + val project = ProjectBuilder.builder().build() + project.plugins.apply(JavaLibraryPlugin::class.java) + + project.getApiJars().get() + val configuration = project.configurations.getByName("shadowMinimizeApi") + val second = project.getApiJars() + + assertThat(configuration.isCanBeResolved).isTrue() + assertThat(configuration.isCanBeConsumed).isFalse() + assertThat(configuration.extendsFrom) + .containsOnly(project.configurations.getByName(JavaPlugin.API_CONFIGURATION_NAME)) + assertThat(configuration.attributes.getAttribute(Usage.USAGE_ATTRIBUTE)?.name) + .isEqualTo(Usage.JAVA_API) + assertThat(configuration.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)?.name) + .isEqualTo(Category.LIBRARY) + assertThat( + configuration.attributes.getAttribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE)?.name + ) + .isEqualTo(LibraryElements.JAR) + assertThat(project.configurations.getByName("shadowMinimizeApi")) + .isSameInstanceAs(configuration) + // Creating the second provider must not register another configuration. + assertThat(second.isPresent).isTrue() + } + + @Test + fun tracksUnusedClassesAndOnlyAddsSelectedDependencies() { + val dependencyClasses = tempDir.resolve("dependency-classes").toFile() + compile( + dependencyClasses, + "dep.Used" to "package dep; public class Used {}", + "dep.Unused" to "package dep; public class Unused {}", + ) + val projectClasses = tempDir.resolve("project-classes").toFile() + compile( + projectClasses, + "app.Main" to "package app; public class Main { dep.Used used; }", + classpath = dependencyClasses, + ) + val project = ProjectBuilder.builder().build() + val tracker = + UnusedTracker( + sourceSetsClassesDirs = listOf(projectClasses), + classJars = project.files(), + toMinimize = project.files(dependencyClasses), + ) + + tracker.addDependency(tempDir.resolve("not-selected").toFile()) + tracker.addDependency(dependencyClasses) + + assertThat(tracker.findUnused()).containsOnly("dep.Unused") + } + + private fun compile( + output: File, + vararg sources: Pair, + classpath: File? = null, + ) { + output.mkdirs() + val sourceFiles = sources.map { (className, source) -> + tempDir.resolve("sources/${className.replace('.', '/')}.java").toFile().apply { + parentFile.mkdirs() + writeText(source) + } + } + val arguments = buildList { + addAll(listOf("-d", output.absolutePath)) + if (classpath != null) addAll(listOf("-classpath", classpath.absolutePath)) + addAll(sourceFiles.map(File::getAbsolutePath)) + } + val result = + ToolProvider.getSystemJavaCompiler().run(null, null, null, *arguments.toTypedArray()) + assertThat(result).isEqualTo(0) + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt index 0bdae4cd4..7bb46a3a2 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt @@ -2,12 +2,39 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import java.nio.charset.StandardCharsets +import java.util.Properties import org.gradle.api.GradleException import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class ZipEntryValidationTest { + @Test + fun zipEntryUsesRequestedOrReproducibleTimestampAndAppliesConfiguration() { + val preserved = zipEntry("file", lastModified = 1234) { comment = "configured" } + val reproducible = zipEntry("file", preserveLastModified = false, lastModified = 1234) + val missingTimestamp = zipEntry("file", lastModified = -1) + + assertThat(preserved.time).isEqualTo(1234) + assertThat(preserved.comment).isEqualTo("configured") + assertThat(reproducible.time).isEqualTo(missingTimestamp.time) + } + + @Test + fun propertiesInputStreamUsesRequestedCharsetAndComments() { + val properties = Properties().apply { setProperty("greeting", "你好") } + + val content = properties.inputStream(StandardCharsets.UTF_8, "header").reader().readText() + + assertThat(content.startsWith("#header")).isTrue() + assertThat(content) + .isEqualTo(content.toByteArray(StandardCharsets.UTF_8).toString(StandardCharsets.UTF_8)) + val loaded = Properties().apply { load(content.reader()) } + assertThat(loaded.getProperty("greeting")).isEqualTo("你好") + } + @Test fun validZipEntryNamesDoNotThrow() { val validNames = From 234b7822d747ea0b5a9a0b8950b9ed303f33c96f Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 00:49:20 +0800 Subject: [PATCH 02/13] Remove public modifiers --- .../gradle/plugins/shadow/internal/DefaultDependencyFilter.kt | 2 +- .../gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 580213da4..c743b14b8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal class DefaultDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - public override fun resolve( + override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt index 8f9414079..e22a33bff 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal open class MinimizeDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - public override fun resolve( + override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, From 6c62bea6921678600d3e8cafe9eb2b0f227ed2f5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 00:50:36 +0800 Subject: [PATCH 03/13] Revert "Remove public modifiers" This reverts commit 234b7822d747ea0b5a9a0b8950b9ed303f33c96f. --- .../gradle/plugins/shadow/internal/DefaultDependencyFilter.kt | 2 +- .../gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index c743b14b8..580213da4 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal class DefaultDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - override fun resolve( + public override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt index e22a33bff..8f9414079 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal open class MinimizeDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - override fun resolve( + public override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, From 5159ea438401b8747b59a230fda20c703cd40d4d Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 00:51:44 +0800 Subject: [PATCH 04/13] Remove `inheritManifestFromMultipleSourcesWithMergeAction` --- .../plugins/shadow/ShadowPropertiesTest.kt | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 236b15268..7ce1be2f8 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -97,28 +97,6 @@ class ShadowPropertiesTest { ) } - @Test - fun inheritManifestFromMultipleSourcesWithMergeAction() = - with(project) { - plugins.apply(JavaPlugin::class.java) - val jar1 = tasks.register("manifestSource1", Jar::class.java) - val jar2 = tasks.register("manifestSource2", Jar::class.java) - jar1.configure { it.manifest.attributes["shared"] = "one" } - jar2.configure { it.manifest.attributes["shared"] = "two" } - - @Suppress("DEPRECATION") - tasks.shadowJar.configure { - it.manifest.inheritFrom(jar1.get().manifest, jar2.get().manifest) { merge -> - merge.eachEntry { details -> - if (details.key == "shared") details.exclude() - } - } - } - - assertThat(tasks.shadowJar.get().manifest.effectiveManifest.attributes) - .containsOnly("Manifest-Version" to "1.0") - } - @Test fun inheritManifestMainClassFromJar() = with(project) { From 089a8dbce25a204ec8a791f6a04ec01c42fb504c Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Wed, 15 Jul 2026 01:11:15 +0800 Subject: [PATCH 05/13] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../shadow/internal/DuplicatesStrategyCheckerTest.kt | 4 ++-- .../gradle/plugins/shadow/internal/UnusedTrackerTest.kt | 9 ++++++--- .../plugins/shadow/internal/ZipEntryValidationTest.kt | 8 ++++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt index 5ac45de99..2a46cf36f 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt @@ -54,7 +54,7 @@ class DuplicatesStrategyCheckerTest { try { ResourceTransformer.checkDupStrategy(false, noOpDelegate()) ResourceTransformer.checkDupStrategy(true, noOpDelegate()) - DuplicatesStrategy.entries.forEach { strategy -> + DuplicatesStrategy.values().forEach { strategy -> val details = object : FileCopyDetails by noOpDelegate() { override fun getPath(): String = "duplicate.txt" @@ -63,7 +63,7 @@ class DuplicatesStrategyCheckerTest { } ResourceTransformer.checkDupStrategy(true, details) } - assertThat(invocationCount).isEqualTo(2 + DuplicatesStrategy.entries.size) + assertThat(invocationCount).isEqualTo(2 + DuplicatesStrategy.values().size) } finally { onCheckDupStrategyInvoked = null } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt index 5b451c77b..bd2b64cf8 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt @@ -101,8 +101,11 @@ class UnusedTrackerTest { if (classpath != null) addAll(listOf("-classpath", classpath.absolutePath)) addAll(sourceFiles.map(File::getAbsolutePath)) } - val result = - ToolProvider.getSystemJavaCompiler().run(null, null, null, *arguments.toTypedArray()) + val compiler = ToolProvider.getSystemJavaCompiler() + org.junit.jupiter.api.Assumptions.assumeTrue( + compiler != null, + "JDK (javax.tools.JavaCompiler) is required to compile test sources" + ) + val result = compiler!!.run(null, null, null, *arguments.toTypedArray()) assertThat(result).isEqualTo(0) - } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt index 7bb46a3a2..0018595f4 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationTest.kt @@ -26,12 +26,12 @@ class ZipEntryValidationTest { fun propertiesInputStreamUsesRequestedCharsetAndComments() { val properties = Properties().apply { setProperty("greeting", "你好") } - val content = properties.inputStream(StandardCharsets.UTF_8, "header").reader().readText() + val charset = StandardCharsets.UTF_16 + val bytes = properties.inputStream(charset, "header").readBytes() + val content = String(bytes, charset) assertThat(content.startsWith("#header")).isTrue() - assertThat(content) - .isEqualTo(content.toByteArray(StandardCharsets.UTF_8).toString(StandardCharsets.UTF_8)) - val loaded = Properties().apply { load(content.reader()) } + val loaded = Properties().apply { load(bytes.inputStream().reader(charset)) } assertThat(loaded.getProperty("greeting")).isEqualTo("你好") } From a0bcefe6849af2f6f70507c61c8562990573c8e7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 01:12:14 +0800 Subject: [PATCH 06/13] Fix style --- .../gradle/plugins/shadow/internal/UnusedTrackerTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt index bd2b64cf8..9a38785de 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt @@ -104,8 +104,9 @@ class UnusedTrackerTest { val compiler = ToolProvider.getSystemJavaCompiler() org.junit.jupiter.api.Assumptions.assumeTrue( compiler != null, - "JDK (javax.tools.JavaCompiler) is required to compile test sources" + "JDK (javax.tools.JavaCompiler) is required to compile test sources", ) val result = compiler!!.run(null, null, null, *arguments.toTypedArray()) assertThat(result).isEqualTo(0) + } } From d4e43e05a0c6be7575380d156ad164d627ce90e7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 01:14:41 +0800 Subject: [PATCH 07/13] Reverts --- .../shadow/internal/DuplicatesStrategyCheckerTest.kt | 4 ++-- .../gradle/plugins/shadow/internal/UnusedTrackerTest.kt | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt index 2a46cf36f..5ac45de99 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt @@ -54,7 +54,7 @@ class DuplicatesStrategyCheckerTest { try { ResourceTransformer.checkDupStrategy(false, noOpDelegate()) ResourceTransformer.checkDupStrategy(true, noOpDelegate()) - DuplicatesStrategy.values().forEach { strategy -> + DuplicatesStrategy.entries.forEach { strategy -> val details = object : FileCopyDetails by noOpDelegate() { override fun getPath(): String = "duplicate.txt" @@ -63,7 +63,7 @@ class DuplicatesStrategyCheckerTest { } ResourceTransformer.checkDupStrategy(true, details) } - assertThat(invocationCount).isEqualTo(2 + DuplicatesStrategy.values().size) + assertThat(invocationCount).isEqualTo(2 + DuplicatesStrategy.entries.size) } finally { onCheckDupStrategyInvoked = null } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt index 9a38785de..5b451c77b 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt @@ -101,12 +101,8 @@ class UnusedTrackerTest { if (classpath != null) addAll(listOf("-classpath", classpath.absolutePath)) addAll(sourceFiles.map(File::getAbsolutePath)) } - val compiler = ToolProvider.getSystemJavaCompiler() - org.junit.jupiter.api.Assumptions.assumeTrue( - compiler != null, - "JDK (javax.tools.JavaCompiler) is required to compile test sources", - ) - val result = compiler!!.run(null, null, null, *arguments.toTypedArray()) + val result = + ToolProvider.getSystemJavaCompiler().run(null, null, null, *arguments.toTypedArray()) assertThat(result).isEqualTo(0) } } From f15aea8c0677b75b3a4a5afcff1f194120c7e2ae Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Wed, 15 Jul 2026 01:20:44 +0800 Subject: [PATCH 08/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../gradle/plugins/shadow/internal/UnusedTrackerTest.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt index 5b451c77b..54b47d7e4 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt @@ -101,8 +101,11 @@ class UnusedTrackerTest { if (classpath != null) addAll(listOf("-classpath", classpath.absolutePath)) addAll(sourceFiles.map(File::getAbsolutePath)) } - val result = - ToolProvider.getSystemJavaCompiler().run(null, null, null, *arguments.toTypedArray()) + val compiler = + requireNotNull(ToolProvider.getSystemJavaCompiler()) { + "No system Java compiler available. Run tests on a JDK (not a JRE)." + } + val result = compiler.run(null, null, null, *arguments.toTypedArray()) assertThat(result).isEqualTo(0) } } From 76a96323825d0248773f284d00093b2395c330aa Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 10:18:58 +0800 Subject: [PATCH 09/13] Delete DependencyFiltersTest --- .../shadow/internal/DependencyFiltersTest.kt | 120 ------------------ 1 file changed, 120 deletions(-) delete mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt deleted file mode 100644 index fd6bdb9d6..000000000 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFiltersTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.github.jengelman.gradle.plugins.shadow.internal - -import assertk.assertThat -import assertk.assertions.containsOnly -import assertk.assertions.isEmpty -import java.lang.reflect.Proxy -import org.gradle.api.artifacts.ResolvedDependency -import org.gradle.api.specs.Spec -import org.gradle.testfixtures.ProjectBuilder -import org.junit.jupiter.api.Test - -class DependencyFiltersTest { - private val project = ProjectBuilder.builder().build() - - @Test - fun defaultFilterTraversesIncludedAndExcludedBranches() { - val includedChild = dependency("included-child") - val excludedChild = dependency("excluded-child") - val includedRoot = dependency("included-root", children = setOf(includedChild)) - val excludedRoot = dependency("excluded-root", children = setOf(excludedChild)) - val filter = DefaultDependencyFilter(project) - filter.exclude(named("excluded-root")) - val included = mutableSetOf() - val excluded = mutableSetOf() - - filter.resolve(setOf(includedRoot, excludedRoot), included, excluded) - - assertThat(included).containsOnly(includedRoot, includedChild, excludedChild) - assertThat(excluded).containsOnly(excludedRoot) - } - - @Test - fun includeRulesExcludeNonMatchingDependencies() { - val includedRoot = dependency("included") - val otherRoot = dependency("other") - val filter = DefaultDependencyFilter(project) - filter.include(named("included")) - val included = mutableSetOf() - val excluded = mutableSetOf() - - filter.resolve(setOf(includedRoot, otherRoot), included, excluded) - - assertThat(included).containsOnly(includedRoot) - assertThat(excluded).containsOnly(otherRoot) - } - - @Test - fun excludeWinsWhenDependencyMatchesIncludeAndExclude() { - val root = dependency("root") - val filter = DefaultDependencyFilter(project) - filter.include(named("root")) - filter.exclude(named("root")) - val included = mutableSetOf() - val excluded = mutableSetOf() - - filter.resolve(setOf(root), included, excluded) - - assertThat(included).isEmpty() - assertThat(excluded).containsOnly(root) - } - - @Test - fun minimizeFilterExcludesChildrenOfExcludedParent() { - lateinit var excludedRoot: ResolvedDependency - val child = dependency("child", parents = { setOf(excludedRoot) }) - excludedRoot = dependency("excluded-root", children = setOf(child)) - val filter = MinimizeDependencyFilter(project) - filter.exclude(named("excluded-root")) - val included = mutableSetOf() - val excluded = mutableSetOf() - - filter.resolve(setOf(excludedRoot), included, excluded) - - assertThat(included).isEmpty() - assertThat(excluded).containsOnly(excludedRoot, child) - } - - @Test - fun diamondGraphIsVisitedOnceAndIncluded() { - val leaf = dependency("leaf") - val left = dependency("left", children = setOf(leaf)) - val right = dependency("right", children = setOf(leaf)) - val filter = DefaultDependencyFilter(project) - val included = mutableSetOf() - val excluded = mutableSetOf() - - filter.resolve(setOf(left, right), included, excluded) - - assertThat(included).containsOnly(left, right, leaf) - assertThat(excluded).isEmpty() - } - - private fun named(name: String) = Spec { it.moduleName == name } - - private fun dependency( - name: String, - children: Set = emptySet(), - parents: () -> Set = { emptySet() }, - ): ResolvedDependency { - lateinit var proxy: ResolvedDependency - proxy = - Proxy.newProxyInstance( - javaClass.classLoader, - arrayOf(ResolvedDependency::class.java), - ) { _, method, args -> - when (method.name) { - "getModuleGroup" -> "test" - "getModuleName" -> name - "getModuleVersion" -> "1.0" - "getChildren" -> children - "getParents" -> parents() - "hashCode" -> System.identityHashCode(proxy) - "equals" -> proxy === args?.firstOrNull() - "toString" -> name - else -> null - } - } as ResolvedDependency - return proxy - } -} From 3a5c49f499de3f252e7b8381b2d41d4e0eca8397 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 10:28:23 +0800 Subject: [PATCH 10/13] Remove checkerHandlesEveryEarlyReturnAndDuplicatesStrategy --- .../internal/DuplicatesStrategyCheckerTest.kt | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt index 5ac45de99..257abe320 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt @@ -5,7 +5,6 @@ import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.transformers.BaseTransformerTest.Companion.canTransformResource import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer.Companion.create -import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File import java.net.JarURLConnection @@ -17,9 +16,6 @@ import kotlin.io.path.isRegularFile import kotlin.io.path.toPath import kotlin.io.path.walk import kotlin.reflect.full.isSubclassOf -import org.gradle.api.file.DuplicatesStrategy -import org.gradle.api.file.FileCopyDetails -import org.gradle.api.file.FileTreeElement import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -46,28 +42,6 @@ class DuplicatesStrategyCheckerTest { onCheckDupStrategyInvoked = null } } - - @Test - fun checkerHandlesEveryEarlyReturnAndDuplicatesStrategy() { - var invocationCount = 0 - onCheckDupStrategyInvoked = { invocationCount++ } - try { - ResourceTransformer.checkDupStrategy(false, noOpDelegate()) - ResourceTransformer.checkDupStrategy(true, noOpDelegate()) - DuplicatesStrategy.entries.forEach { strategy -> - val details = - object : FileCopyDetails by noOpDelegate() { - override fun getPath(): String = "duplicate.txt" - - override fun getDuplicatesStrategy(): DuplicatesStrategy = strategy - } - ResourceTransformer.checkDupStrategy(true, details) - } - assertThat(invocationCount).isEqualTo(2 + DuplicatesStrategy.entries.size) - } finally { - onCheckDupStrategyInvoked = null - } - } } private fun getTransformerClasses(): List> { From 81405b6f43b75ccc325493d053c5fee2b841d49b Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 10:44:01 +0800 Subject: [PATCH 11/13] Remove UnusedTrackerTest --- .../shadow/internal/UnusedTrackerTest.kt | 111 ------------------ 1 file changed, 111 deletions(-) delete mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt deleted file mode 100644 index 54b47d7e4..000000000 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTrackerTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.github.jengelman.gradle.plugins.shadow.internal - -import assertk.assertThat -import assertk.assertions.containsOnly -import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import assertk.assertions.isSameInstanceAs -import assertk.assertions.isTrue -import java.io.File -import java.nio.file.Path -import javax.tools.ToolProvider -import org.gradle.api.attributes.Category -import org.gradle.api.attributes.LibraryElements -import org.gradle.api.attributes.Usage -import org.gradle.api.plugins.JavaLibraryPlugin -import org.gradle.api.plugins.JavaPlugin -import org.gradle.testfixtures.ProjectBuilder -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir - -class UnusedTrackerTest { - @TempDir lateinit var tempDir: Path - - @Test - fun getApiJarsIsEmptyWithoutApiConfiguration() { - val project = ProjectBuilder.builder().build() - - assertThat(project.getApiJars().get()).isEmpty() - } - - @Test - fun getApiJarsCreatesAndReusesResolvableApiView() { - val project = ProjectBuilder.builder().build() - project.plugins.apply(JavaLibraryPlugin::class.java) - - project.getApiJars().get() - val configuration = project.configurations.getByName("shadowMinimizeApi") - val second = project.getApiJars() - - assertThat(configuration.isCanBeResolved).isTrue() - assertThat(configuration.isCanBeConsumed).isFalse() - assertThat(configuration.extendsFrom) - .containsOnly(project.configurations.getByName(JavaPlugin.API_CONFIGURATION_NAME)) - assertThat(configuration.attributes.getAttribute(Usage.USAGE_ATTRIBUTE)?.name) - .isEqualTo(Usage.JAVA_API) - assertThat(configuration.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)?.name) - .isEqualTo(Category.LIBRARY) - assertThat( - configuration.attributes.getAttribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE)?.name - ) - .isEqualTo(LibraryElements.JAR) - assertThat(project.configurations.getByName("shadowMinimizeApi")) - .isSameInstanceAs(configuration) - // Creating the second provider must not register another configuration. - assertThat(second.isPresent).isTrue() - } - - @Test - fun tracksUnusedClassesAndOnlyAddsSelectedDependencies() { - val dependencyClasses = tempDir.resolve("dependency-classes").toFile() - compile( - dependencyClasses, - "dep.Used" to "package dep; public class Used {}", - "dep.Unused" to "package dep; public class Unused {}", - ) - val projectClasses = tempDir.resolve("project-classes").toFile() - compile( - projectClasses, - "app.Main" to "package app; public class Main { dep.Used used; }", - classpath = dependencyClasses, - ) - val project = ProjectBuilder.builder().build() - val tracker = - UnusedTracker( - sourceSetsClassesDirs = listOf(projectClasses), - classJars = project.files(), - toMinimize = project.files(dependencyClasses), - ) - - tracker.addDependency(tempDir.resolve("not-selected").toFile()) - tracker.addDependency(dependencyClasses) - - assertThat(tracker.findUnused()).containsOnly("dep.Unused") - } - - private fun compile( - output: File, - vararg sources: Pair, - classpath: File? = null, - ) { - output.mkdirs() - val sourceFiles = sources.map { (className, source) -> - tempDir.resolve("sources/${className.replace('.', '/')}.java").toFile().apply { - parentFile.mkdirs() - writeText(source) - } - } - val arguments = buildList { - addAll(listOf("-d", output.absolutePath)) - if (classpath != null) addAll(listOf("-classpath", classpath.absolutePath)) - addAll(sourceFiles.map(File::getAbsolutePath)) - } - val compiler = - requireNotNull(ToolProvider.getSystemJavaCompiler()) { - "No system Java compiler available. Run tests on a JDK (not a JRE)." - } - val result = compiler.run(null, null, null, *arguments.toTypedArray()) - assertThat(result).isEqualTo(0) - } -} From dd1fd642a89e119e316301bb6c67fe6b38faad77 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 10:57:16 +0800 Subject: [PATCH 12/13] Remove R8MinimizerTest --- .../plugins/shadow/internal/R8Minimizer.kt | 12 +- .../shadow/internal/R8MinimizerTest.kt | 213 ------------------ 2 files changed, 6 insertions(+), 219 deletions(-) delete mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 65db4cc13..ac661970b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -127,7 +127,7 @@ internal class R8Minimizer( } } - internal fun createRules( + private fun createRules( inputJar: File, r8Args: List, extractedRulesFile: File, @@ -151,14 +151,14 @@ internal class R8Minimizer( return rules.toList() } - internal fun shouldDisableOptimization(r8Args: List): Boolean { + private fun shouldDisableOptimization(r8Args: List): Boolean { return !r8Spec.optimizationEnabled.get() && (r8Spec.obfuscationEnabled.get() || DefaultR8Spec.NO_MINIFICATION_ARG in r8Args) } // Project classes are the public surface of the shadowed jar, even when nothing in the input jar // refers to every class directly. - internal fun sourceKeepRules(inputJar: File): List { + private fun sourceKeepRules(inputJar: File): List { val jarClasses = jarClassEntries(inputJar) return sourceSetsClassesDirs .asSequence() @@ -182,7 +182,7 @@ internal class R8Minimizer( // Keep dependencies users explicitly excluded from minimization, matching the existing // minimize { exclude(...) } contract for the default analyzer. - internal fun keptDependencyRules(inputJar: File): List { + private fun keptDependencyRules(inputJar: File): List { val jarClasses = jarClassEntries(inputJar) return keptDependencyFiles .asSequence() @@ -198,7 +198,7 @@ internal class R8Minimizer( // Service descriptors are usage edges for downstream ServiceLoader calls, so keep the service // interface and every listed provider even if R8 sees no direct references. - internal fun serviceKeepRules(inputJar: File): List { + private fun serviceKeepRules(inputJar: File): List { val rules = linkedSetOf() JarFile(inputJar).use { jarFile -> jarFile @@ -275,7 +275,7 @@ internal class R8Minimizer( // R8 writes a fresh jar, so rewrite it through Shadow's archive settings to preserve // reproducible ordering, timestamps, compression, zip64, and metadata charset behavior. - internal fun normalizeJar(inputJar: File, outputJar: File) { + private fun normalizeJar(inputJar: File, outputJar: File) { val entries = JarFile(inputJar).use { jarFile -> jarFile diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt deleted file mode 100644 index fadfd5052..000000000 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8MinimizerTest.kt +++ /dev/null @@ -1,213 +0,0 @@ -package com.github.jengelman.gradle.plugins.shadow.internal - -import assertk.assertThat -import assertk.assertions.containsExactly -import assertk.assertions.containsOnly -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import assertk.assertions.isTrue -import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONSTANT_TIME_FOR_ZIP_ENTRIES -import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate -import java.io.File -import java.nio.file.Path -import java.util.jar.JarEntry -import java.util.jar.JarFile -import java.util.jar.JarOutputStream -import org.apache.tools.zip.ZipOutputStream -import org.gradle.api.GradleException -import org.gradle.api.logging.Logging -import org.gradle.api.tasks.bundling.ZipEntryCompression -import org.gradle.jvm.toolchain.JavaLauncher -import org.gradle.process.ExecOperations -import org.gradle.testfixtures.ProjectBuilder -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.io.TempDir - -class R8MinimizerTest { - @TempDir lateinit var tempDir: Path - private val project = ProjectBuilder.builder().build() - private val r8Spec = project.objects.newInstance(DefaultR8Spec::class.java) - - @Test - fun minimizeRequiresR8Classpath() { - val failure = - assertThrows { - minimizer().minimize(tempDir.resolve("input.jar").toFile(), tempDir.toFile()) - } - - assertThat(failure.message) - .isEqualTo( - "R8 minimization requires a non-empty R8 classpath. Apply the Shadow plugin or configure the shadowR8 configuration." - ) - } - - @Test - fun optimizationRuleReflectsR8Options() { - val minimizer = minimizer() - - assertThat(minimizer.shouldDisableOptimization(r8Spec.args.get())).isTrue() - r8Spec.enableOptimization() - assertThat(minimizer.shouldDisableOptimization(r8Spec.args.get())).isFalse() - } - - @Test - fun sourceRulesRelocateFilterDeduplicateAndSortClasses() { - val source1 = tempDir.resolve("source1").toFile() - val source2 = tempDir.resolve("source2").toFile() - source1.resolve("old/B.class").writeBytesCreatingParents() - source1.resolve("old/A.class").writeBytesCreatingParents() - source1.resolve("module-info.class").writeBytesCreatingParents() - source2.resolve("old/A.class").writeBytesCreatingParents() - source2.resolve("old/Missing.class").writeBytesCreatingParents() - val input = tempDir.resolve("input.jar").toFile() - input.writeJar("new/A.class" to byteArrayOf(), "new/B.class" to byteArrayOf()) - val minimizer = - minimizer( - sourceDirs = listOf(source1, source2, tempDir.resolve("missing").toFile()), - relocators = listOf(SimpleRelocator("old", "new")), - ) - - assertThat(minimizer.sourceKeepRules(input)) - .containsExactly( - "-keep,includedescriptorclasses class new.A { *; }", - "-keep,includedescriptorclasses class new.B { *; }", - ) - } - - @Test - fun dependencyRulesReadDirectoriesAndJarsAndOnlyKeepClassesInInput() { - val directory = tempDir.resolve("dependency").toFile() - directory.resolve("dep/B.class").writeBytesCreatingParents() - directory.resolve("dep/Missing.class").writeBytesCreatingParents() - val dependencyJar = tempDir.resolve("dependency.jar").toFile() - dependencyJar.writeJar( - "dep/A.class" to byteArrayOf(), - "module-info.class" to byteArrayOf(), - "resource.txt" to byteArrayOf(), - ) - val input = tempDir.resolve("input.jar").toFile() - input.writeJar("dep/A.class" to byteArrayOf(), "dep/B.class" to byteArrayOf()) - - assertThat(minimizer(keptFiles = listOf(directory, dependencyJar)).keptDependencyRules(input)) - .containsExactly("-keep class dep.A { *; }", "-keep class dep.B { *; }") - } - - @Test - fun serviceRulesIgnoreCommentsMalformedNamesAndDuplicates() { - val input = tempDir.resolve("services.jar").toFile() - input.writeJar( - "META-INF/services/example.Service" to - "# heading\nexample.Provider # comment\ninvalid-name\nexample.Provider\n\n".toByteArray(), - "META-INF/services/invalid-name" to "example.Other\n".toByteArray(), - ) - - assertThat(minimizer().serviceKeepRules(input)) - .containsExactly( - "-keep class example.Service { *; }", - "-keep class example.Provider { *; }", - "-keep class example.Other { *; }", - ) - } - - @Test - fun createRulesCombinesSourcesInStableOrderAndDeduplicates() { - val input = tempDir.resolve("input.jar").toFile() - input.writeJar() - val extracted = - tempDir.resolve("extracted.pro").toFile().apply { - writeText("-keep class extracted.One\n-keep class duplicate.Rule\n") - } - val first = - tempDir.resolve("a.pro").toFile().apply { - writeText("-keep class duplicate.Rule\n-keep class file.One\n") - } - val second = tempDir.resolve("b.pro").toFile().apply { writeText("-keep class file.Two\n") } - r8Spec.keepRuleFiles.from(second, tempDir.toFile(), first) - r8Spec.keepRules.addAll("-keep class file.One", "-keep class inline.One") - - assertThat(minimizer().createRules(input, r8Spec.args.get(), extracted)) - .containsExactly( - DefaultR8Spec.DONT_OPTIMIZE_RULE, - "-keep class extracted.One", - "-keep class duplicate.Rule", - "-keep class file.One", - "-keep class file.Two", - "-keep class inline.One", - ) - } - - @Test - fun normalizeJarAddsParentsSortsAndNormalizesTimestamps() { - val input = tempDir.resolve("input.jar").toFile() - input.writeJar("z/B.txt" to "b".toByteArray(), "a/A.txt" to "a".toByteArray()) - val output = tempDir.resolve("output.jar").toFile() - - minimizer(reproducibleOrder = true, preserveTimestamps = false).normalizeJar(input, output) - - JarFile(output).use { jar -> - assertThat(jar.entries().toList().map { it.name }) - .containsExactly("a/", "a/A.txt", "z/", "z/B.txt") - assertThat(jar.entries().toList().map { it.time }.distinct()) - .containsOnly(CONSTANT_TIME_FOR_ZIP_ENTRIES) - assertThat(jar.getInputStream(jar.getJarEntry("a/A.txt")).readBytes().decodeToString()) - .isEqualTo("a") - } - } - - @Test - fun normalizeJarSupportsStoredEntriesAndPreservedTimes() { - val input = tempDir.resolve("input.jar").toFile() - input.writeJar("file.txt" to "content".toByteArray()) - val output = tempDir.resolve("output.jar").toFile() - - minimizer(compression = ZipEntryCompression.STORED, preserveTimestamps = true) - .normalizeJar(input, output) - - JarFile(output).use { jar -> - val entry = jar.getJarEntry("file.txt") - assertThat(entry.method).isEqualTo(ZipOutputStream.STORED) - assertThat(entry.time >= 0).isTrue() - } - } - - private fun minimizer( - sourceDirs: Iterable = emptyList(), - keptFiles: Iterable = emptyList(), - relocators: Iterable = emptyList(), - preserveTimestamps: Boolean = false, - reproducibleOrder: Boolean = true, - compression: ZipEntryCompression = ZipEntryCompression.DEFLATED, - ) = - R8Minimizer( - execOperations = noOpDelegate(), - logger = Logging.getLogger(R8MinimizerTest::class.java), - r8Classpath = project.files(), - r8Spec = r8Spec, - javaLauncher = project.objects.property(JavaLauncher::class.java), - sourceSetsClassesDirs = sourceDirs, - keptDependencyFiles = keptFiles, - relocators = relocators, - preserveFileTimestamps = preserveTimestamps, - reproducibleFileOrder = reproducibleOrder, - zip64 = false, - entryCompression = compression, - metadataCharset = null, - ) - - private fun File.writeBytesCreatingParents() { - parentFile.mkdirs() - writeBytes(byteArrayOf()) - } - - private fun File.writeJar(vararg entries: Pair) { - JarOutputStream(outputStream()).use { output -> - entries.forEach { (name, bytes) -> - output.putNextEntry(JarEntry(name)) - output.write(bytes) - output.closeEntry() - } - } - } -} From b83caeab55f411b5879f037a8683d1a595577e30 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 15 Jul 2026 10:58:35 +0800 Subject: [PATCH 13/13] Revert public --- .../gradle/plugins/shadow/internal/DefaultDependencyFilter.kt | 2 +- .../gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 580213da4..c743b14b8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal class DefaultDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - public override fun resolve( + override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt index 8f9414079..e22a33bff 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.ResolvedDependency internal open class MinimizeDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { - public override fun resolve( + override fun resolve( dependencies: Set, includedDependencies: MutableSet, excludedDependencies: MutableSet,