The cause of

Recently, a new project was written that introduced a unit test coverage statistical framework, Jacoco, to better ensure the quality of project output. Since there are only a few default Settings for Tasks (test, JacocoTestReport, etc.) on gradle’s official website, AND I wanted to provide separate tests for different layers in addition, this is where the interesting things happen.

1. WhyJacocoTestReportAlways be SKIPPED

First, I wrote a custom test. And JacocoTestReport is automatically executed after test execution by specifying finalizedBy as JacocoTestReport. Custom test and JacocoTestReport are as follows:

// Customize test
task serviceTest(type: Test) {
    useTestNG()
    useJUnitPlatform()

	finalizedBy jacocoTestReport

    jacoco {
        enabled = true
        // Specify the location of the original data file
        destinationFile = layout.buildDirectory.file("jacoco/${taskName}.exec").get().asFile
        includes = ['xxxservice']
        excludeClassLoaders = []
        includeNoLocationClasses = false
        sessionId = "<auto-generated value>"
        output = JacocoTaskExtension.Output.FILE
    }
}

jacocoTestReport {
    // tests are required to run before generating the report
    dependsOn serviceTest
		
	// Specify the coverage statistics file.
	executionData(layout.buildDirectory.file("jacoco/${serviceTest.name}.exec").get().asFile)

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, include: ['xxxservice'])
        }))
    }
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoReport')}}Copy the code

Looks like there’s no problem. Everything’s been done. But the jacocoTestReport is skipped. At that time special feel suspicious, went to the net raised a problem. The executionData file does not exist. Add onlyIf = {true} to jacocoTestReport to ensure that jacocoTestReport will execute. And the execution will eventually report an error that the test.exec file does not exist. Ok, we have a direction. Let’s analyze it one by one.

2. taskIn theonlyIf

In fact, onlyIf determines whether the task is executed. OnlyIf can be set to multiple rule judgments. If all rule judgments return true, the task is executed. Otherwise, skip task.

2.1 SkipOnlyIfTaskExecuter

Let’s go to SkipOnlyIfTaskExecuter to find out:

public class SkipOnlyIfTaskExecuter implements TaskExecuter {
	@Override
    public TaskExecuterResult execute(TaskInternal task, TaskStateInternal state, TaskExecutionContext context) {
        booleanskip = ! task.getOnlyIf().isSatisfiedBy(task);/ / skip the task
        if (skip) {
            return TaskExecuterResult.WITHOUT_OUTPUTS;
        }
				
		/ / the task execution
        return executer.execute(task, state, context);
    }

	// Iterate over all rules
	public boolean isSatisfiedBy(T object) {
        Spec<? super T>[] specs = getSpecsArray();
        for (Spec<? super T> spec : specs) {
            if(! spec.isSatisfiedBy(object)) {// If one rule fails, return false
                return false; }}return true; }}Copy the code

2.2 onlyIf usage

So how do we use onlyIf?

2.2.1 Customize rules as the sole judgment condition

I’m just going to use theta.

task foo{
	onlyIf = {true}}/ / or
foo.onlyIf = {true}
Copy the code

