blob: a5d18e06dff970457c92283e15e740fa257a263a [file] [log] [blame]
Colin Cross0ef08162019-05-01 15:50:51 -07001// Copyright 2019 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
17import (
18 "fmt"
19 "io"
Colin Crossd2d11772019-05-30 11:17:23 -070020 "strconv"
Colin Cross0ef08162019-05-01 15:50:51 -070021 "strings"
22
23 "android/soong/android"
24)
25
26func init() {
27 android.RegisterModuleType("android_robolectric_test", RobolectricTestFactory)
28}
29
30var robolectricDefaultLibs = []string{
31 "robolectric_android-all-stub",
32 "Robolectric_all-target",
33 "mockito-robolectric-prebuilt",
34 "truth-prebuilt",
35}
36
Colin Cross3ec27ec2019-05-01 15:54:05 -070037var (
38 roboCoverageLibsTag = dependencyTag{name: "roboSrcs"}
39)
40
Colin Cross0ef08162019-05-01 15:50:51 -070041type robolectricProperties struct {
42 // The name of the android_app module that the tests will run against.
43 Instrumentation_for *string
44
Colin Cross3ec27ec2019-05-01 15:54:05 -070045 // Additional libraries for which coverage data should be generated
46 Coverage_libs []string
47
Colin Cross0ef08162019-05-01 15:50:51 -070048 Test_options struct {
49 // Timeout in seconds when running the tests.
Colin Cross2f9a7c82019-05-30 11:16:26 -070050 Timeout *int64
Colin Crossd2d11772019-05-30 11:17:23 -070051
52 // Number of shards to use when running the tests.
53 Shards *int64
Colin Cross0ef08162019-05-01 15:50:51 -070054 }
55}
56
57type robolectricTest struct {
58 Library
59
60 robolectricProperties robolectricProperties
61
Colin Crossd2d11772019-05-30 11:17:23 -070062 libs []string
63 tests []string
Colin Cross3ec27ec2019-05-01 15:54:05 -070064
65 roboSrcJar android.Path
Colin Cross0ef08162019-05-01 15:50:51 -070066}
67
68func (r *robolectricTest) DepsMutator(ctx android.BottomUpMutatorContext) {
69 r.Library.DepsMutator(ctx)
70
71 if r.robolectricProperties.Instrumentation_for != nil {
72 ctx.AddVariationDependencies(nil, instrumentationForTag, String(r.robolectricProperties.Instrumentation_for))
73 } else {
74 ctx.PropertyErrorf("instrumentation_for", "missing required instrumented module")
75 }
76
77 ctx.AddVariationDependencies(nil, libTag, robolectricDefaultLibs...)
Colin Cross3ec27ec2019-05-01 15:54:05 -070078
79 ctx.AddVariationDependencies(nil, roboCoverageLibsTag, r.robolectricProperties.Coverage_libs...)
Colin Cross0ef08162019-05-01 15:50:51 -070080}
81
82func (r *robolectricTest) GenerateAndroidBuildActions(ctx android.ModuleContext) {
Colin Cross3ec27ec2019-05-01 15:54:05 -070083 roboTestConfig := android.PathForModuleGen(ctx, "robolectric").
84 Join(ctx, "com/android/tools/test_config.properties")
85
86 // TODO: this inserts paths to built files into the test, it should really be inserting the contents.
87 instrumented := ctx.GetDirectDepsWithTag(instrumentationForTag)
88
89 if len(instrumented) != 1 {
90 panic(fmt.Errorf("expected exactly 1 instrumented dependency, got %d", len(instrumented)))
91 }
92
93 instrumentedApp, ok := instrumented[0].(*AndroidApp)
94 if !ok {
95 ctx.PropertyErrorf("instrumentation_for", "dependency must be an android_app")
96 }
97
98 generateRoboTestConfig(ctx, roboTestConfig, instrumentedApp)
99 r.extraResources = android.Paths{roboTestConfig}
100
Colin Cross0ef08162019-05-01 15:50:51 -0700101 r.Library.GenerateAndroidBuildActions(ctx)
102
Colin Cross3ec27ec2019-05-01 15:54:05 -0700103 roboSrcJar := android.PathForModuleGen(ctx, "robolectric", ctx.ModuleName()+".srcjar")
104 r.generateRoboSrcJar(ctx, roboSrcJar, instrumentedApp)
105 r.roboSrcJar = roboSrcJar
106
Colin Cross0ef08162019-05-01 15:50:51 -0700107 for _, dep := range ctx.GetDirectDepsWithTag(libTag) {
108 r.libs = append(r.libs, ctx.OtherModuleName(dep))
109 }
Colin Crossd2d11772019-05-30 11:17:23 -0700110
111 // TODO: this could all be removed if tradefed was used as the test runner, it will find everything
112 // annotated as a test and run it.
113 for _, src := range r.compiledJavaSrcs {
114 s := src.Rel()
115 if !strings.HasSuffix(s, "Test.java") {
116 continue
117 } else if strings.HasSuffix(s, "/BaseRobolectricTest.java") {
118 continue
119 } else if strings.HasPrefix(s, "src/") {
120 s = strings.TrimPrefix(s, "src/")
121 }
122 r.tests = append(r.tests, s)
123 }
124}
125
126func shardTests(paths []string, shards int) [][]string {
127 if shards > len(paths) {
128 shards = len(paths)
129 }
130 if shards == 0 {
131 return nil
132 }
133 ret := make([][]string, 0, shards)
134 shardSize := (len(paths) + shards - 1) / shards
135 for len(paths) > shardSize {
136 ret = append(ret, paths[0:shardSize])
137 paths = paths[shardSize:]
138 }
139 if len(paths) > 0 {
140 ret = append(ret, paths)
141 }
142 return ret
Colin Cross0ef08162019-05-01 15:50:51 -0700143}
144
Colin Cross3ec27ec2019-05-01 15:54:05 -0700145func generateRoboTestConfig(ctx android.ModuleContext, outputFile android.WritablePath, instrumentedApp *AndroidApp) {
146 manifest := instrumentedApp.mergedManifestFile
147 resourceApk := instrumentedApp.outputFile
148
149 rule := android.NewRuleBuilder()
150
151 rule.Command().Text("rm -f").Output(outputFile)
152 rule.Command().
153 Textf(`echo "android_merged_manifest=%s" >>`, manifest.String()).Output(outputFile).Text("&&").
154 Textf(`echo "android_resource_apk=%s" >>`, resourceApk.String()).Output(outputFile).
155 // Make it depend on the files to which it points so the test file's timestamp is updated whenever the
156 // contents change
157 Implicit(manifest).
158 Implicit(resourceApk)
159
160 rule.Build(pctx, ctx, "generate_test_config", "generate test_config.properties")
161}
162
163func (r *robolectricTest) generateRoboSrcJar(ctx android.ModuleContext, outputFile android.WritablePath,
164 instrumentedApp *AndroidApp) {
165
166 srcJarArgs := copyOf(instrumentedApp.srcJarArgs)
167 srcJarDeps := append(android.Paths(nil), instrumentedApp.srcJarDeps...)
168
169 for _, m := range ctx.GetDirectDepsWithTag(roboCoverageLibsTag) {
170 if dep, ok := m.(Dependency); ok {
171 depSrcJarArgs, depSrcJarDeps := dep.SrcJarArgs()
172 srcJarArgs = append(srcJarArgs, depSrcJarArgs...)
173 srcJarDeps = append(srcJarDeps, depSrcJarDeps...)
174 }
175 }
176
177 TransformResourcesToJar(ctx, outputFile, srcJarArgs, srcJarDeps)
178}
179
Jaewoong Jungb0c127c2019-08-29 14:56:03 -0700180func (r *robolectricTest) AndroidMkEntries() android.AndroidMkEntries {
181 entries := r.Library.AndroidMkEntries()
Colin Cross0ef08162019-05-01 15:50:51 -0700182
Jaewoong Jungb0c127c2019-08-29 14:56:03 -0700183 entries.ExtraFooters = []android.AndroidMkExtraFootersFunc{
184 func(w io.Writer, name, prefix, moduleDir string, entries *android.AndroidMkEntries) {
185 if s := r.robolectricProperties.Test_options.Shards; s != nil && *s > 1 {
186 shards := shardTests(r.tests, int(*s))
187 for i, shard := range shards {
188 r.writeTestRunner(w, name, "Run"+name+strconv.Itoa(i), shard)
189 }
Colin Cross0ef08162019-05-01 15:50:51 -0700190
Jaewoong Jungb0c127c2019-08-29 14:56:03 -0700191 // TODO: add rules to dist the outputs of the individual tests, or combine them together?
192 fmt.Fprintln(w, "")
193 fmt.Fprintln(w, ".PHONY:", "Run"+name)
194 fmt.Fprintln(w, "Run"+name, ": \\")
195 for i := range shards {
196 fmt.Fprintln(w, " ", "Run"+name+strconv.Itoa(i), "\\")
197 }
198 fmt.Fprintln(w, "")
199 } else {
200 r.writeTestRunner(w, name, "Run"+name, r.tests)
Colin Crossd2d11772019-05-30 11:17:23 -0700201 }
Jaewoong Jungb0c127c2019-08-29 14:56:03 -0700202 },
Colin Cross0ef08162019-05-01 15:50:51 -0700203 }
204
Jaewoong Jungb0c127c2019-08-29 14:56:03 -0700205 return entries
Colin Cross0ef08162019-05-01 15:50:51 -0700206}
207
Colin Crossd2d11772019-05-30 11:17:23 -0700208func (r *robolectricTest) writeTestRunner(w io.Writer, module, name string, tests []string) {
209 fmt.Fprintln(w, "")
210 fmt.Fprintln(w, "include $(CLEAR_VARS)")
211 fmt.Fprintln(w, "LOCAL_MODULE :=", name)
212 fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES :=", module)
213 fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES += ", strings.Join(r.libs, " "))
214 fmt.Fprintln(w, "LOCAL_TEST_PACKAGE :=", String(r.robolectricProperties.Instrumentation_for))
215 fmt.Fprintln(w, "LOCAL_INSTRUMENT_SRCJARS :=", r.roboSrcJar.String())
216 fmt.Fprintln(w, "LOCAL_ROBOTEST_FILES :=", strings.Join(tests, " "))
217 if t := r.robolectricProperties.Test_options.Timeout; t != nil {
218 fmt.Fprintln(w, "LOCAL_ROBOTEST_TIMEOUT :=", *t)
219 }
220 fmt.Fprintln(w, "-include external/robolectric-shadows/run_robotests.mk")
221
222}
223
Colin Cross0ef08162019-05-01 15:50:51 -0700224// An android_robolectric_test module compiles tests against the Robolectric framework that can run on the local host
225// instead of on a device. It also generates a rule with the name of the module prefixed with "Run" that can be
226// used to run the tests. Running the tests with build rule will eventually be deprecated and replaced with atest.
Colin Crossd2d11772019-05-30 11:17:23 -0700227//
228// The test runner considers any file listed in srcs whose name ends with Test.java to be a test class, unless
229// it is named BaseRobolectricTest.java. The path to the each source file must exactly match the package
230// name, or match the package name when the prefix "src/" is removed.
Colin Cross0ef08162019-05-01 15:50:51 -0700231func RobolectricTestFactory() android.Module {
232 module := &robolectricTest{}
233
234 module.AddProperties(
235 &module.Module.properties,
236 &module.Module.protoProperties,
237 &module.robolectricProperties)
238
239 module.Module.dexpreopter.isTest = true
240
241 InitJavaModule(module, android.DeviceSupported)
242 return module
243}