1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package java
16
17// Rules for instrumenting classes using jacoco
18
19import (
20	"fmt"
21	"path/filepath"
22	"strings"
23
24	"github.com/google/blueprint"
25	"github.com/google/blueprint/proptools"
26
27	"android/soong/android"
28	"android/soong/java/config"
29)
30
31var (
32	jacoco = pctx.AndroidStaticRule("jacoco", blueprint.RuleParams{
33		Command: `rm -rf $tmpDir && mkdir -p $tmpDir && ` +
34			`${config.Zip2ZipCmd} -i $in -o $strippedJar $stripSpec && ` +
35			`${config.JavaCmd} ${config.JavaVmFlags} -jar ${config.JacocoCLIJar} ` +
36			`  instrument --quiet --dest $tmpDir $strippedJar && ` +
37			`${config.MergeZipsCmd} --ignore-duplicates -j $out $tmpJar $in`,
38		CommandDeps: []string{
39			"${config.Zip2ZipCmd}",
40			"${config.JavaCmd}",
41			"${config.JacocoCLIJar}",
42			"${config.MergeZipsCmd}",
43		},
44	},
45		"strippedJar", "stripSpec", "tmpDir", "tmpJar")
46)
47
48func jacocoDepsMutator(ctx android.BottomUpMutatorContext) {
49	type instrumentable interface {
50		shouldInstrument(ctx android.BaseModuleContext) bool
51		shouldInstrumentInApex(ctx android.BaseModuleContext) bool
52		setInstrument(value bool)
53	}
54
55	j, ok := ctx.Module().(instrumentable)
56	if !ctx.Module().Enabled(ctx) || !ok {
57		return
58	}
59
60	if j.shouldInstrumentInApex(ctx) {
61		j.setInstrument(true)
62	}
63
64	if j.shouldInstrument(ctx) && ctx.ModuleName() != "jacocoagent" {
65		// We can use AddFarVariationDependencies here because, since this dep
66		// is added as libs only (i.e. a compiletime CLASSPATH entry only),
67		// the first variant of jacocoagent is sufficient to prevent
68		// compile time errors.
69		// At this stage in the build, AddVariationDependencies is not always
70		// able to procure a variant of jacocoagent that matches the calling
71		// module.
72		ctx.AddFarVariationDependencies(ctx.Module().Target().Variations(), libTag, "jacocoagent")
73	}
74}
75
76// Instruments a jar using the Jacoco command line interface.  Uses stripSpec to extract a subset
77// of the classes in inputJar into strippedJar, instruments strippedJar into tmpJar, and then
78// combines the classes in tmpJar with inputJar (preferring the instrumented classes in tmpJar)
79// to produce instrumentedJar.
80func jacocoInstrumentJar(ctx android.ModuleContext, instrumentedJar, strippedJar android.WritablePath,
81	inputJar android.Path, stripSpec string) {
82
83	// The basename of tmpJar has to be the same as the basename of strippedJar
84	tmpJar := android.PathForModuleOut(ctx, "jacoco", "tmp", strippedJar.Base())
85
86	ctx.Build(pctx, android.BuildParams{
87		Rule:           jacoco,
88		Description:    "jacoco",
89		Output:         instrumentedJar,
90		ImplicitOutput: strippedJar,
91		Input:          inputJar,
92		Args: map[string]string{
93			"strippedJar": strippedJar.String(),
94			"stripSpec":   stripSpec,
95			"tmpDir":      filepath.Dir(tmpJar.String()),
96			"tmpJar":      tmpJar.String(),
97		},
98	})
99}
100
101func (j *Module) jacocoModuleToZipCommand(ctx android.ModuleContext) string {
102	includes, err := jacocoFiltersToSpecs(j.properties.Jacoco.Include_filter)
103	if err != nil {
104		ctx.PropertyErrorf("jacoco.include_filter", "%s", err.Error())
105	}
106	// Also include the default list of classes to exclude from instrumentation.
107	excludes, err := jacocoFiltersToSpecs(append(j.properties.Jacoco.Exclude_filter, config.DefaultJacocoExcludeFilter...))
108	if err != nil {
109		ctx.PropertyErrorf("jacoco.exclude_filter", "%s", err.Error())
110	}
111
112	return jacocoFiltersToZipCommand(includes, excludes)
113}
114
115func jacocoFiltersToZipCommand(includes, excludes []string) string {
116	specs := ""
117	if len(excludes) > 0 {
118		specs += android.JoinWithPrefix(excludes, "-x ") + " "
119	}
120	if len(includes) > 0 {
121		specs += strings.Join(includes, " ")
122	} else {
123		specs += "'**/*.class'"
124	}
125	return specs
126}
127
128func jacocoFiltersToSpecs(filters []string) ([]string, error) {
129	specs := make([]string, len(filters))
130	var err error
131	for i, f := range filters {
132		specs[i], err = jacocoFilterToSpec(f)
133		if err != nil {
134			return nil, err
135		}
136	}
137	return proptools.NinjaAndShellEscapeList(specs), nil
138}
139
140func jacocoFilterToSpec(filter string) (string, error) {
141	recursiveWildcard := strings.HasSuffix(filter, "**")
142	nonRecursiveWildcard := false
143	if !recursiveWildcard {
144		nonRecursiveWildcard = strings.HasSuffix(filter, "*")
145		filter = strings.TrimSuffix(filter, "*")
146	} else {
147		filter = strings.TrimSuffix(filter, "**")
148	}
149
150	if recursiveWildcard && !(strings.HasSuffix(filter, ".") || filter == "") {
151		return "", fmt.Errorf("only '**' or '.**' is supported as recursive wildcard in a filter")
152	}
153
154	if strings.ContainsRune(filter, '*') {
155		return "", fmt.Errorf("'*' is only supported as the last character in a filter")
156	}
157
158	spec := strings.Replace(filter, ".", "/", -1)
159
160	if recursiveWildcard {
161		spec += "**/*.class"
162	} else if nonRecursiveWildcard {
163		spec += "*.class"
164	} else {
165		spec += ".class"
166	}
167
168	return spec, nil
169}
170