Skip to content

Commit

Permalink
Merge pull request #102 from koral--/maintenance
Browse files Browse the repository at this point in the history
Copy runtime classpath only if jetifier is disabled
  • Loading branch information
koral-- authored Nov 6, 2023
2 parents 5cf31a7 + 22252ac commit 708b169
Show file tree
Hide file tree
Showing 13 changed files with 127 additions and 62 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ pitest {
```
In such case default mockable Android JAR won't be added and alternative one will be used under tests.

## Using jetifier
This plugin tries to discover all the unit test dependencies and add them to the pitest classpath automatically.
But, this feature doesn't work with [jetifier](https://developer.android.com/studio/command-line/jetifier)
(`android.enableJetifier=true` in `gradle.properties`).

If you are using jetifier and encounter `NoClassDefFoundError`s in tests run under pitest, you may want to add
some dependencies manually to the `pitestTestCompile` configuration. For example:
```groovy
buildscript {
configurations.create("pitestTestCompile")
dependencies {
pitestTestCompile 'io.mockk:mockk-agent-jvm:1.11.0'
}
}
```

# <a name="troubleshooting"></a> Troubleshooting
## Tests fail when run under pitest but pass without it
Issue occurs when using [Android API](https://developer.android.com/reference/packages.html)
Expand Down
14 changes: 4 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ plugins {

sourceCompatibility = 1.8

ext.pitestAggregatorVersion = "1.9.11" //Must be equal to default PIT version in PitestPlugin
ext.pitestAggregatorVersion = "1.15.0" //Must be equal to default PIT version in PitestPlugin

repositories {
mavenCentral()
Expand All @@ -39,21 +39,15 @@ dependencies {
testImplementation('org.spockframework:spock-junit4:2.3-groovy-3.0') {
exclude group: 'org.codehaus.groovy'
}
testImplementation 'net.bytebuddy:byte-buddy:1.14.7' //for Spying in Spock
testImplementation 'net.bytebuddy:byte-buddy:1.14.8' //for Spying in Spock

funcTestImplementation sourceSets.main.output
//to make production plugin classes visible in functional tests (it's not in testImplementation configuration)
funcTestImplementation sourceSets.main.output //to make production plugin classes visible in functional tests (it's not in testCompile configuration)
funcTestImplementation sourceSets.test.output
funcTestImplementation configurations.testImplementation
funcTestRuntimeOnly configurations.testRuntimeOnly
funcTestImplementation('com.netflix.nebula:nebula-test:10.5.0') {
exclude group: 'org.codehaus.groovy', module: 'groovy-all'
}
def toolingApiBuildersJar = (project as ProjectInternal).services.get(ModuleRegistry.class)
.getModule("gradle-tooling-api-builders")
.classpath
.asFiles
.first()
testRuntimeOnly(files(toolingApiBuildersJar))
}

tasks.register('funcTest', Test) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,7 @@ class KotlinPitestPluginFunctionalSpec extends AbstractPitestFunctionalSpec {
result.wasExecuted(':test')
}

void "should run mutation analysis with Android Gradle plugin 3"() {
when:
copyResources("testProjects/simpleKotlin", "")
then:
fileExists('build.gradle')
when:
ExecutionResult result = runTasksSuccessfully('pitestRelease')
then:
result.wasExecuted(':pitestRelease')
result.standardOutput.contains('Generated 3 mutations Killed 3 (100%)')
}

void "should run mutation analysis with Android Gradle plugin 2"() {
void "should run mutation analysis with kotlin Android plugin"() {
when:
copyResources("testProjects/simpleKotlin", "")
then:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class MockableAndroidJarFunctionalSpec extends AbstractPitestFunctionalSpec {
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20180813'
}
""".stripIndent()
and:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.Test;

import pitest.test.Library;
import org.json.JSONObject;

import static org.junit.Assert.*;

Expand All @@ -12,4 +13,10 @@ public void testSomeLibraryMethod() {
Library classUnderTest = new Library();
assertTrue("someLibraryMethod should return 'true'", classUnderTest.someLibraryMethod());
}

@Test
public void testJSON() throws Exception {
JSONObject jsonObject = new JSONObject("{\"id\": 123}");
assertEquals("123", jsonObject.optString("id"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
android.useAndroidX=true
android.enableJetifier=true
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ abstract class AggregateReportTask extends DefaultTask {

@TaskAction
void aggregate() {
logger.info("Aggregating pitest reports")
logger.info("Aggregating pitest reports (mutationFiles: {}, lineCoverageFiles: {})", mutationFiles.elements.getOrNull(), lineCoverageFiles.elements.getOrNull())

WorkQueue workQueue = getWorkerExecutor().classLoaderIsolation { workerSpec ->
workerSpec.getClasspath().from(getPitestReportClasspath())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,19 @@ class PitestAggregatorPlugin implements Plugin<Project> {
return configuration
}

addPitAggregateReportDependency(pitestReportConfiguration)
project.tasks.register(PITEST_REPORT_AGGREGATE_TASK_NAME, AggregateReportTask) { t ->
t.description = "Aggregate PIT reports"
t.group = PitestPlugin.PITEST_TASK_GROUP
configureTaskDefaults(t)
//shouldRunAfter should be enough, but it fails in functional tests as :pitestReportAggregate is executed before :pitest tasks from subprojects
t.mustRunAfter(project.allprojects.collect { Project p -> p.tasks.withType(PitestTask) })
addPitAggregateReportDependency(pitestReportConfiguration)
t.pitestReportClasspath.from(pitestReportConfiguration)
}
}

private void configureTaskDefaults(AggregateReportTask aggregateReportTask) {
aggregateReportTask.with {
aggregateReportTask.with { task ->
reportDir.set(new File(getReportBaseDirectory(), PitestPlugin.PITEST_REPORT_DIRECTORY_NAME))
reportFile.set(reportDir.file("index.html"))

Expand Down
45 changes: 36 additions & 9 deletions src/main/groovy/pl/droidsonroids/gradle/pitest/PitestPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@
*/
package pl.droidsonroids.gradle.pitest

import com.android.build.api.dsl.AndroidSourceSet
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.TestPlugin
import com.android.build.api.dsl.AndroidSourceSet
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.internal.api.TestedVariant
import com.android.builder.model.AndroidProject
import com.vdurmont.semver4j.Semver
import groovy.transform.CompileDynamic
import groovy.transform.PackageScope
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
Expand All @@ -51,9 +52,10 @@ import static org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_
@CompileDynamic
class PitestPlugin implements Plugin<Project> {

public final static String DEFAULT_PITEST_VERSION = '1.9.11'
public final static String DEFAULT_PITEST_VERSION = '1.15.0'
public final static String PITEST_TASK_GROUP = VERIFICATION_GROUP
public final static String PITEST_TASK_NAME = "pitest"
public final static String PITEST_REPORT_DIRECTORY_NAME = 'pitest'
public final static String PITEST_CONFIGURATION_NAME = 'pitest'
public final static String PITEST_TEST_COMPILE_CONFIGURATION_NAME = 'pitestTestCompile'

Expand All @@ -69,7 +71,6 @@ class PitestPlugin implements Plugin<Project> {
//visible for testing
final static String PIT_HISTORY_DEFAULT_FILE_NAME = 'pitHistory.txt'
private final static String PIT_ADDITIONAL_CLASSPATH_DEFAULT_FILE_NAME = "pitClasspath"
public final static String PITEST_REPORT_DIRECTORY_NAME = 'pitest'
public static final String PLUGIN_ID = 'pl.droidsonroids.pitest'

private Project project
Expand All @@ -89,13 +90,15 @@ class PitestPlugin implements Plugin<Project> {

void apply(Project project) {
this.project = project
failWithMeaningfulErrorMessageOnUnsupportedConfigurationInRootProjectBuildScript()
createConfigurations()

pitestExtension = project.extensions.create("pitest", PitestPluginExtension, project)
pitestExtension.pitestVersion.set(DEFAULT_PITEST_VERSION)
pitestExtension.fileExtensionsToFilter.set(DEFAULT_FILE_EXTENSIONS_TO_FILTER_FROM_CLASSPATH)
pitestExtension.useClasspathFile.set(false)
pitestExtension.verbosity.set("NO_SPINNER")
pitestExtension.addJUnitPlatformLauncher.set(true)

project.pluginManager.apply(BasePlugin)

Expand All @@ -121,6 +124,14 @@ class PitestPlugin implements Plugin<Project> {
}
}

private void failWithMeaningfulErrorMessageOnUnsupportedConfigurationInRootProjectBuildScript() {
if (project.rootProject.buildscript.configurations.findByName(PITEST_CONFIGURATION_NAME) != null) {
throw new GradleException("The '${PITEST_CONFIGURATION_NAME}' buildscript configuration found in the root project. " +
"This is no longer supported in 1.5.0+ and has to be changed to the regular (sub)project configuration. " +
"See the project FAQ for migration details.")
}
}

@SuppressWarnings("BuilderMethodWithSideEffects")
private void createPitestTasks(DefaultDomainObjectSet<? extends BaseVariant> variants) {
Task globalTask = project.tasks.create(PITEST_TASK_NAME)
Expand All @@ -133,6 +144,11 @@ class PitestPlugin implements Plugin<Project> {
variants.all { BaseVariant variant ->
PitestTask variantTask = project.tasks.create("${PITEST_TASK_NAME}${variant.name.capitalize()}", PitestTask)

boolean includeMockableAndroidJar = !pitestExtension.excludeMockableAndroidJar.getOrElse(false)
if (includeMockableAndroidJar) {
addMockableAndroidJarDependencies()
}

Task mockableAndroidJarTask
if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER < new Semver("3.2.0")) {
mockableAndroidJarTask = project.tasks.findByName("mockableAndroidJar")
Expand All @@ -142,7 +158,7 @@ class PitestPlugin implements Plugin<Project> {
configureTaskDefault(variantTask, variant, mockableAndroidJarTask.outputJar)
}

if (!pitestExtension.excludeMockableAndroidJar.getOrElse(false)) {
if (includeMockableAndroidJar) {
variantTask.dependsOn mockableAndroidJarTask
}

Expand All @@ -158,6 +174,18 @@ class PitestPlugin implements Plugin<Project> {
}
}

private void addMockableAndroidJarDependencies() {
//according to https://search.maven.org/artifact/com.google.android/android/4.1.1.4/jar
project.buildscript.dependencies {
"$PITEST_TEST_COMPILE_CONFIGURATION_NAME" "org.json:json:20080701"
"$PITEST_TEST_COMPILE_CONFIGURATION_NAME" "xpp3:xpp3:1.1.4c"
"$PITEST_TEST_COMPILE_CONFIGURATION_NAME" "xerces:xmlParserAPIs:2.6.2"
"$PITEST_TEST_COMPILE_CONFIGURATION_NAME" "org.khronos:opengl-api:gl1.1-android-2.1_r1"
"$PITEST_TEST_COMPILE_CONFIGURATION_NAME" "org.apache.httpcomponents:httpclient:4.0.1"
"$PITEST_TEST_COMPILE_CONFIGURATION_NAME" "commons-logging:commons-logging:1.1.1"
}
}

@SuppressWarnings("BuilderMethodWithSideEffects")
private void createConfigurations() {
[PITEST_CONFIGURATION_NAME, PITEST_TEST_COMPILE_CONFIGURATION_NAME].each { configuration ->
Expand All @@ -181,7 +209,7 @@ class PitestPlugin implements Plugin<Project> {
from(mockableAndroidJar)
}

if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER.major == 3) {
if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER.major == 3 && project.findProperty("android.enableJetifier") != "true") {
if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER.minor < 3) {
from(project.configurations["${variant.name}CompileClasspath"].copyRecursive { configuration ->
configuration.properties.dependencyProject == null
Expand All @@ -196,9 +224,9 @@ class PitestPlugin implements Plugin<Project> {
} else if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER.major == 4) {
from(project.configurations["compile"])
from(project.configurations["testCompile"])
} else if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER.major > 4) {
} else if (ANDROID_GRADLE_PLUGIN_VERSION_NUMBER.major > 4 && project.findProperty("android.enableJetifier") != "true") {
from(project.configurations["${variant.name}UnitTestRuntimeClasspath"].copyRecursive { configuration ->
configuration.properties.dependencyProject == null
configuration.properties.dependencyProject == null && configuration.version != null
})
}
from(project.configurations["pitestRuntimeOnly"])
Expand Down Expand Up @@ -240,7 +268,6 @@ class PitestPlugin implements Plugin<Project> {
return targetClasses.getOrNull()
}
} as Provider<Iterable<String>>)
dependencyDistance.set(pitestExtension.dependencyDistance)
threads.set(pitestExtension.threads)
mutators.set(pitestExtension.mutators)
excludedMethods.set(pitestExtension.excludedMethods)
Expand Down Expand Up @@ -351,7 +378,7 @@ class PitestPlugin implements Plugin<Project> {
final GradleVersion minimalPitVersionNotNeedingTestPluginProperty = GradleVersion.version("1.6.7")
if (GradleVersion.version(configuredPitVersion) >= minimalPitVersionNotNeedingTestPluginProperty) {
log.info("Passing '--testPlugin' to PIT disabled for PIT 1.6.7+. See https://github.com/szpak/gradle-pitest-plugin/issues/277")
pitestTask.testPlugin.set((String)null)
pitestTask.testPlugin.set((String) null)
}
} catch (IllegalArgumentException e) {
log.warn("Error during PIT versions comparison. Is '$configuredPitVersion' really valid? If yes, please report that case. " +
Expand Down
Loading

0 comments on commit 708b169

Please sign in to comment.