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.Ziptime} $tmpJar && ` +
38			`${config.MergeZipsCmd} --ignore-duplicates -j $out $tmpJar $in`,
39		CommandDeps: []string{
40			"${config.Zip2ZipCmd}",
41			"${config.JavaCmd}",
42			"${config.JacocoCLIJar}",
43			"${config.Ziptime}",
44			"${config.MergeZipsCmd}",
45		},
46	},
47		"strippedJar", "stripSpec", "tmpDir", "tmpJar")
48)
49
50// Instruments a jar using the Jacoco command line interface.  Uses stripSpec to extract a subset
51// of the classes in inputJar into strippedJar, instruments strippedJar into tmpJar, and then
52// combines the classes in tmpJar with inputJar (preferring the instrumented classes in tmpJar)
53// to produce instrumentedJar.
54func jacocoInstrumentJar(ctx android.ModuleContext, instrumentedJar, strippedJar android.WritablePath,
55	inputJar android.Path, stripSpec string) {
56
57	// The basename of tmpJar has to be the same as the basename of strippedJar
58	tmpJar := android.PathForModuleOut(ctx, "jacoco", "tmp", strippedJar.Base())
59
60	ctx.Build(pctx, android.BuildParams{
61		Rule:           jacoco,
62		Description:    "jacoco",
63		Output:         instrumentedJar,
64		ImplicitOutput: strippedJar,
65		Input:          inputJar,
66		Args: map[string]string{
67			"strippedJar": strippedJar.String(),
68			"stripSpec":   stripSpec,
69			"tmpDir":      filepath.Dir(tmpJar.String()),
70			"tmpJar":      tmpJar.String(),
71		},
72	})
73}
74
75func (j *Module) jacocoModuleToZipCommand(ctx android.ModuleContext) string {
76	includes, err := jacocoFiltersToSpecs(j.properties.Jacoco.Include_filter)
77	if err != nil {
78		ctx.PropertyErrorf("jacoco.include_filter", "%s", err.Error())
79	}
80	// Also include the default list of classes to exclude from instrumentation.
81	excludes, err := jacocoFiltersToSpecs(append(j.properties.Jacoco.Exclude_filter, config.DefaultJacocoExcludeFilter...))
82	if err != nil {
83		ctx.PropertyErrorf("jacoco.exclude_filter", "%s", err.Error())
84	}
85
86	return jacocoFiltersToZipCommand(includes, excludes)
87}
88
89func jacocoFiltersToZipCommand(includes, excludes []string) string {
90	specs := ""
91	if len(excludes) > 0 {
92		specs += android.JoinWithPrefix(excludes, "-x ") + " "
93	}
94	if len(includes) > 0 {
95		specs += strings.Join(includes, " ")
96	} else {
97		specs += "**/*.class"
98	}
99	return specs
100}
101
102func jacocoFiltersToSpecs(filters []string) ([]string, error) {
103	specs := make([]string, len(filters))
104	var err error
105	for i, f := range filters {
106		specs[i], err = jacocoFilterToSpec(f)
107		if err != nil {
108			return nil, err
109		}
110	}
111	return proptools.NinjaAndShellEscapeList(specs), nil
112}
113
114func jacocoFilterToSpec(filter string) (string, error) {
115	recursiveWildcard := strings.HasSuffix(filter, "**")
116	nonRecursiveWildcard := false
117	if !recursiveWildcard {
118		nonRecursiveWildcard = strings.HasSuffix(filter, "*")
119		filter = strings.TrimSuffix(filter, "*")
120	} else {
121		filter = strings.TrimSuffix(filter, "**")
122	}
123
124	if recursiveWildcard && !(strings.HasSuffix(filter, ".") || filter == "") {
125		return "", fmt.Errorf("only '**' or '.**' is supported as recursive wildcard in a filter")
126	}
127
128	if strings.ContainsRune(filter, '*') {
129		return "", fmt.Errorf("'*' is only supported as the last character in a filter")
130	}
131
132	spec := strings.Replace(filter, ".", "/", -1)
133
134	if recursiveWildcard {
135		spec += "**/*.class"
136	} else if nonRecursiveWildcard {
137		spec += "*.class"
138	} else {
139		spec += ".class"
140	}
141
142	return spec, nil
143}
144