diff --git a/Android.bp b/Android.bp index e89f90837..d32c9572a 100644 --- a/Android.bp +++ b/Android.bp @@ -213,6 +213,7 @@ bootstrap_go_package { "java/app.go", "java/builder.go", "java/gen.go", + "java/jacoco.go", "java/java.go", "java/proto.go", "java/resources.go", diff --git a/java/androidmk.go b/java/androidmk.go index 1a4b27d7a..df83faa23 100644 --- a/java/androidmk.go +++ b/java/androidmk.go @@ -42,6 +42,10 @@ func (library *Library) AndroidMk() android.AndroidMkData { } 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 05cc9756a..68e4d5cc7 100644 --- a/java/app.go +++ b/java/app.go @@ -54,6 +54,8 @@ type androidAppProperties struct { // list of directories relative to the Blueprints file containing // Android resources Resource_dirs []string + + Instrumentation_for *string } type AndroidApp struct { @@ -119,6 +121,10 @@ func (a *AndroidApp) GenerateAndroidBuildActions(ctx android.ModuleContext) { // 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 3cd284157..49481be30 100644 --- a/java/config/config.go +++ b/java/config/config.go @@ -28,6 +28,16 @@ var ( 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 @@ func init() { 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 @@ func init() { } return "", nil }) + + pctx.HostJavaToolVariable("JacocoCLIJar", "jacoco-cli.jar") } diff --git a/java/config/makevars.go b/java/config/makevars.go index dabf2e722..b9009f307 100644 --- a/java/config/makevars.go +++ b/java/config/makevars.go @@ -62,4 +62,7 @@ func makeVarsProvider(ctx android.MakeVarsContext) { 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 000000000..b26b046d2 --- /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 b2bd2b0a4..417cf7418 100644 --- a/java/java.go +++ b/java/java.go @@ -51,9 +51,6 @@ func init() { // Renderscript // Post-jar passes: // Proguard -// Jacoco -// Jarjar -// Dex // Rmtypedefs // DroidDoc // Findbugs @@ -127,6 +124,26 @@ type CompilerProperties struct { // 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 @@ type Module struct { // 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 @@ -717,6 +737,20 @@ func (j *Module) compile(ctx android.ModuleContext) { j.headerJarFile = j.implementationJarFile } + 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() { @@ -766,6 +800,42 @@ func (j *Module) compileJavaHeader(ctx android.ModuleContext, srcFiles, srcJars 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 @@ func (j *Module) compileDex(ctx android.ModuleContext, flags javaBuilderFlags, "--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 }