Gradle AbstractTask#setOnlyIf = AbstractTask#setOnlyIf = AbstractTask#setOnlyIf = AbstractTask#setOnlyIf

	public void setOnlyIf(final Closure onlyIfClosure) {
        taskMutator.mutate("Task.setOnlyIf(Closure)".new Runnable() {
            @Override
            public void run(a) {
				// Set onlyIfClosure and override onlyIfSpeconlyIfSpec = createNewOnlyIfSpec().and(onlyIfClosure); }}); }// Create a new element that returns true by default
	private AndSpec<Task> createNewOnlyIfSpec(a) {
        return new AndSpec<Task>(new Spec<Task>() {
            @Override
            public boolean isSatisfiedBy(Task element) {
				IsSatisfiedBy (task)
                return element == AbstractTask.this&& enabled; }}); }Copy the code

2.2.2 Customize rules as part of condition judgment

Remove =

task foo{
	onlyIf {true}}/ / or
foo.onlyIf {true}
Copy the code

Source code:

	public void onlyIf(final Closure onlyIfClosure) {
        taskMutator.mutate("Task.onlyIf(Closure)".new Runnable() {
            @Override
            public void run(a) {
				/ / insertonlyIfSpec = onlyIfSpec.and(onlyIfClosure); }}); }Copy the code

This approach makes it easy to define multiple onlyIf blocks, while ensuring that the default onlyIf is not overridden. Let’s talk about onlyIf preset by jacocoTestReport

3. jacocoTestReporttheonlyIfWhy is “default”false

IsSatisfiedBy: jacocoTestReport will be skipped. The isSatisfiedBy of a spec initialized by createNewOnlyIfSpec() must return true. So, there must have been an extra spec added somewhere. Let’s start by looking at how tasks of a JacocoReport type are initialized:

/** * JacocoReport extends JacocoReportBase * JacocoReportBase extends AbstractTask */
public abstract class JacocoReportBase extends JacocoBase {

	public JacocoReportBase() {
		/ / add the spec
        onlyIf(new Spec<Task>() {
            @Override
            public boolean isSatisfiedBy(Task element) {
                return Iterables.any(getExecutionData(), new Predicate<File>() {
                    @Override
                    public boolean apply(File file) {
                    	// Returns whether the file exists
                        returnfile.exists(); }}); }}); }}Copy the code

As you can see, JacocoReport adds a validation rule to initialization, returning true if getExecutionData() has all the corresponding files, false otherwise. So, onlyIf” defaults “to false, obviously some files in the file path returned by getExecutionData() don’t exist. Next, let’s look at why some files don’t exist.

4. jacocoTestReporttheexecutionDataWhy is “test.exec” included by default?

After the above analysis, we know that the task will execute as soon as onlyIf={true} is set to the task. JacocoTestReport runs by adding onlyIf={true}. It was suddenly another error to get:

Unable to read execution data file /xxx/test.exec
Copy the code

Exec = serviceTest. Exec = serviceTest. Exec = serviceTest. Again, obviously, this is preset behavior. So let’s first look at where the jacocoTestReport Task was created:

public class JavaPlugin implements Plugin<Project> {
		public static final String TEST_TASK_NAME = "test";
}

public class JacocoPlugin implements Plugin<Project> {

	private void addDefaultReportAndCoverageVerificationTasks(final JacocoPluginExtension extension) {
        project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {

			// Get the test Task
            TaskProvider<Task> testTaskProvider = project.getTasks().named(JavaPlugin.TEST_TASK_NAME);
			// Initialize the jacocoTestReport Task
            addDefaultReportTask(extension, testTaskProvider);
        });
    }
		
	private void addDefaultReportTask(final JacocoPluginExtension extension, final TaskProvider<Task> testTaskProvider) {
		//testTaskName is constant test
        final String testTaskName = testTaskProvider.getName();
		// Register the jacocoTestReport Task
        project.getTasks().register(
            "jacoco" + StringUtils.capitalize(testTaskName) + "Report",
            JacocoReport.class,
            reportTask -> {
				// Define the default configuration items for this Task.// Set executionData to the destinationFile defined in testreportTask.executionData(testTaskProvider.get()); . }); }}public abstract class JacocoReportBase extends JacocoBase {

	public void executionData(Task... tasks) {
        for (Task task : tasks) {
			// Get the jacoco extension defined in the passed task, i.e. the jacoco{} definition
            final JacocoTaskExtension extension = task.getExtensions().findByType(JacocoTaskExtension.class);
            if(extension ! =null) {
                executionData(new Callable<File>() {
                    @Override
                    public File call(a) {
						// Set executionData to the destinationFile defined in the task
                        returnextension.getDestinationFile(); }}); mustRunAfter(task); }}}}Copy the code

As you can see, the JacocoPlugin registers a Task of type JacocoReport named jacocoTestReport when it is initialized. The executionData for this Task is set by default to the destinationFile defined in the Jacoco extension of the Test Task. And the test above jacoco destinationFile defined within the extension is layout. BuildDirectory. The file (” jacoco / ${taskName}. The exec “). The get (). The asFile. So this is where the truth comes out.

In fact, The default destinationFile of jacoco extension for Task of type Test is layout.getBuildDirectory().file(” Jacoco /” + taskName + “. The exec “). The map (RegularFile: : getAsFile). So, if we don’t define this value, the same error will be reported. The source code is as follows:

public class JacocoPlugin implements Plugin<Project> {
	public void apply(Project project) {
        JacocoPluginExtension extension = project.getExtensions().create(PLUGIN_EXTENSION_NAME, JacocoPluginExtension.class, project, agent);
        // Set the default Jacoco extension for test
        applyToDefaultTasks(extension);
        // Initialize the default jacocoTestReport
        addDefaultReportAndCoverageVerificationTasks(extension);
    }

	private void applyToDefaultTasks(final JacocoPluginExtension extension) {
		// Get all test tasks and configure Jacoco Extension
        project.getTasks().withType(Test.class).configureEach(extension::applyTo); }}public class JacocoPluginExtension {
	public <T extends Task & JavaForkOptions> void applyTo(final T task) {
        final String taskName = task.getName();
        final JacocoTaskExtension extension = task.getExtensions().create(TASK_EXTENSION_NAME, JacocoTaskExtension.class, objects, agent, task);
		// Set the file name to ${taskName}.exec. The test Task corresponds to test.exec
        extension.setDestinationFile(layout.getBuildDirectory().file("jacoco/" + taskName + ".exec").map(RegularFile::getAsFile)); }}Copy the code

5. How do you achieve your initial goal

Now that we know that executionData is preset in jacocoTestReport, all we need to do to achieve our original goal is to clear and reset it during task execution, or to customize a clean JacocoReport.

5.1 Clear and Reset the Settings

jacocoTestReport {
    // tests are required to run before generating the report
    dependsOn serviceTest
		// Clear the default executionData
		((DefaultConfigurableFileCollection) executionData).filesWrapper.clear();
		// Specify the coverage statistics file.
		executionData(layout.buildDirectory.file("jacoco/${serviceTest.name}.exec").get().asFile)

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, include: ['xxxservice'])
        }))
    }
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoReport')}}Copy the code

5.2 Customize a cleanJacocoReport

task jacocoServiceTestReport(type: JacocoReport) {
    // tests are required to run before generating the report
    dependsOn serviceTest

    // Custom source directory will not be initialized, you need to specify manually
    sourceSets sourceSets.main

    executionData(layout.buildDirectory.file("jacoco/${serviceTest.name}.exec").get().asFile)

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, include: project.ext.serviceSources)
        }))
    }
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir(project.ext.reportDir as String)
    }

    onlyIf = { true}}Copy the code

conclusion

To bounce about. To bounce about. To bounce about, I was completely confused because I didn’t know Gradle and Jacoco well. Fortunately, there was an entry point from stackOverFlow and the authorities: onlyIf={true}. Let the problem finally be solved.

On the other hand, I think the official sample in this section is a little rough, and these default behaviors should be mentioned at least, otherwise it will be easy to make people confused. So I didn’t come up with a solution after all (if any of you find this described in the official documentation, please point to 👏).

github