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. WhyJacocoTestReport
Always 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. task
In 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. jacocoTestReport
theonlyIf
Why 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. jacocoTestReport
theexecutionData
Why 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 👏).