Add Jacoco support
Add support for instrumenting jars with jacoco. Unlike in Make,
Jacoco in Soong is done entirely using jars.
Instrumentation is enabled by EMMA_INSTRUMENT=true, and affects
all apps. If EMMA_INSTRUMENT_FRAMEWORK=true then it also affects
any java libraries listed in InstrumentFrameworkModules.
Bug: 69629238
Test: m EMMA_INSTRUMENT=true EMMA_INSTRUMENT_FRAMEWORK=true SKIP_BOOT_JARS_CHECK=true WITH_DEXPREOPT=false
Change-Id: If699715b277529cd7322ffca67c23b0746e1cccd
diff --git a/java/androidmk.go b/java/androidmk.go
index 1a4b27d..df83faa 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -42,6 +42,10 @@
}
fmt.Fprintln(w, "LOCAL_SDK_VERSION :=", String(library.deviceProperties.Sdk_version))
fmt.Fprintln(w, "LOCAL_SOONG_HEADER_JAR :=", library.headerJarFile.String())
+
+ if library.jacocoReportClassesFile != nil {
+ fmt.Fprintln(w, "LOCAL_SOONG_JACOCO_REPORT_CLASSES_JAR :=", library.jacocoReportClassesFile.String())
+ }
},
},
Custom: func(w io.Writer, name, prefix, moduleDir string, data android.AndroidMkData) {
diff --git a/java/app.go b/java/app.go
index 05cc975..68e4d5c 100644
--- a/java/app.go
+++ b/java/app.go
@@ -54,6 +54,8 @@
// list of directories relative to the Blueprints file containing
// Android resources
Resource_dirs []string
+
+ Instrumentation_for *string
}
type AndroidApp struct {
@@ -119,6 +121,10 @@
// a.properties.Proguard.Enabled = true
//}
+ if String(a.appProperties.Instrumentation_for) == "" {
+ a.properties.Instrument = true
+ }
+
a.Module.compile(ctx)
aaptPackageFlags := append([]string(nil), aaptFlags...)
diff --git a/java/config/config.go b/java/config/config.go
index 3cd2841..49481be 100644
--- a/java/config/config.go
+++ b/java/config/config.go
@@ -28,6 +28,16 @@
DefaultBootclasspathLibraries = []string{"core-oj", "core-libart"}
DefaultSystemModules = "core-system-modules"
DefaultLibraries = []string{"ext", "framework", "okhttp"}
+
+ DefaultJacocoExcludeFilter = []string{"org.junit.*", "org.jacoco.*", "org.mockito.*"}
+
+ InstrumentFrameworkModules = []string{
+ "framework",
+ "telephony-common",
+ "services",
+ "android.car",
+ "android.car7",
+ }
)
func init() {
@@ -74,6 +84,7 @@
pctx.SourcePathVariable("JarArgsCmd", "build/soong/scripts/jar-args.sh")
pctx.HostBinToolVariable("SoongZipCmd", "soong_zip")
pctx.HostBinToolVariable("MergeZipsCmd", "merge_zips")
+ pctx.HostBinToolVariable("Zip2ZipCmd", "zip2zip")
pctx.VariableFunc("DxCmd", func(config interface{}) (string, error) {
if config.(android.Config).IsEnvFalse("USE_D8") {
if config.(android.Config).UnbundledBuild() || config.(android.Config).IsPdkBuild() {
@@ -117,4 +128,6 @@
}
return "", nil
})
+
+ pctx.HostJavaToolVariable("JacocoCLIJar", "jacoco-cli.jar")
}
diff --git a/java/config/makevars.go b/java/config/makevars.go
index dabf2e7..b9009f3 100644
--- a/java/config/makevars.go
+++ b/java/config/makevars.go
@@ -62,4 +62,7 @@
ctx.Strict("SOONG_JAVAC_WRAPPER", "${SoongJavacWrapper}")
ctx.Strict("EXTRACT_SRCJARS", "${ExtractSrcJarsCmd}")
+
+ ctx.Strict("JACOCO_CLI_JAR", "${JacocoCLIJar}")
+ ctx.Strict("DEFAULT_JACOCO_EXCLUDE_FILTER", strings.Join(DefaultJacocoExcludeFilter, ","))
}
diff --git a/java/jacoco.go b/java/jacoco.go
new file mode 100644
index 0000000..b26b046
--- /dev/null
+++ b/java/jacoco.go
@@ -0,0 +1,108 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package java
+
+// Rules for instrumenting classes using jacoco
+
+import (
+ "strings"
+
+ "github.com/google/blueprint"
+
+ "android/soong/android"
+)
+
+var (
+ jacoco = pctx.AndroidStaticRule("jacoco", blueprint.RuleParams{
+ Command: `${config.Zip2ZipCmd} -i $in -o $strippedJar $stripSpec && ` +
+ `${config.JavaCmd} -jar ${config.JacocoCLIJar} instrument -quiet -dest $instrumentedJar $strippedJar && ` +
+ `${config.Ziptime} $instrumentedJar && ` +
+ `${config.MergeZipsCmd} --ignore-duplicates -j $out $instrumentedJar $in`,
+ CommandDeps: []string{
+ "${config.Zip2ZipCmd}",
+ "${config.JavaCmd}",
+ "${config.JacocoCLIJar}",
+ "${config.Ziptime}",
+ "${config.MergeZipsCmd}",
+ },
+ },
+ "strippedJar", "stripSpec", "instrumentedJar")
+)
+
+func jacocoInstrumentJar(ctx android.ModuleContext, outputJar, strippedJar android.WritablePath,
+ inputJar android.Path, stripSpec string) {
+ instrumentedJar := android.PathForModuleOut(ctx, "jacoco/instrumented.jar")
+
+ ctx.Build(pctx, android.BuildParams{
+ Rule: jacoco,
+ Description: "jacoco",
+ Output: outputJar,
+ ImplicitOutput: strippedJar,
+ Input: inputJar,
+ Args: map[string]string{
+ "strippedJar": strippedJar.String(),
+ "stripSpec": stripSpec,
+ "instrumentedJar": instrumentedJar.String(),
+ },
+ })
+}
+
+func (j *Module) jacocoStripSpecs(ctx android.ModuleContext) string {
+ includes := jacocoFiltersToSpecs(ctx,
+ j.properties.Jacoco.Include_filter, "jacoco.include_filter")
+ excludes := jacocoFiltersToSpecs(ctx,
+ j.properties.Jacoco.Exclude_filter, "jacoco.exclude_filter")
+
+ specs := ""
+ if len(excludes) > 0 {
+ specs += android.JoinWithPrefix(excludes, "-x") + " "
+ }
+
+ if len(includes) > 0 {
+ specs += strings.Join(includes, " ")
+ } else {
+ specs += "**/*.class"
+ }
+
+ return specs
+}
+
+func jacocoFiltersToSpecs(ctx android.ModuleContext, filters []string, property string) []string {
+ specs := make([]string, len(filters))
+ for i, f := range filters {
+ specs[i] = jacocoFilterToSpec(ctx, f, property)
+ }
+ return specs
+}
+
+func jacocoFilterToSpec(ctx android.ModuleContext, filter string, property string) string {
+ wildcard := strings.HasSuffix(filter, "*")
+ filter = strings.TrimSuffix(filter, "*")
+ recursiveWildcard := wildcard && (strings.HasSuffix(filter, ".") || filter == "")
+
+ if strings.ContainsRune(filter, '*') {
+ ctx.PropertyErrorf(property, "'*' is only supported as the last character in a filter")
+ }
+
+ spec := strings.Replace(filter, ".", "/", -1)
+
+ if recursiveWildcard {
+ spec += "**/*.class"
+ } else if wildcard {
+ spec += "*.class"
+ }
+
+ return spec
+}
diff --git a/java/java.go b/java/java.go
index b2bd2b0..417cf74 100644
--- a/java/java.go
+++ b/java/java.go
@@ -51,9 +51,6 @@
// Renderscript
// Post-jar passes:
// Proguard
-// Jacoco
-// Jarjar
-// Dex
// Rmtypedefs
// DroidDoc
// Findbugs
@@ -127,6 +124,26 @@
// List of javac flags that should only be used when passing -source 1.9
Javacflags []string
}
+
+ Jacoco struct {
+ // List of classes to include for instrumentation with jacoco to collect coverage
+ // information at runtime when building with coverage enabled. If unset defaults to all
+ // classes.
+ // Supports '*' as the last character of an entry in the list as a wildcard match.
+ // If preceded by '.' it matches all classes in the package and subpackages, otherwise
+ // it matches classes in the package that have the class name as a prefix.
+ Include_filter []string
+
+ // List of classes to exclude from instrumentation with jacoco to collect coverage
+ // information at runtime when building with coverage enabled. Overrides classes selected
+ // by the include_filter property.
+ // Supports '*' as the last character of an entry in the list as a wildcard match.
+ // If preceded by '.' it matches all classes in the package and subpackages, otherwise
+ // it matches classes in the package that have the class name as a prefix.
+ Exclude_filter []string
+ }
+
+ Instrument bool `blueprint:"mutated"`
}
type CompilerDeviceProperties struct {
@@ -177,6 +194,9 @@
// output file containing classes.dex
dexJarFile android.Path
+ // output file containing uninstrumented classes that will be instrumented by jacoco
+ jacocoReportClassesFile android.Path
+
// output file suitable for installing or running
outputFile android.Path
@@ -718,6 +738,20 @@
}
if ctx.Device() && j.installable() {
+ outputFile = j.desugar(ctx, flags, outputFile, jarName)
+ }
+
+ if ctx.AConfig().IsEnvTrue("EMMA_INSTRUMENT_FRAMEWORK") {
+ if inList(ctx.ModuleName(), config.InstrumentFrameworkModules) {
+ j.properties.Instrument = true
+ }
+ }
+
+ if ctx.AConfig().IsEnvTrue("EMMA_INSTRUMENT") && j.properties.Instrument {
+ outputFile = j.instrument(ctx, flags, outputFile, jarName)
+ }
+
+ if ctx.Device() && j.installable() {
outputFile = j.compileDex(ctx, flags, outputFile, jarName)
if ctx.Failed() {
return
@@ -766,6 +800,42 @@
return headerJar
}
+func (j *Module) desugar(ctx android.ModuleContext, flags javaBuilderFlags,
+ classesJar android.Path, jarName string) android.Path {
+
+ desugarFlags := []string{
+ "--min_sdk_version " + j.minSdkVersionNumber(ctx),
+ "--desugar_try_with_resources_if_needed=false",
+ "--allow_empty_bootclasspath",
+ }
+
+ if inList("--core-library", j.deviceProperties.Dxflags) {
+ desugarFlags = append(desugarFlags, "--core_library")
+ }
+
+ flags.desugarFlags = strings.Join(desugarFlags, " ")
+
+ desugarJar := android.PathForModuleOut(ctx, "desugar", jarName)
+ TransformDesugar(ctx, desugarJar, classesJar, flags)
+
+ return desugarJar
+}
+
+func (j *Module) instrument(ctx android.ModuleContext, flags javaBuilderFlags,
+ classesJar android.Path, jarName string) android.Path {
+
+ specs := j.jacocoStripSpecs(ctx)
+
+ jacocoReportClassesFile := android.PathForModuleOut(ctx, "jacoco", "jacoco-report-classes.jar")
+ instrumentedJar := android.PathForModuleOut(ctx, "jacoco", jarName)
+
+ jacocoInstrumentJar(ctx, instrumentedJar, jacocoReportClassesFile, classesJar, specs)
+
+ j.jacocoReportClassesFile = jacocoReportClassesFile
+
+ return instrumentedJar
+}
+
func (j *Module) compileDex(ctx android.ModuleContext, flags javaBuilderFlags,
classesJar android.Path, jarName string) android.Path {
@@ -792,47 +862,29 @@
"--dump-width=1000")
}
- var minSdkVersion string
- switch String(j.deviceProperties.Sdk_version) {
- case "", "current", "test_current", "system_current":
- minSdkVersion = strconv.Itoa(ctx.AConfig().DefaultAppTargetSdkInt())
- default:
- minSdkVersion = String(j.deviceProperties.Sdk_version)
- }
-
- dxFlags = append(dxFlags, "--min-sdk-version="+minSdkVersion)
+ dxFlags = append(dxFlags, "--min-sdk-version="+j.minSdkVersionNumber(ctx))
flags.dxFlags = strings.Join(dxFlags, " ")
- desugarFlags := []string{
- "--min_sdk_version " + minSdkVersion,
- "--desugar_try_with_resources_if_needed=false",
- "--allow_empty_bootclasspath",
- }
-
- if inList("--core-library", dxFlags) {
- desugarFlags = append(desugarFlags, "--core_library")
- }
-
- flags.desugarFlags = strings.Join(desugarFlags, " ")
-
- desugarJar := android.PathForModuleOut(ctx, "desugar", jarName)
- TransformDesugar(ctx, desugarJar, classesJar, flags)
- if ctx.Failed() {
- return nil
- }
-
// Compile classes.jar into classes.dex and then javalib.jar
javalibJar := android.PathForModuleOut(ctx, "dex", jarName)
- TransformClassesJarToDexJar(ctx, javalibJar, desugarJar, flags)
- if ctx.Failed() {
- return nil
- }
+ TransformClassesJarToDexJar(ctx, javalibJar, classesJar, flags)
j.dexJarFile = javalibJar
return javalibJar
}
+// Returns a sdk version as a string that is guaranteed to be a parseable as a number. For
+// modules targeting an unreleased SDK (meaning it does not yet have a number) it returns "10000".
+func (j *Module) minSdkVersionNumber(ctx android.ModuleContext) string {
+ switch String(j.deviceProperties.Sdk_version) {
+ case "", "current", "test_current", "system_current":
+ return strconv.Itoa(ctx.AConfig().DefaultAppTargetSdkInt())
+ default:
+ return String(j.deviceProperties.Sdk_version)
+ }
+}
+
func (j *Module) installable() bool {
return j.properties.Installable == nil || *j.properties.Installable
}