aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Ducrohet <xav@android.com>2013-01-04 14:58:14 -0800
committerXavier Ducrohet <xav@android.com>2013-01-10 18:16:40 -0800
commit06dc485552957e6b0492e269e2fa0347790498e9 (patch)
tree990d72fbe227d42a425c1913fd93fdc1ab90d467
parent1ac02d56be392643eb31a855fb418cfa926b63f8 (diff)
downloadbuild-06dc485552957e6b0492e269e2fa0347790498e9.tar.gz
Custom Junit test report.
Duplicated the report classes from Gradle to customize them in order to show the device the test run on, or which flavor and project the test is coming from. This duplicated code is in its own source folder to separate this from AOSP code and retains the original copyright. The "test" tasks which runs all test<Flavor> tasks now also aggregate the results of the test<Flavor> tasks and is designed to run even when those fails by telling them to ignore errors if the "test" task is set to run. Added a new plugin strictly to do aggregate for the subprojects. Plugin is called 'android-reporting' It's meant to be used by the root project. It behaves similarly as the "test" class in the android project, by telling sub tasks to ignore errors. The aggregate tasks will throw an exception if the sub test tasks saw errors in order to ensure that the caller to gradle can manage errors still. Change-Id: I2c1df77cf8073fb74b4145b09aa7609d7999ef6c
-rw-r--r--builder/src/main/java/com/android/builder/testing/CustomTestRunListener.java25
-rw-r--r--gradle/NOTICE22
-rw-r--r--gradle/README17
-rw-r--r--gradle/build.gradle4
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/AllTestResults.java85
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassPageRenderer.java253
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassTestResults.java109
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/CompositeTestResults.java132
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/OverviewPageRenderer.java137
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackagePageRenderer.java84
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackageTestResults.java68
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PageRenderer.java184
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestReport.java149
-rw-r--r--gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestResult.java136
-rw-r--r--gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/base-style.css162
-rw-r--r--gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/report.js101
-rw-r--r--gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/style.css81
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/AppPlugin.groovy47
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy76
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/BuildVariant.groovy2
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/LibraryPlugin.groovy2
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy93
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/ApplicationVariant.groovy12
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/DefaultBuildVariant.groovy4
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.groovy133
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidTestTask.java32
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestFlavorTask.groovy (renamed from gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy)46
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestLibraryTask.groovy24
-rw-r--r--gradle/src/main/groovy/com/android/build/gradle/internal/test/report/ReportType.java35
-rw-r--r--gradle/src/main/resources/META-INF/gradle-plugins/android-reporting.properties1
-rw-r--r--gradle/src/test/groovy/com/android/build/gradle/AppPluginDslTest.groovy4
-rw-r--r--gradle/src/test/groovy/com/android/build/gradle/LibraryPluginDslTest.groovy4
-rw-r--r--tests/flavorlib/build.gradle2
-rw-r--r--tests/flavorlibWithFailedTests/build.gradle2
34 files changed, 2214 insertions, 54 deletions
diff --git a/builder/src/main/java/com/android/builder/testing/CustomTestRunListener.java b/builder/src/main/java/com/android/builder/testing/CustomTestRunListener.java
index 762d1e0..5b61a94 100644
--- a/builder/src/main/java/com/android/builder/testing/CustomTestRunListener.java
+++ b/builder/src/main/java/com/android/builder/testing/CustomTestRunListener.java
@@ -23,6 +23,7 @@ import com.android.ddmlib.testrunner.TestResult;
import com.android.ddmlib.testrunner.XmlTestRunListener;
import com.android.utils.ILogger;
import com.google.common.collect.Sets;
+import org.kxml2.io.KXmlSerializer;
import java.io.File;
import java.io.IOException;
@@ -34,20 +35,29 @@ import java.util.Set;
*/
public class CustomTestRunListener extends XmlTestRunListener {
+ @NonNull
private final String mDeviceName;
+ @NonNull
+ private final String mProjectName;
+ @NonNull
+ private final String mFlavorName;
private final ILogger mLogger;
private final Set<TestIdentifier> mFailedTests = Sets.newHashSet();
- public CustomTestRunListener(@NonNull String deviceName, @Nullable ILogger logger) {
+ public CustomTestRunListener(@NonNull String deviceName,
+ @NonNull String projectName, @NonNull String flavorName,
+ @Nullable ILogger logger) {
mDeviceName = deviceName;
+ mProjectName = projectName;
+ mFlavorName = flavorName;
mLogger = logger;
- setHostName(mDeviceName);
}
@Override
protected File getResultFile(File reportDir) throws IOException {
- return new File(reportDir, "TEST-" + mDeviceName + ".xml");
+ return new File(reportDir,
+ "TEST-" + mDeviceName + "-" + mProjectName + "-" + mFlavorName + ".xml");
}
@Override
@@ -65,8 +75,13 @@ public class CustomTestRunListener extends XmlTestRunListener {
}
@Override
- protected String getTestName(TestIdentifier testId) {
- return String.format("%1$s[%2$s]", testId.getTestName(), mDeviceName);
+ protected void setPropertiesAttributes(KXmlSerializer serializer, String namespace)
+ throws IOException {
+ super.setPropertiesAttributes(serializer, namespace);
+
+ serializer.attribute(null, "device", mDeviceName);
+ serializer.attribute(null, "flavor", mFlavorName);
+ serializer.attribute(null, "project", mProjectName);
}
@Override
diff --git a/gradle/NOTICE b/gradle/NOTICE
index c77f135..448b38a 100644
--- a/gradle/NOTICE
+++ b/gradle/NOTICE
@@ -1,3 +1,25 @@
+============================================================
+Notices for file(s):
+/src/fromGradle/*
+------------------------------------------------------------
+
+ Copyright 2011 the original author or authors.
+
+ 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.
+
+============================================================
+Notices for all other files
+------------------------------------------------------------
Copyright (c) 2012, The Android Open Source Project
diff --git a/gradle/README b/gradle/README
new file mode 100644
index 0000000..1e5e0c7
--- /dev/null
+++ b/gradle/README
@@ -0,0 +1,17 @@
+The code under src/fromGradle/ comes from Gradle 1.3 and is under the following license:
+
+Copyright 2011 the original author or authors.
+
+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.
+
+
diff --git a/gradle/build.gradle b/gradle/build.gradle
index f627d3a..9a47299 100644
--- a/gradle/build.gradle
+++ b/gradle/build.gradle
@@ -9,6 +9,10 @@ configurations {
}
sourceSets {
+ main {
+ groovy.srcDirs 'src/main/groovy', 'src/fromGradle/groovy'
+ resources.srcDirs 'src/main/resources', 'src/fromGradle/resources'
+ }
buildTest {
groovy.srcDir file('src/build-test/groovy')
resources.srcDir file('src/build-test/resources')
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/AllTestResults.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/AllTestResults.java
new file mode 100644
index 0000000..33d01a8
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/AllTestResults.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ *
+ * Custom test results based on Gradle's AllTestResults
+ */
+class AllTestResults extends CompositeTestResults {
+ private final Map<String, PackageTestResults> packages = new TreeMap<String, PackageTestResults>();
+
+ public AllTestResults() {
+ super(null, null, null, null);
+ }
+
+ @Override
+ public String getTitle() {
+ return "Test Summary";
+ }
+
+ public Collection<PackageTestResults> getPackages() {
+ return packages.values();
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+ public TestResult addTest(String className, String testName, long duration,
+ String device, String project, String flavor) {
+ PackageTestResults packageResults = addPackageForClass(className,
+ device, project, flavor);
+ return addTest(packageResults.addTest(className, testName, duration,
+ device, project, flavor));
+ }
+
+ public ClassTestResults addTestClass(String className,
+ String device, String project, String flavor) {
+ return addPackageForClass(className, device, project, flavor).addClass(className,
+ device, project, flavor);
+ }
+
+ private PackageTestResults addPackageForClass(String className,
+ String device, String project, String flavor) {
+ String packageName;
+ int pos = className.lastIndexOf(".");
+ if (pos != -1) {
+ packageName = className.substring(0, pos);
+ } else {
+ packageName = "";
+ }
+ return addPackage(packageName, device, project, flavor);
+ }
+
+ private PackageTestResults addPackage(String packageName,
+ String device, String project, String flavor) {
+ String key = device + "/" + project + "/" + flavor + "/" + packageName;
+
+ PackageTestResults packageResults = packages.get(key);
+ if (packageResults == null) {
+ packageResults = new PackageTestResults(packageName, this, device, project, flavor);
+ packages.put(key, packageResults);
+ }
+ return packageResults;
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassPageRenderer.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassPageRenderer.java
new file mode 100644
index 0000000..292696f
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassPageRenderer.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.gradle.api.Action;
+import org.gradle.api.internal.tasks.testing.junit.report.TestFailure;
+import org.gradle.reporting.CodePanelRenderer;
+import org.w3c.dom.Element;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.gradle.api.tasks.testing.TestResult.ResultType;
+
+/**
+ * Custom ClassPageRenderer based on Gradle's ClassPageRenderer
+ */
+class ClassPageRenderer extends PageRenderer<ClassTestResults> {
+ private final CodePanelRenderer codePanelRenderer = new CodePanelRenderer();
+
+ ClassPageRenderer(ReportType reportType) {
+ super(reportType);
+ }
+
+ @Override
+ protected String getTitle() {
+ ClassTestResults model = getModel();
+
+ switch (reportType) {
+ case MULTI_PROJECT:
+ return model.getProject() + ": " + model.getFlavor() + ": " + model.getTitle();
+ case MULTI_FLAVOR:
+ return model.getFlavor() + ": " + model.getTitle();
+ }
+
+ return model.getTitle();
+ }
+
+ @Override protected void renderBreadcrumbs(Element parent) {
+ Element div = append(parent, "div");
+ div.setAttribute("class", "breadcrumbs");
+ appendLink(div, "index.html", "all");
+ appendText(div, " > ");
+ appendLink(div,
+ String.format("%s.html", getResults().getPackageResults().getFilename(reportType)),
+ getResults().getPackageResults().getName());
+ appendText(div, String.format(" > %s", getResults().getSimpleName()));
+ }
+
+ private void renderTests(Element parent) {
+ Element table = append(parent, "table");
+ Element thead = append(table, "thead");
+ Element tr = append(thead, "tr");
+
+ // get all the results per device and per test name
+ Map<String, Map<String, TestResult>> results = getResults().getTestResultsMap();
+
+ // gather all devices.
+ List<String> devices = Lists.newArrayList(results.keySet());
+ Collections.sort(devices);
+
+ appendWithText(tr, "th", "Test");
+ for (String device : devices) {
+ appendWithText(tr, "th", device);
+ }
+
+ // gather all tests
+ Set<String> tests = Sets.newHashSet();
+ for (Map<String, TestResult> deviceMap : results.values()) {
+ tests.addAll(deviceMap.keySet());
+ }
+ List<String> sortedTests = Lists.newArrayList(tests);
+ Collections.sort(sortedTests);
+
+ for (String testName : sortedTests) {
+ tr = append(table, "tr");
+ Element td = appendWithText(tr, "td", testName);
+
+ ResultType currentType = ResultType.SKIPPED;
+
+ // loop for all devices to find this test and put its result
+ for (String device : devices) {
+ Map<String, TestResult> deviceMap = results.get(device);
+ TestResult test = deviceMap.get(testName);
+
+ Element deviceTd = appendWithText(tr, "td", test.getFormattedResultType());
+ deviceTd.setAttribute("class", test.getStatusClass());
+
+ currentType = combineResultType(currentType, test.getResultType());
+ }
+
+ // finally based on whether if a single test failed, set the class on the test name.
+ td.setAttribute("class", getStatusClass(currentType));
+ }
+ }
+
+ public static ResultType combineResultType(ResultType currentType, ResultType newType) {
+ switch (currentType) {
+ case SUCCESS:
+ if (newType == ResultType.FAILURE) {
+ return newType;
+ }
+
+ return currentType;
+ case FAILURE:
+ return currentType;
+ case SKIPPED:
+ if (newType != ResultType.SKIPPED) {
+ return newType;
+ }
+ return currentType;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ public String getStatusClass(ResultType resultType) {
+ switch (resultType) {
+ case SUCCESS:
+ return "success";
+ case FAILURE:
+ return "failures";
+ case SKIPPED:
+ return "skipped";
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private static final class TestPercent {
+ int failed;
+ int total;
+ TestPercent(int failed, int total) {
+ this.failed = failed;
+ this.total = total;
+ }
+
+ boolean isFullFailure() {
+ return failed == total;
+ }
+ }
+
+ @Override
+ protected void renderFailures(Element parent) {
+ // get all the results per device and per test name
+ Map<String, Map<String, TestResult>> results = getResults().getTestResultsMap();
+
+ Map<String, TestPercent> testPassPercent = Maps.newHashMap();
+
+ for (TestResult test : getResults().getFailures()) {
+ String testName = test.getName();
+ // compute the display name which will include the name of the device and how many
+ // devices are impact so to not force counting.
+ // If all devices, then we don't display all of them.
+ // (The off chance that all devices fail the test with a different stack trace is slim)
+ TestPercent percent = testPassPercent.get(testName);
+ if (percent != null && percent.isFullFailure()) {
+ continue;
+ }
+
+ if (percent == null) {
+ int failed = 0;
+ int total = 0;
+ for (Map<String, TestResult> deviceMap : results.values()) {
+ ResultType resultType = deviceMap.get(testName).getResultType();
+
+ if (resultType == ResultType.FAILURE) {
+ failed++;
+ }
+
+ if (resultType != ResultType.SKIPPED) {
+ total++;
+ }
+ }
+
+ percent = new TestPercent(failed, total);
+ testPassPercent.put(testName, percent);
+ }
+
+ Element div = append(parent, "div");
+ div.setAttribute("class", "test");
+ append(div, "a").setAttribute("name", test.getId().toString());
+
+ String name;
+ if (percent.total == 1) {
+ name = testName;
+ } else if (percent.isFullFailure()) {
+ name = testName + " [all devices]";
+ } else {
+ name = String.format("%s [%s] (on %d/%d devices)", testName, test.getDevice(),
+ percent.failed, percent.total);
+ }
+
+ appendWithText(div, "h3", name).setAttribute("class", test.getStatusClass());
+ for (TestFailure failure : test.getFailures()) {
+ codePanelRenderer.render(failure.getStackTrace(), div);
+ }
+ }
+ }
+
+ private void renderStdOut(Element parent) {
+ codePanelRenderer.render(getResults().getStandardOutput().toString(), parent);
+ }
+
+ private void renderStdErr(Element parent) {
+ codePanelRenderer.render(getResults().getStandardError().toString(), parent);
+ }
+
+ @Override protected void registerTabs() {
+ addFailuresTab();
+ addTab("Tests", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderTests(element);
+ }
+ });
+ if (getResults().getStandardOutput().length() > 0) {
+ addTab("Standard output", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderStdOut(element);
+ }
+ });
+ }
+ if (getResults().getStandardError().length() > 0) {
+ addTab("Standard error", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderStdErr(element);
+ }
+ });
+ }
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassTestResults.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassTestResults.java
new file mode 100644
index 0000000..4108b52
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/ClassTestResults.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import com.google.common.collect.Maps;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Custom ClassTestResults based on Gradle's ClassTestResults
+ */
+class ClassTestResults extends CompositeTestResults {
+
+ private final String name;
+ private final PackageTestResults packageResults;
+ private final Set<TestResult> results = new TreeSet<TestResult>();
+ private final StringBuilder standardOutput = new StringBuilder();
+ private final StringBuilder standardError = new StringBuilder();
+
+ public ClassTestResults(String name, PackageTestResults packageResults,
+ String device, String project, String flavor) {
+ super(packageResults, device, project, flavor);
+ this.name = name;
+ this.packageResults = packageResults;
+ }
+
+ @Override
+ public String getTitle() {
+ return String.format("Class %s", name);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public String getSimpleName() {
+ int pos = name.lastIndexOf(".");
+ if (pos != -1) {
+ return name.substring(pos + 1);
+ }
+ return name;
+ }
+
+ public PackageTestResults getPackageResults() {
+ return packageResults;
+ }
+
+ public Map<String, Map<String, TestResult>> getTestResultsMap() {
+ Map<String, Map<String, TestResult>> map = Maps.newHashMap();
+ for (TestResult result : results) {
+ String device = result.getDevice();
+
+ Map<String, TestResult> deviceMap = map.get(device);
+ if (deviceMap == null) {
+ deviceMap = Maps.newHashMap();
+ map.put(device, deviceMap);
+ }
+
+ deviceMap.put(result.getName(), result);
+ }
+
+ return map;
+ }
+
+ public Collection<TestResult> getTestResults() {
+ return results;
+ }
+
+ public CharSequence getStandardError() {
+ return standardError;
+ }
+
+ public CharSequence getStandardOutput() {
+ return standardOutput;
+ }
+
+ public TestResult addTest(String testName, long duration,
+ String device, String project, String flavor) {
+ TestResult test = new TestResult(testName, duration, device, project, flavor, this);
+ results.add(test);
+ return addTest(test);
+ }
+
+ public void addStandardOutput(String textContent) {
+ standardOutput.append(textContent);
+ }
+
+ public void addStandardError(String textContent) {
+ standardError.append(textContent);
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/CompositeTestResults.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/CompositeTestResults.java
new file mode 100644
index 0000000..1fe3eda
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/CompositeTestResults.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import org.gradle.api.internal.tasks.testing.junit.report.TestResultModel;
+
+import java.math.BigDecimal;
+import java.util.Set;
+import java.util.TreeSet;
+
+import static org.gradle.api.tasks.testing.TestResult.ResultType;
+
+/**
+ * Custom CompositeTestResults based on Gradle's CompositeTestResults
+ */
+public abstract class CompositeTestResults extends TestResultModel {
+ private final CompositeTestResults parent;
+ private int tests;
+ private final Set<TestResult> failures = new TreeSet<TestResult>();
+ private long duration;
+
+ private final String device;
+ private final String project;
+ private final String flavor;
+
+ protected CompositeTestResults(CompositeTestResults parent,
+ String device, String project, String flavor) {
+ this.parent = parent;
+ this.device = device;
+ this.project = project;
+ this.flavor = flavor;
+ }
+
+ public String getFilename(ReportType reportType) {
+ switch (reportType) {
+ case MULTI_PROJECT:
+ return project + "-" + flavor + "-" + getName();
+ case MULTI_FLAVOR:
+ return flavor + "-" + getName();
+ }
+
+ return getName();
+ }
+
+ public abstract String getName();
+
+ public int getTestCount() {
+ return tests;
+ }
+
+ public int getFailureCount() {
+ return failures.size();
+ }
+
+ public String getDevice() {
+ return device;
+ }
+
+ public String getProject() {
+ return project;
+ }
+
+ public String getFlavor() {
+ return flavor;
+ }
+
+ @Override
+ public long getDuration() {
+ return duration;
+ }
+
+ @Override
+ public String getFormattedDuration() {
+ return getTestCount() == 0 ? "-" : super.getFormattedDuration();
+ }
+
+ public Set<TestResult> getFailures() {
+ return failures;
+ }
+
+ @Override
+ public ResultType getResultType() {
+ return failures.isEmpty() ? ResultType.SUCCESS : ResultType.FAILURE;
+ }
+
+ public String getFormattedSuccessRate() {
+ Number successRate = getSuccessRate();
+ if (successRate == null) {
+ return "-";
+ }
+ return successRate + "%";
+ }
+
+ public Number getSuccessRate() {
+ if (getTestCount() == 0) {
+ return null;
+ }
+
+ BigDecimal tests = BigDecimal.valueOf(getTestCount());
+ BigDecimal successful = BigDecimal.valueOf(getTestCount() - getFailureCount());
+
+ return successful.divide(tests, 2,
+ BigDecimal.ROUND_DOWN).multiply(BigDecimal.valueOf(100)).intValue();
+ }
+
+ protected void failed(TestResult failedTest) {
+ failures.add(failedTest);
+ if (parent != null) {
+ parent.failed(failedTest);
+ }
+ }
+
+ protected TestResult addTest(TestResult test) {
+ tests++;
+ duration += test.getDuration();
+ return test;
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/OverviewPageRenderer.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/OverviewPageRenderer.java
new file mode 100644
index 0000000..d51c701
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/OverviewPageRenderer.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+
+import org.gradle.api.Action;
+import org.w3c.dom.Element;
+
+/**
+ * Custom OverviewPageRenderer based on Gradle's OverviewPageRenderer
+ */
+class OverviewPageRenderer extends PageRenderer<AllTestResults> {
+
+ public OverviewPageRenderer(ReportType reportType) {
+ super(reportType);
+ }
+
+ @Override protected void registerTabs() {
+ addFailuresTab();
+ if (!getResults().getPackages().isEmpty()) {
+ addTab("Packages", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderPackages(element);
+ }
+ });
+ }
+ addTab("Classes", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderClasses(element);
+ }
+ });
+ }
+
+ @Override protected void renderBreadcrumbs(Element element) {
+ }
+
+ private void renderPackages(Element parent) {
+ Element table = append(parent, "table");
+ Element thead = append(table, "thead");
+ Element tr = append(thead, "tr");
+ if (reportType == ReportType.MULTI_PROJECT) {
+ appendWithText(tr, "th", "Project");
+ appendWithText(tr, "th", "Flavor");
+ } else if (reportType == ReportType.MULTI_FLAVOR) {
+ appendWithText(tr, "th", "Flavor");
+ }
+ appendWithText(tr, "th", "Package");
+ appendWithText(tr, "th", "Tests");
+ appendWithText(tr, "th", "Failures");
+ appendWithText(tr, "th", "Duration");
+ appendWithText(tr, "th", "Success rate");
+ for (PackageTestResults testPackage : getResults().getPackages()) {
+ tr = append(table, "tr");
+ Element td;
+
+ if (reportType == ReportType.MULTI_PROJECT) {
+ td = appendWithText(tr, "td", testPackage.getProject());
+ td.setAttribute("class", testPackage.getStatusClass());
+ td = appendWithText(tr, "td", testPackage.getFlavor());
+ td.setAttribute("class", testPackage.getStatusClass());
+ } else if (reportType == ReportType.MULTI_FLAVOR) {
+ td = appendWithText(tr, "td", testPackage.getFlavor());
+ td.setAttribute("class", testPackage.getStatusClass());
+ }
+
+ td = append(tr, "td");
+ td.setAttribute("class", testPackage.getStatusClass());
+ appendLink(td,
+ String.format("%s.html", testPackage.getFilename(reportType)),
+ testPackage.getName());
+ appendWithText(tr, "td", testPackage.getTestCount());
+ appendWithText(tr, "td", testPackage.getFailureCount());
+ appendWithText(tr, "td", testPackage.getFormattedDuration());
+ td = appendWithText(tr, "td", testPackage.getFormattedSuccessRate());
+ td.setAttribute("class", testPackage.getStatusClass());
+ }
+ }
+
+ private void renderClasses(Element parent) {
+ Element table = append(parent, "table");
+ Element thead = append(table, "thead");
+ Element tr = append(thead, "tr");
+ if (reportType == ReportType.MULTI_PROJECT) {
+ appendWithText(tr, "th", "Project");
+ appendWithText(tr, "th", "Flavor");
+ } else if (reportType == ReportType.MULTI_FLAVOR) {
+ appendWithText(tr, "th", "Flavor");
+ }
+ appendWithText(tr, "th", "Class");
+ appendWithText(tr, "th", "Tests");
+ appendWithText(tr, "th", "Failures");
+ appendWithText(tr, "th", "Duration");
+ appendWithText(tr, "th", "Success rate");
+ for (PackageTestResults testPackage : getResults().getPackages()) {
+ for (ClassTestResults testClass : testPackage.getClasses()) {
+ tr = append(table, "tr");
+ Element td;
+
+ if (reportType == ReportType.MULTI_PROJECT) {
+ td = appendWithText(tr, "td", testClass.getProject());
+ td.setAttribute("class", testClass.getStatusClass());
+ td = appendWithText(tr, "td", testClass.getFlavor());
+ td.setAttribute("class", testClass.getStatusClass());
+ } else if (reportType == ReportType.MULTI_FLAVOR) {
+ td = appendWithText(tr, "td", testClass.getFlavor());
+ td.setAttribute("class", testClass.getStatusClass());
+ }
+
+ td = append(tr, "td");
+ td.setAttribute("class", testClass.getStatusClass());
+ appendLink(td,
+ String.format("%s.html", testClass.getFilename(reportType)),
+ testClass.getName());
+ appendWithText(tr, "td", testClass.getTestCount());
+ appendWithText(tr, "td", testClass.getFailureCount());
+ appendWithText(tr, "td", testClass.getFormattedDuration());
+ td = appendWithText(tr, "td", testClass.getFormattedSuccessRate());
+ td.setAttribute("class", testClass.getStatusClass());
+ }
+ }
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackagePageRenderer.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackagePageRenderer.java
new file mode 100644
index 0000000..ce5e548
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackagePageRenderer.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import org.gradle.api.Action;
+import org.w3c.dom.Element;
+
+/**
+ * Custom PackagePageRenderer based on Gradle's PackagePageRenderer
+ */
+public class PackagePageRenderer extends PageRenderer<PackageTestResults> {
+
+ public PackagePageRenderer(ReportType reportType) {
+ super(reportType);
+ }
+
+ @Override
+ protected String getTitle() {
+ PackageTestResults model = getModel();
+
+ switch (reportType) {
+ case MULTI_PROJECT:
+ return model.getProject() + ": " + model.getFlavor() + ": " + model.getTitle();
+ case MULTI_FLAVOR:
+ return model.getFlavor() + ": " + model.getTitle();
+ }
+
+ return model.getTitle();
+ }
+
+ @Override protected void renderBreadcrumbs(Element parent) {
+ Element div = append(parent, "div");
+ div.setAttribute("class", "breadcrumbs");
+ appendLink(div, "index.html", "all");
+ appendText(div, String.format(" > %s", getResults().getName()));
+ }
+
+ private void renderClasses(Element parent) {
+ Element table = append(parent, "table");
+ Element thead = append(table, "thead");
+ Element tr = append(thead, "tr");
+ appendWithText(tr, "th", "Class");
+ appendWithText(tr, "th", "Tests");
+ appendWithText(tr, "th", "Failures");
+ appendWithText(tr, "th", "Duration");
+ appendWithText(tr, "th", "Success rate");
+ for (ClassTestResults testClass : getResults().getClasses()) {
+ tr = append(table, "tr");
+ Element td = append(tr, "td");
+ td.setAttribute("class", testClass.getStatusClass());
+ appendLink(td,
+ String.format("%s.html", testClass.getFilename(reportType)),
+ testClass.getSimpleName());
+ appendWithText(tr, "td", testClass.getTestCount());
+ appendWithText(tr, "td", testClass.getFailureCount());
+ appendWithText(tr, "td", testClass.getFormattedDuration());
+ td = appendWithText(tr, "td", testClass.getFormattedSuccessRate());
+ td.setAttribute("class", testClass.getStatusClass());
+ }
+ }
+
+ @Override protected void registerTabs() {
+ addFailuresTab();
+ addTab("Classes", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderClasses(element);
+ }
+ });
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackageTestResults.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackageTestResults.java
new file mode 100644
index 0000000..4e6e5db
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PackageTestResults.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Custom PackageTestResults based on Gradle's PackageTestResults
+ */
+class PackageTestResults extends CompositeTestResults {
+
+ private static final String DEFAULT_PACKAGE = "default-package";
+ private final String name;
+ private final Map<String, ClassTestResults> classes = new TreeMap<String, ClassTestResults>();
+
+ public PackageTestResults(String name, AllTestResults model,
+ String device, String project, String flavor) {
+ super(model, device, project, flavor);
+ this.name = name.length() == 0 ? DEFAULT_PACKAGE : name;
+ }
+
+ @Override
+ public String getTitle() {
+ return name.equals(DEFAULT_PACKAGE) ? "Default package" : String.format("Package %s", name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Collection<ClassTestResults> getClasses() {
+ return classes.values();
+ }
+
+ public TestResult addTest(String className, String testName, long duration,
+ String device, String project, String flavor) {
+ ClassTestResults classResults = addClass(className, device, project, flavor);
+ return addTest(classResults.addTest(testName, duration, device, project, flavor));
+ }
+
+
+ public ClassTestResults addClass(String className,
+ String device, String project, String flavor) {
+ String key = device + "/" + project + "/" + flavor + "/" + className;
+
+ ClassTestResults classResults = classes.get(key);
+ if (classResults == null) {
+ classResults = new ClassTestResults(className, this, device, project, flavor);
+ classes.put(key, classResults);
+ }
+ return classResults;
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PageRenderer.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PageRenderer.java
new file mode 100644
index 0000000..3fe3a1f
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/PageRenderer.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import org.gradle.api.Action;
+import org.gradle.api.internal.tasks.testing.junit.report.TestResultModel;
+import org.gradle.reporting.DomReportRenderer;
+import org.gradle.reporting.TabbedPageRenderer;
+import org.gradle.reporting.TabsRenderer;
+import org.w3c.dom.Element;
+
+/**
+ * Custom PageRenderer based on Gradle's PageRenderer
+ */
+abstract class PageRenderer<T extends CompositeTestResults> extends TabbedPageRenderer<T> {
+ private T results;
+ private final TabsRenderer<T> tabsRenderer = new TabsRenderer<T>();
+ protected final ReportType reportType;
+
+ PageRenderer(ReportType reportType) {
+ this.reportType = reportType;
+ }
+
+ protected T getResults() {
+ return results;
+ }
+
+ protected abstract void renderBreadcrumbs(Element parent);
+
+ protected abstract void registerTabs();
+
+ protected void addTab(String title, final Action<Element> contentRenderer) {
+ tabsRenderer.add(title, new DomReportRenderer<T>() {
+ @Override
+ public void render(T model, Element parent) {
+ contentRenderer.execute(parent);
+ }
+ });
+ }
+
+ protected void renderTabs(Element element) {
+ tabsRenderer.render(getModel(), element);
+ }
+
+ protected void addFailuresTab() {
+ if (!results.getFailures().isEmpty()) {
+ addTab("Failed tests", new Action<Element>() {
+ @Override
+ public void execute(Element element) {
+ renderFailures(element);
+ }
+ });
+ }
+ }
+
+ protected void renderFailures(Element parent) {
+ Element ul = append(parent, "ul");
+ ul.setAttribute("class", "linkList");
+
+ // TODO
+
+ for (TestResult test : results.getFailures()) {
+ Element li = append(ul, "li");
+ if (reportType == ReportType.MULTI_PROJECT) {
+ appendText(li, test.getProject());
+ appendText(li, ".");
+ appendText(li, test.getFlavor());
+ appendText(li, ".");
+ } else if (reportType == ReportType.MULTI_FLAVOR) {
+ appendText(li, test.getFlavor());
+ appendText(li, ".");
+ }
+ appendLink(li, String.format("%s.html", test.getClassResults().getFilename(reportType)),
+ test.getClassResults().getSimpleName());
+ appendText(li, ".");
+ appendLink(li,
+ String.format("%s.html#%s", test.getClassResults().getFilename(reportType),
+ test.getName()),
+ test.getName());
+ }
+ }
+
+ protected Element appendTableAndRow(Element parent) {
+ return append(append(parent, "table"), "tr");
+ }
+
+ protected Element appendCell(Element parent) {
+ return append(append(parent, "td"), "div");
+ }
+
+ protected <T extends TestResultModel> DomReportRenderer<T> withStatus(
+ final DomReportRenderer<T> renderer) {
+ return new DomReportRenderer<T>() {
+ @Override
+ public void render(T model, Element parent) {
+ parent.setAttribute("class", model.getStatusClass());
+ renderer.render(model, parent);
+ }
+ };
+ }
+
+ @Override
+ protected String getTitle() {
+ return getModel().getTitle();
+ }
+
+ @Override
+ protected String getPageTitle() {
+ return String.format("Test results - %s", getModel().getTitle());
+ }
+
+ @Override
+ protected DomReportRenderer<T> getHeaderRenderer() {
+ return new DomReportRenderer<T>() {
+ @Override
+ public void render(T model, Element content) {
+ PageRenderer.this.results = model;
+ renderBreadcrumbs(content);
+
+ // summary
+ Element summary = appendWithId(content, "div", "summary");
+ Element row = appendTableAndRow(summary);
+ Element group = appendCell(row);
+ group.setAttribute("class", "summaryGroup");
+ Element summaryRow = appendTableAndRow(group);
+
+ Element tests = appendCell(summaryRow);
+ tests.setAttribute("id", "tests");
+ tests.setAttribute("class", "infoBox");
+ Element div = appendWithText(tests, "div", results.getTestCount());
+ div.setAttribute("class", "counter");
+ appendWithText(tests, "p", "tests");
+
+ Element failures = appendCell(summaryRow);
+ failures.setAttribute("id", "failures");
+ failures.setAttribute("class", "infoBox");
+ div = appendWithText(failures, "div", results.getFailureCount());
+ div.setAttribute("class", "counter");
+ appendWithText(failures, "p", "failures");
+
+ Element duration = appendCell(summaryRow);
+ duration.setAttribute("id", "duration");
+ duration.setAttribute("class", "infoBox");
+ div = appendWithText(duration, "div", results.getFormattedDuration());
+ div.setAttribute("class", "counter");
+ appendWithText(duration, "p", "duration");
+
+ Element successRate = appendCell(row);
+ successRate.setAttribute("id", "successRate");
+ successRate.setAttribute("class",
+ String.format("infoBox %s", results.getStatusClass()));
+ div = appendWithText(successRate, "div", results.getFormattedSuccessRate());
+ div.setAttribute("class", "percent");
+ appendWithText(successRate, "p", "successful");
+ }
+ };
+ }
+
+ @Override
+ protected DomReportRenderer<T> getContentRenderer() {
+ return new DomReportRenderer<T>() {
+ @Override
+ public void render(T model, Element content) {
+ PageRenderer.this.results = model;
+ tabsRenderer.clear();
+ registerTabs();
+ renderTabs(content);
+ }
+ };
+ }
+}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestReport.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestReport.java
new file mode 100644
index 0000000..b5b715f
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestReport.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import org.gradle.api.GradleException;
+import org.gradle.api.internal.tasks.testing.junit.report.LocaleSafeDecimalFormat;
+import org.gradle.reporting.HtmlReportRenderer;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+
+/**
+ * Custom test reporter based on Gradle's DefaultTestReport
+ */
+public class TestReport {
+ private final HtmlReportRenderer htmlRenderer = new HtmlReportRenderer();
+ private final ReportType reportType;
+ private final File resultDir;
+ private final File reportDir;
+
+ public TestReport(ReportType reportType, File resultDir, File reportDir) {
+ this.reportType = reportType;
+ this.resultDir = resultDir;
+ this.reportDir = reportDir;
+ htmlRenderer.requireResource(getClass().getResource("report.js"));
+ htmlRenderer.requireResource(getClass().getResource("base-style.css"));
+ htmlRenderer.requireResource(getClass().getResource("style.css"));
+ }
+
+ public void generateReport() {
+ AllTestResults model = loadModel();
+ generateFiles(model);
+ }
+
+ private AllTestResults loadModel() {
+ AllTestResults model = new AllTestResults();
+ if (resultDir.exists()) {
+ for (File file : resultDir.listFiles()) {
+ if (file.getName().startsWith("TEST-") && file.getName().endsWith(".xml")) {
+ mergeFromFile(file, model);
+ }
+ }
+ }
+ return model;
+ }
+
+ private void mergeFromFile(File file, AllTestResults model) {
+ try {
+ InputStream inputStream = new FileInputStream(file);
+ Document document;
+ try {
+ document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
+ new InputSource(inputStream));
+ } finally {
+ inputStream.close();
+ }
+
+ String deviceName = null;
+ String projectName = null;
+ String flavorName = null;
+ NodeList propertiesList = document.getElementsByTagName("properties");
+ for (int i = 0; i < propertiesList.getLength(); i++) {
+ Element properties = (Element) propertiesList.item(i);
+ deviceName = properties.getAttribute("device");
+ projectName = properties.getAttribute("project");
+ flavorName = properties.getAttribute("flavor");
+ }
+
+ NodeList testCases = document.getElementsByTagName("testcase");
+ for (int i = 0; i < testCases.getLength(); i++) {
+ Element testCase = (Element) testCases.item(i);
+ String className = testCase.getAttribute("classname");
+ String testName = testCase.getAttribute("name");
+ LocaleSafeDecimalFormat format = new LocaleSafeDecimalFormat();
+ BigDecimal duration = format.parse(testCase.getAttribute("time"));
+ duration = duration.multiply(BigDecimal.valueOf(1000));
+ NodeList failures = testCase.getElementsByTagName("failure");
+ TestResult testResult = model.addTest(className, testName, duration.longValue(),
+ deviceName, projectName, flavorName);
+ for (int j = 0; j < failures.getLength(); j++) {
+ Element failure = (Element) failures.item(j);
+ testResult.addFailure(failure.getAttribute("message"),
+ failure.getTextContent());
+ }
+ }
+ NodeList ignoredTestCases = document.getElementsByTagName("ignored-testcase");
+ for (int i = 0; i < ignoredTestCases.getLength(); i++) {
+ Element testCase = (Element) ignoredTestCases.item(i);
+ String className = testCase.getAttribute("classname");
+ String testName = testCase.getAttribute("name");
+ model.addTest(className, testName, 0, deviceName, projectName, flavorName).ignored();
+ }
+ String suiteClassName = document.getDocumentElement().getAttribute("name");
+ ClassTestResults suiteResults = model.addTestClass(suiteClassName,
+ deviceName, projectName, flavorName);
+ NodeList stdOutElements = document.getElementsByTagName("system-out");
+ for (int i = 0; i < stdOutElements.getLength(); i++) {
+ suiteResults.addStandardOutput(stdOutElements.item(i).getTextContent());
+ }
+ NodeList stdErrElements = document.getElementsByTagName("system-err");
+ for (int i = 0; i < stdErrElements.getLength(); i++) {
+ suiteResults.addStandardError(stdErrElements.item(i).getTextContent());
+ }
+ } catch (Exception e) {
+ throw new GradleException(String.format("Could not load test results from '%s'.", file), e);
+ }
+ }
+
+ private void generateFiles(AllTestResults model) {
+ try {
+ generatePage(model, new OverviewPageRenderer(reportType), new File(reportDir, "index.html"));
+ for (PackageTestResults packageResults : model.getPackages()) {
+ generatePage(packageResults, new PackagePageRenderer(reportType),
+ new File(reportDir, packageResults.getFilename(reportType) + ".html"));
+ for (ClassTestResults classResults : packageResults.getClasses()) {
+ generatePage(classResults, new ClassPageRenderer(reportType),
+ new File(reportDir, classResults.getFilename(reportType) + ".html"));
+ }
+ }
+ } catch (Exception e) {
+ throw new GradleException(
+ String.format("Could not generate test report to '%s'.", reportDir), e);
+ }
+ }
+
+ private <T extends CompositeTestResults> void generatePage(T model, PageRenderer<T> renderer,
+ File outputFile) throws Exception {
+ htmlRenderer.renderer(renderer).writeTo(model, outputFile);
+ }}
diff --git a/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestResult.java b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestResult.java
new file mode 100644
index 0000000..63aa8a8
--- /dev/null
+++ b/gradle/src/fromGradle/groovy/com/android/build/gradle/internal/test/report/TestResult.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+import org.gradle.api.internal.tasks.testing.junit.report.TestFailure;
+import org.gradle.api.internal.tasks.testing.junit.report.TestResultModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.gradle.api.tasks.testing.TestResult.ResultType;
+
+/**
+ * Custom test result based on Gradle's TestResult
+ */
+class TestResult extends TestResultModel implements Comparable<TestResult> {
+
+ private final long duration;
+ private final String device;
+ private final String project;
+ private final String flavor;
+ final ClassTestResults classResults;
+ final List<TestFailure> failures = new ArrayList<TestFailure>();
+ final String name;
+ private boolean ignored;
+
+ public TestResult(String name, long duration, String device, String project, String flavor,
+ ClassTestResults classResults) {
+ this.name = name;
+ this.duration = duration;
+ this.device = device;
+ this.project = project;
+ this.flavor = flavor;
+ this.classResults = classResults;
+ }
+
+ public Object getId() {
+ return name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDevice() {
+ return device;
+ }
+
+ public String getProject() {
+ return project;
+ }
+
+ public String getFlavor() {
+ return flavor;
+ }
+
+ @Override
+ public String getTitle() {
+ return String.format("Test %s", name);
+ }
+
+ @Override
+ public ResultType getResultType() {
+ if (ignored) {
+ return ResultType.SKIPPED;
+ }
+ return failures.isEmpty() ? ResultType.SUCCESS : ResultType.FAILURE;
+ }
+
+ @Override
+ public long getDuration() {
+ return duration;
+ }
+
+ @Override
+ public String getFormattedDuration() {
+ return ignored ? "-" : super.getFormattedDuration();
+ }
+
+ public ClassTestResults getClassResults() {
+ return classResults;
+ }
+
+ public List<TestFailure> getFailures() {
+ return failures;
+ }
+
+ public void addFailure(String message, String stackTrace) {
+ classResults.failed(this);
+ failures.add(new TestFailure(message, stackTrace));
+ }
+
+ public void ignored() {
+ ignored = true;
+ }
+
+ @Override
+ public int compareTo(TestResult testResult) {
+ int diff = classResults.getName().compareTo(testResult.classResults.getName());
+ if (diff != 0) {
+ return diff;
+ }
+
+ diff = name.compareTo(testResult.name);
+ if (diff != 0) {
+ return diff;
+ }
+
+ diff = device.compareTo(testResult.device);
+ if (diff != 0) {
+ return diff;
+ }
+
+ diff = flavor.compareTo(testResult.flavor);
+ if (diff != 0) {
+ return diff;
+ }
+
+ Integer thisIdentity = System.identityHashCode(this);
+ int otherIdentity = System.identityHashCode(testResult);
+ return thisIdentity.compareTo(otherIdentity);
+ }
+}
diff --git a/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/base-style.css b/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/base-style.css
new file mode 100644
index 0000000..e09a387
--- /dev/null
+++ b/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/base-style.css
@@ -0,0 +1,162 @@
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: sans-serif;
+ font-size: 12pt;
+}
+
+body, a, a:visited {
+ color: #303030;
+}
+
+#content {
+ padding-left: 50px;
+ padding-right: 50px;
+ padding-top: 30px;
+ padding-bottom: 30px;
+}
+
+#content h1 {
+ font-size: 160%;
+ margin-bottom: 10px;
+}
+
+#footer {
+ margin-top: 100px;
+ font-size: 80%;
+ white-space: nowrap;
+}
+
+#footer, #footer a {
+ color: #a0a0a0;
+}
+
+ul {
+ margin-left: 0;
+}
+
+h1, h2, h3 {
+ white-space: nowrap;
+}
+
+h2 {
+ font-size: 120%;
+}
+
+ul.tabLinks {
+ padding-left: 0;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ overflow: auto;
+ min-width: 800px;
+ width: auto !important;
+ width: 800px;
+}
+
+ul.tabLinks li {
+ float: left;
+ height: 100%;
+ list-style: none;
+ padding-left: 10px;
+ padding-right: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ margin-bottom: 0;
+ -moz-border-radius: 7px;
+ border-radius: 7px;
+ margin-right: 25px;
+ border: solid 1px #d4d4d4;
+ background-color: #f0f0f0;
+ behavior: url(css3-pie-1.0beta3.htc);
+}
+
+ul.tabLinks li:hover {
+ background-color: #fafafa;
+}
+
+ul.tabLinks li.selected {
+ background-color: #c5f0f5;
+ border-color: #c5f0f5;
+}
+
+ul.tabLinks a {
+ font-size: 120%;
+ display: block;
+ outline: none;
+ text-decoration: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.tabLinks li h2 {
+ margin: 0;
+ padding: 0;
+}
+
+div.tab {
+}
+
+div.selected {
+ display: block;
+}
+
+div.deselected {
+ display: none;
+}
+
+div.tab table {
+ min-width: 350px;
+ width: auto !important;
+ width: 350px;
+ border-collapse: collapse;
+}
+
+div.tab th, div.tab table {
+ border-bottom: solid #d0d0d0 1px;
+}
+
+div.tab th {
+ text-align: left;
+ white-space: nowrap;
+ padding-left: 6em;
+}
+
+div.tab th:first-child {
+ padding-left: 0;
+}
+
+div.tab td {
+ white-space: nowrap;
+ padding-left: 6em;
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+div.tab td:first-child {
+ padding-left: 0;
+}
+
+div.tab td.numeric, div.tab th.numeric {
+ text-align: right;
+}
+
+span.code {
+ display: inline-block;
+ margin-top: 0em;
+ margin-bottom: 1em;
+}
+
+span.code pre {
+ font-size: 11pt;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-left: 10px;
+ padding-right: 10px;
+ margin: 0;
+ background-color: #f7f7f7;
+ border: solid 1px #d0d0d0;
+ min-width: 700px;
+ width: auto !important;
+ width: 700px;
+}
diff --git a/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/report.js b/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/report.js
new file mode 100644
index 0000000..a4455e4
--- /dev/null
+++ b/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/report.js
@@ -0,0 +1,101 @@
+var tabs = new Object();
+
+function initTabs() {
+ var container = document.getElementById('tabs');
+ tabs.tabs = findTabs(container);
+ tabs.titles = findTitles(tabs.tabs);
+ tabs.headers = findHeaders(container);
+ tabs.select = select;
+ tabs.deselectAll = deselectAll;
+ tabs.select(0);
+ return true;
+}
+
+window.onload = initTabs;
+
+function switchTab() {
+ var id = this.id.substr(1);
+ for (var i = 0; i < tabs.tabs.length; i++) {
+ if (tabs.tabs[i].id == id) {
+ tabs.select(i);
+ break;
+ }
+ }
+ return false;
+}
+
+function select(i) {
+ this.deselectAll();
+ changeElementClass(this.tabs[i], 'tab selected');
+ changeElementClass(this.headers[i], 'selected');
+ while (this.headers[i].firstChild) {
+ this.headers[i].removeChild(this.headers[i].firstChild);
+ }
+ var h2 = document.createElement('H2');
+ h2.appendChild(document.createTextNode(this.titles[i]));
+ this.headers[i].appendChild(h2);
+}
+
+function deselectAll() {
+ for (var i = 0; i < this.tabs.length; i++) {
+ changeElementClass(this.tabs[i], 'tab deselected');
+ changeElementClass(this.headers[i], 'deselected');
+ while (this.headers[i].firstChild) {
+ this.headers[i].removeChild(this.headers[i].firstChild);
+ }
+ var a = document.createElement('A');
+ a.setAttribute('id', 'ltab' + i);
+ a.setAttribute('href', '#tab' + i);
+ a.onclick = switchTab;
+ a.appendChild(document.createTextNode(this.titles[i]));
+ this.headers[i].appendChild(a);
+ }
+}
+
+function changeElementClass(element, classValue) {
+ if (element.getAttribute('className')) {
+ /* IE */
+ element.setAttribute('className', classValue)
+ } else {
+ element.setAttribute('class', classValue)
+ }
+}
+
+function findTabs(container) {
+ return findChildElements(container, 'DIV', 'tab');
+}
+
+function findHeaders(container) {
+ var owner = findChildElements(container, 'UL', 'tabLinks');
+ return findChildElements(owner[0], 'LI', null);
+}
+
+function findTitles(tabs) {
+ var titles = new Array();
+ for (var i = 0; i < tabs.length; i++) {
+ var tab = tabs[i];
+ var header = findChildElements(tab, 'H2', null)[0];
+ header.parentNode.removeChild(header);
+ if (header.innerText) {
+ titles.push(header.innerText)
+ } else {
+ titles.push(header.textContent)
+ }
+ }
+ return titles;
+}
+
+function findChildElements(container, name, targetClass) {
+ var elements = new Array();
+ var children = container.childNodes;
+ for (var i = 0; i < children.length; i++) {
+ var child = children.item(i);
+ if (child.nodeType == 1 && child.nodeName == name) {
+ if (targetClass && child.className.indexOf(targetClass) < 0) {
+ continue;
+ }
+ elements.push(child);
+ }
+ }
+ return elements;
+}
diff --git a/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/style.css b/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/style.css
new file mode 100644
index 0000000..2440a1f
--- /dev/null
+++ b/gradle/src/fromGradle/resources/com/android/build/gradle/internal/test/report/style.css
@@ -0,0 +1,81 @@
+
+#summary {
+ margin-top: 30px;
+ margin-bottom: 40px;
+}
+
+#summary table {
+ border-collapse: collapse;
+}
+
+#summary td {
+ vertical-align: top;
+}
+
+.breadcrumbs, .breadcrumbs a {
+ color: #606060;
+}
+
+.infoBox {
+ width: 110px;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ text-align: center;
+}
+
+.infoBox p {
+ margin: 0;
+}
+
+.counter, .percent {
+ font-size: 120%;
+ font-weight: bold;
+ margin-bottom: 8px;
+}
+
+#duration {
+ width: 125px;
+}
+
+#successRate, .summaryGroup {
+ border: solid 2px #d0d0d0;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ behavior: url(css3-pie-1.0beta3.htc);
+}
+
+#successRate {
+ width: 140px;
+ margin-left: 35px;
+}
+
+#successRate .percent {
+ font-size: 180%;
+}
+
+.success, .success a {
+ color: #008000;
+}
+
+div.success, #successRate.success {
+ background-color: #bbd9bb;
+ border-color: #008000;
+}
+
+.failures, .failures a {
+ color: #b60808;
+}
+
+div.failures, #successRate.failures {
+ background-color: #ecdada;
+ border-color: #b60808;
+}
+
+ul.linkList {
+ padding-left: 0;
+}
+
+ul.linkList li {
+ list-style: none;
+ margin-bottom: 5px;
+}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/AppPlugin.groovy b/gradle/src/main/groovy/com/android/build/gradle/AppPlugin.groovy
index 5b26a46..d788c97 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/AppPlugin.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/AppPlugin.groovy
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.build.gradle
import com.android.build.gradle.internal.BuildTypeData
@@ -25,7 +26,10 @@ import com.android.build.gradle.internal.dsl.BuildTypeFactory
import com.android.build.gradle.internal.dsl.GroupableProductFlavor
import com.android.build.gradle.internal.dsl.GroupableProductFlavorFactory
import com.android.build.gradle.internal.dsl.KeystoreFactory
+import com.android.build.gradle.internal.tasks.AndroidReportTask
+import com.android.build.gradle.internal.tasks.AndroidTestTask
import com.android.build.gradle.internal.test.PluginHolder
+import com.android.build.gradle.internal.test.report.ReportType
import com.android.builder.AndroidDependency
import com.android.builder.BuildType
import com.android.builder.JarDependency
@@ -37,6 +41,7 @@ import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.plugins.BasePlugin
+import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.internal.reflect.Instantiator
import javax.inject.Inject
@@ -52,6 +57,7 @@ class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradl
final Map<String, Keystore> keystores = [:]
AppExtension extension
+ AndroidReportTask testTask
@Inject
public AppPlugin(Instantiator instantiator) {
@@ -158,6 +164,25 @@ class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradl
assembleTest.group = BasePlugin.BUILD_GROUP
assembleTest.description = "Assembles all the Test applications"
+ // same for the test task
+ testTask = project.tasks.add("test", AndroidReportTask)
+ testTask.group = JavaBasePlugin.VERIFICATION_GROUP
+ testTask.description = "Installs and runs tests for all flavors"
+ testTask.reportType = ReportType.MULTI_FLAVOR
+
+ testTask.conventionMapping.resultsDir = {
+ String rootLocation = extension.testOptions.resultsDir != null ?
+ extension.testOptions.resultsDir : "$project.buildDir/test-results"
+
+ project.file("$rootLocation/all")
+ }
+ testTask.conventionMapping.reportsDir = {
+ String rootLocation = extension.testOptions.reportDir != null ?
+ extension.testOptions.reportDir : "$project.buildDir/reports/tests"
+
+ project.file("$rootLocation/all")
+ }
+
// check whether we have multi flavor builds
if (extension.flavorGroupList == null || extension.flavorGroupList.size() < 2) {
productFlavors.values().each { ProductFlavorData productFlavorData ->
@@ -186,6 +211,21 @@ class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradl
createTasksForMultiFlavoredBuilds(array, 0, map)
}
}
+
+ // If gradle is launched with --continue, we want to run all tests and generate an
+ // aggregate report (to help with the fact that we may have several build variants).
+ // To do that, the "test" task (which does the aggregation) must always run even if
+ // one of its dependent task (all the testFlavor tasks) fails, so we make them ignore their
+ // error.
+ // We cannot do that always: in case the test task is not going to run, we do want the
+ // individual testFlavor tasks to fail.
+ if (testTask != null && project.gradle.startParameter.continueOnFailure) {
+ project.gradle.taskGraph.whenReady { taskGraph ->
+ if (taskGraph.hasTask(testTask)) {
+ testTask.setWillRun()
+ }
+ }
+ }
}
private createTasksForMultiFlavoredBuilds(ProductFlavorData[] datas, int i,
@@ -279,7 +319,7 @@ class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradl
def testVariant = new TestAppVariant(testVariantConfig)
variants.add(testVariant)
- createTestTasks(testVariant, testedVariant, testConfigDependencies)
+ createTestTasks(testVariant, testedVariant, testConfigDependencies, true /*mainTestTask*/)
// add the test and tested variants to the list
DefaultBuildVariant testedBuildVariant = instantiator.newInstance(
@@ -394,7 +434,10 @@ class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradl
def testVariant = new TestAppVariant(testVariantConfig)
variants.add(testVariant)
- createTestTasks(testVariant, testedVariant, testConfigDependencies)
+ AndroidTestTask testFlavorTask = createTestTasks(testVariant, testedVariant,
+ testConfigDependencies, false /*mainTestTask*/)
+
+ testTask.addTask(testFlavorTask)
// add the test and tested variants to the list
DefaultBuildVariant testedBuildVariant = instantiator.newInstance(
diff --git a/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy b/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
index 3e331a1..bfa448d 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/BasePlugin.groovy
@@ -28,6 +28,7 @@ import com.android.build.gradle.internal.dependency.ManifestDependencyImpl
import com.android.build.gradle.internal.dependency.SymbolFileProviderImpl
import com.android.build.gradle.internal.tasks.AidlCompileTask
import com.android.build.gradle.internal.tasks.AndroidDependencyTask
+import com.android.build.gradle.internal.tasks.AndroidTestTask
import com.android.build.gradle.internal.tasks.DexTask
import com.android.build.gradle.internal.tasks.GenerateBuildConfigTask
import com.android.build.gradle.internal.tasks.IncrementalTask
@@ -39,7 +40,8 @@ import com.android.build.gradle.internal.tasks.PrepareLibraryTask
import com.android.build.gradle.internal.tasks.ProcessManifestTask
import com.android.build.gradle.internal.tasks.ProcessResourcesTask
import com.android.build.gradle.internal.tasks.ProcessTestManifestTask
-import com.android.build.gradle.internal.tasks.RunTestsTask
+import com.android.build.gradle.internal.tasks.TestFlavorTask
+import com.android.build.gradle.internal.tasks.TestLibraryTask
import com.android.build.gradle.internal.tasks.UninstallTask
import com.android.build.gradle.internal.tasks.ZipAlignTask
import com.android.builder.AndroidBuilder
@@ -525,8 +527,23 @@ public abstract class BasePlugin {
}
}
- protected void createTestTasks(TestAppVariant variant, ProductionAppVariant testedVariant,
- List<ConfigurationDependencies> configDependencies) {
+ /**
+ * Creates the test tasks, and return the main test[*] entry point.
+ *
+ * The main "test[*]" task can be created two different ways:
+ * mainTask is false: this creates the task for the given variant (with its variant name).
+ * mainTask is true: this creates the main "test" task, and makes check depend on it.
+ *
+ * @param variant the test variant
+ * @param testedVariant the tested variant
+ * @param configDependencies the list of config dependencies
+ * @param mainTestTask whether the main task is a main test task.
+ * @return the test task.
+ */
+ protected AndroidTestTask createTestTasks(TestAppVariant variant,
+ ProductionAppVariant testedVariant,
+ List<ConfigurationDependencies> configDependencies,
+ boolean mainTestTask) {
// The test app is signed with the same info as the tested app so there's no need
// to test both.
if (!testedVariant.isSigned()) {
@@ -570,37 +587,42 @@ public abstract class BasePlugin {
}
// create the check task for this test
- def runTestsTask = project.tasks.add("check${testedVariant.name}", RunTestsTask)
- runTestsTask.description = "Installs and runs the checks for Build ${testedVariant.name}."
- runTestsTask.group = JavaBasePlugin.VERIFICATION_GROUP
- runTestsTask.dependsOn testedVariant.assembleTask, variant.assembleTask
- project.tasks.check.dependsOn runTestsTask
-
- runTestsTask.plugin = this
- runTestsTask.variant = variant
- runTestsTask.testedVariant = testedVariant
- runTestsTask.sdkDir = sdkDir
-
- runTestsTask.conventionMapping.testApp = { variant.outputFile }
+ def testFlavorTask = project.tasks.add(mainTestTask ? "test" : "test${testedVariant.name}",
+ mainTestTask ? TestLibraryTask : TestFlavorTask)
+ testFlavorTask.description = "Installs and runs the tests for Build ${testedVariant.name}."
+ testFlavorTask.group = JavaBasePlugin.VERIFICATION_GROUP
+ testFlavorTask.dependsOn testedVariant.assembleTask, variant.assembleTask
+
+ if (mainTestTask) {
+ project.tasks.check.dependsOn testFlavorTask
+ }
+
+ testFlavorTask.plugin = this
+ testFlavorTask.variant = variant
+ testFlavorTask.testedVariant = testedVariant
+ testFlavorTask.sdkDir = sdkDir
+ testFlavorTask.flavorName = variant.flavorName
+
+ testFlavorTask.conventionMapping.testApp = { variant.outputFile }
if (testedVariant.config.type != VariantConfiguration.Type.LIBRARY) {
- runTestsTask.conventionMapping.testedApp = { testedVariant.outputFile }
+ testFlavorTask.conventionMapping.testedApp = { testedVariant.outputFile }
}
- runTestsTask.conventionMapping.resultsDir = {
- String location = extension.testOptions.resultsDir != null ?
- extension.testOptions.resultsDir :
- "$project.buildDir/test-results/$variant.flavorDirName"
+ testFlavorTask.conventionMapping.resultsDir = {
+ String rootLocation = extension.testOptions.resultsDir != null ?
+ extension.testOptions.resultsDir : "$project.buildDir/test-results"
- project.file(location)
+ project.file("$rootLocation/flavors/$variant.flavorDirName")
}
- runTestsTask.conventionMapping.reportsDir = {
- String location = extension.testOptions.reportDir != null ?
- extension.testOptions.reportDir :
- "$project.buildDir/reports/tests/$variant.flavorDirName"
+ testFlavorTask.conventionMapping.reportsDir = {
+ String rootLocation = extension.testOptions.reportDir != null ?
+ extension.testOptions.reportDir : "$project.buildDir/reports/tests"
- project.file(location)
+ project.file("$rootLocation/flavors/$variant.flavorDirName")
}
- variant.runTestsTask = runTestsTask
+ variant.testFlavorTask = testFlavorTask
+
+ return testFlavorTask
}
/**
diff --git a/gradle/src/main/groovy/com/android/build/gradle/BuildVariant.groovy b/gradle/src/main/groovy/com/android/build/gradle/BuildVariant.groovy
index 046a8de..7c9a556 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/BuildVariant.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/BuildVariant.groovy
@@ -212,5 +212,5 @@ public interface BuildVariant {
* Only valid for test project.
*/
@Nullable
- Task getRunTests()
+ Task getTestFlavor()
}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/LibraryPlugin.groovy b/gradle/src/main/groovy/com/android/build/gradle/LibraryPlugin.groovy
index 6612bcd..351af77 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/LibraryPlugin.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/LibraryPlugin.groovy
@@ -258,7 +258,7 @@ public class LibraryPlugin extends BasePlugin implements Plugin<Project> {
def testVariant = new TestAppVariant(testVariantConfig,)
variants.add(testVariant)
- createTestTasks(testVariant, testedVariant, configDependencies)
+ createTestTasks(testVariant, testedVariant, configDependencies, true /*mainTestTask*/)
return testVariant
}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy b/gradle/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy
new file mode 100644
index 0000000..882ea1b
--- /dev/null
+++ b/gradle/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 com.android.build.gradle
+import com.android.build.gradle.internal.tasks.AndroidReportTask
+import com.android.build.gradle.internal.tasks.TestLibraryTask
+import com.android.build.gradle.internal.test.TestOptions
+import com.android.build.gradle.internal.test.report.ReportType
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaBasePlugin
+import org.gradle.api.tasks.TaskCollection
+/**
+ * Gradle plugin class for 'reporting' projects.
+ *
+ * This is mostly used to aggregate reports from subprojects.
+ *
+ */
+class ReportingPlugin implements org.gradle.api.Plugin<Project> {
+
+ private TestOptions extension
+
+ @Override
+ void apply(Project project) {
+ // make sure this project depends on the evaluation of all sub projects so that
+ // it's evaluated last.
+ project.evaluationDependsOnChildren()
+
+ extension = project.extensions.create('android', TestOptions)
+
+ AndroidReportTask testTask = project.tasks.add("test", AndroidReportTask)
+ testTask.group = JavaBasePlugin.VERIFICATION_GROUP
+ testTask.description = "Installs and runs tests for all flavors, and aggregate the results"
+ testTask.reportType = ReportType.MULTI_PROJECT
+
+ testTask.conventionMapping.resultsDir = {
+ String location = extension.resultsDir != null ?
+ extension.resultsDir : "$project.buildDir/test-results"
+
+ project.file(location)
+ }
+ testTask.conventionMapping.reportsDir = {
+ String location = extension.reportDir != null ?
+ extension.reportDir : "$project.buildDir/reports/tests"
+
+ project.file(location)
+ }
+
+ // TODO: deal with existing/missing test/check tasks.
+// project.tasks.check.dependsOn testTask
+
+ // gather the subprojects
+ project.afterEvaluate {
+ project.subprojects.each { p ->
+ TaskCollection<AndroidReportTask> tasks = p.tasks.withType(AndroidReportTask)
+ for (AndroidReportTask task : tasks) {
+ testTask.addTask(task)
+ }
+ TaskCollection<TestLibraryTask> tasks2= p.tasks.withType(TestLibraryTask)
+ for (TestLibraryTask task : tasks2) {
+ testTask.addTask(task)
+ }
+ }
+ }
+
+ // If gradle is launched with --continue, we want to run all tests and generate an
+ // aggregate report (to help with the fact that we may have several build variants).
+ // To do that, the "test" task (which does the aggregation) must always run even if
+ // one of its dependent task (all the testFlavor tasks) fails, so we make them ignore their
+ // error.
+ // We cannot do that always: in case the test task is not going to run, we do want the
+ // individual testFlavor tasks to fail.
+ if (testTask != null && project.gradle.startParameter.continueOnFailure) {
+ project.gradle.taskGraph.whenReady { taskGraph ->
+ if (taskGraph.hasTask(testTask)) {
+ testTask.setWillRun()
+ }
+ }
+ }
+ }
+}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/ApplicationVariant.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/ApplicationVariant.groovy
index 87b2abb..7bb3c87 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/ApplicationVariant.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/ApplicationVariant.groovy
@@ -23,7 +23,7 @@ import com.android.build.gradle.internal.tasks.MergeResourcesTask
import com.android.build.gradle.internal.tasks.PackageApplicationTask
import com.android.build.gradle.internal.tasks.PrepareDependenciesTask
import com.android.build.gradle.internal.tasks.ProcessResourcesTask
-import com.android.build.gradle.internal.tasks.RunTestsTask
+import com.android.build.gradle.internal.tasks.TestFlavorTask
import com.android.build.gradle.internal.tasks.ZipAlignTask
import com.android.build.gradle.tasks.ProcessManifest
import com.android.builder.AndroidBuilder
@@ -65,7 +65,7 @@ public abstract class ApplicationVariant {
Task installTask
Task uninstallTask
- RunTestsTask runTestsTask
+ TestFlavorTask testFlavorTask
ApplicationVariant(VariantConfiguration config) {
this.config = config
@@ -89,6 +89,14 @@ public abstract class ApplicationVariant {
}
}
+ String getFlavorName() {
+ if (config.hasFlavors()) {
+ return "${getFlavoredName(true)}"
+ } else {
+ return "Main"
+ }
+ }
+
abstract String getBaseName()
abstract boolean getZipAlign()
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultBuildVariant.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultBuildVariant.groovy
index c1e46ab..9ccae0f 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultBuildVariant.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/DefaultBuildVariant.groovy
@@ -163,8 +163,8 @@ public class DefaultBuildVariant implements BuildVariant {
}
@Override
- Task getRunTests() {
- return variant.runTestsTask
+ Task getTestFlavor() {
+ return variant.testFlavorTask
}
@Override
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.groovy
new file mode 100644
index 0000000..6e0cd31
--- /dev/null
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.groovy
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 com.android.build.gradle.internal.tasks
+
+import com.android.build.gradle.internal.test.report.ReportType
+import com.android.build.gradle.internal.test.report.TestReport
+import com.google.common.collect.Lists
+import com.google.common.io.Files
+import org.gradle.api.GradleException
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import org.gradle.logging.ConsoleRenderer
+/**
+ * Task doing test report aggregation.
+ */
+class AndroidReportTask extends BaseTask implements AndroidTestTask {
+
+ private final List<AndroidTestTask> subTasks = Lists.newArrayList()
+
+ ReportType reportType
+
+ boolean ignoreFailures
+ boolean testFailed
+
+ @OutputDirectory
+ File reportsDir
+
+ @OutputDirectory
+ File resultsDir
+
+ public void addTask(AndroidTestTask task) {
+ subTasks.add(task)
+ dependsOn task
+ }
+
+ @InputFiles
+ List<File> getResultInputs() {
+ List<File> list = Lists.newArrayList()
+
+ for (AndroidTestTask task : subTasks) {
+ list.add(task.getResultsDir())
+ }
+
+ return list
+ }
+
+ public void setWillRun() {
+ for (AndroidTestTask task : subTasks) {
+ task.ignoreFailures = true
+ }
+ }
+
+ @TaskAction
+ protected void createReport() {
+ File resultsOutDir = getResultsDir()
+ File reportOutDir = getReportsDir()
+
+ // empty the folders
+ emptyFolder(resultsOutDir)
+ emptyFolder(reportOutDir)
+
+ // do the copy.
+ copyResults(resultsOutDir)
+
+ // create the report.
+ TestReport report = new TestReport(reportType, resultsOutDir, reportOutDir)
+ report.generateReport()
+
+ // fail if any of the tasks failed.
+ for (AndroidTestTask task : subTasks) {
+ if (task.testFailed) {
+
+ String reportUrl = new ConsoleRenderer().asClickableFileUrl(
+ new File(reportOutDir, "index.html"))
+ String message = "There were failing tests. See the report at: " + reportUrl
+
+ if (getIgnoreFailures()) {
+ getLogger().warn(message)
+ } else {
+ throw new GradleException(message)
+ }
+
+ break
+ }
+ }
+ }
+
+ private void copyResults(File reportOutDir) {
+ List<File> inputs = getResultInputs()
+
+ for (File input : inputs) {
+ File[] children = input.listFiles()
+ if (children != null) {
+ for (File child : children) {
+ copyFile(child, reportOutDir)
+ }
+ }
+ }
+ }
+
+ private void copyFile(File from, File to) {
+ to = new File(to, from.getName())
+ if (from.isDirectory()) {
+ if (!to.exists()) {
+ to.mkdirs()
+ }
+
+ File[] children = from.listFiles()
+ if (children != null) {
+ for (File child : children) {
+ copyFile(child, to)
+ }
+ }
+ } else if (from.isFile()) {
+ Files.copy(from, to)
+ }
+ }
+}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidTestTask.java b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidTestTask.java
new file mode 100644
index 0000000..620bf73
--- /dev/null
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidTestTask.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 com.android.build.gradle.internal.tasks;
+
+import org.gradle.api.tasks.VerificationTask;
+
+import java.io.File;
+
+/**
+ * Base interface for test classes that integrate with other test classes for reporting
+ * reasons.
+ */
+public interface AndroidTestTask extends VerificationTask {
+
+ File getResultsDir();
+
+ boolean getTestFailed();
+}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestFlavorTask.groovy
index af19c7e..bcc8930 100644
--- a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/RunTestsTask.groovy
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestFlavorTask.groovy
@@ -14,30 +14,33 @@
* limitations under the License.
*/
package com.android.build.gradle.internal.tasks
-
import com.android.SdkConstants
import com.android.annotations.NonNull
import com.android.annotations.Nullable
import com.android.build.gradle.internal.ApplicationVariant
+import com.android.build.gradle.internal.test.report.ReportType
+import com.android.build.gradle.internal.test.report.TestReport
import com.android.builder.internal.util.concurrent.WaitableExecutor
import com.android.builder.testing.CustomTestRunListener
import com.android.ddmlib.AndroidDebugBridge
import com.android.ddmlib.IDevice
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner
import com.android.utils.ILogger
-import org.gradle.api.internal.tasks.testing.junit.report.DefaultTestReport
+import org.gradle.api.GradleException
+import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
+import org.gradle.logging.ConsoleRenderer
import org.gradle.tooling.BuildException
import java.util.concurrent.Callable
/**
* Run tests for a given variant
*/
-public class RunTestsTask extends BaseTask {
+public class TestFlavorTask extends BaseTask implements AndroidTestTask {
@Input
File sdkDir
@@ -54,8 +57,14 @@ public class RunTestsTask extends BaseTask {
@OutputDirectory
File resultsDir
+ @Input
+ String flavorName
+
ApplicationVariant testedVariant
+ boolean ignoreFailures
+ boolean testFailed
+
/**
* Callable to run tests on a given device.
*/
@@ -63,19 +72,24 @@ public class RunTestsTask extends BaseTask {
private final IDevice mDevice
private final String mDeviceName
+ private final String mFlavorName
private final File mResultsDir
private final File mTestApk
private final ApplicationVariant mVariant
private final File mTestedApk
private final ApplicationVariant mTestedVariant
private final ILogger mLogger
+ private final Project mProject
- DeviceTestRunner(@NonNull IDevice device,
+ DeviceTestRunner(@NonNull IDevice device, @NonNull Project project,
+ @NonNull String flavorName,
@NonNull File testApk, @NonNull ApplicationVariant variant,
@Nullable File testedApk, @NonNull ApplicationVariant testedVariant,
@NonNull File resultsDir, @NonNull ILogger logger) {
+ mProject = project
mDevice = device
mDeviceName = computeDeviceName(device)
+ mFlavorName = flavorName
mResultsDir = resultsDir
mTestApk = testApk
mVariant = variant
@@ -102,7 +116,7 @@ public class RunTestsTask extends BaseTask {
runner.setRunName(mDevice.serialNumber)
CustomTestRunListener runListener = new CustomTestRunListener(
- mDeviceName, mLogger)
+ mDeviceName, mProject.name, mFlavorName, mLogger)
runListener.setReportDir(mResultsDir)
runner.run(runListener)
@@ -174,8 +188,10 @@ public class RunTestsTask extends BaseTask {
File testApk = getTestApp()
File testedApk = getTestedApp()
+ String flavor = getFlavorName()
+
for (IDevice device : devices) {
- executor.execute(new DeviceTestRunner(device,
+ executor.execute(new DeviceTestRunner(device, project, flavor,
testApk, variant,
testedApk, testedVariant,
resultsOutDir, plugin.logger))
@@ -187,16 +203,26 @@ public class RunTestsTask extends BaseTask {
File reportOutDir = getReportsDir()
emptyFolder(reportOutDir)
- DefaultTestReport report = new DefaultTestReport(
- testReportDir: reportOutDir, testResultsDir: resultsOutDir)
+ TestReport report = new TestReport(ReportType.SINGLE_FLAVOR, resultsOutDir, reportOutDir)
report.generateReport()
// check if one test failed.
for (Boolean b : results) {
if (b.booleanValue()) {
- throw new BuildException(
- "Failed tests\n\tCheck report at ${reportOutDir.absolutePath}", null)
+ testFailed = true
+ String reportUrl = new ConsoleRenderer().asClickableFileUrl(
+ new File(reportOutDir, "index.html"));
+ String message = "There were failing tests. See the report at: " + reportUrl;
+ if (getIgnoreFailures()) {
+ getLogger().warn(message)
+
+ return
+ } else {
+ throw new GradleException(message)
+ }
}
}
+
+ testFailed = false
}
}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestLibraryTask.groovy b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestLibraryTask.groovy
new file mode 100644
index 0000000..9a0d583
--- /dev/null
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/tasks/TestLibraryTask.groovy
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 com.android.build.gradle.internal.tasks
+
+/**
+ * class to test library project. Exactly the same as TestFlavorTask but is needed to be gathered
+ * by the reporting plugin.
+ */
+class TestLibraryTask extends TestFlavorTask {
+}
diff --git a/gradle/src/main/groovy/com/android/build/gradle/internal/test/report/ReportType.java b/gradle/src/main/groovy/com/android/build/gradle/internal/test/report/ReportType.java
new file mode 100644
index 0000000..df983c6
--- /dev/null
+++ b/gradle/src/main/groovy/com/android/build/gradle/internal/test/report/ReportType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 com.android.build.gradle.internal.test.report;
+
+/**
+ * Types of report to control aggregation and display
+ */
+public enum ReportType {
+ /**
+ * Report that only shows a single flavor
+ */
+ SINGLE_FLAVOR,
+ /**
+ * Report that shows one or more flavors
+ */
+ MULTI_FLAVOR,
+ /**
+ * Report that shows multiple projects.
+ */
+ MULTI_PROJECT
+}
diff --git a/gradle/src/main/resources/META-INF/gradle-plugins/android-reporting.properties b/gradle/src/main/resources/META-INF/gradle-plugins/android-reporting.properties
new file mode 100644
index 0000000..38b4622
--- /dev/null
+++ b/gradle/src/main/resources/META-INF/gradle-plugins/android-reporting.properties
@@ -0,0 +1 @@
+implementation-class=com.android.build.gradle.ReportingPlugin \ No newline at end of file
diff --git a/gradle/src/test/groovy/com/android/build/gradle/AppPluginDslTest.groovy b/gradle/src/test/groovy/com/android/build/gradle/AppPluginDslTest.groovy
index c9fe6b3..4e6a74e 100644
--- a/gradle/src/test/groovy/com/android/build/gradle/AppPluginDslTest.groovy
+++ b/gradle/src/test/groovy/com/android/build/gradle/AppPluginDslTest.groovy
@@ -232,9 +232,9 @@ public class AppPluginDslTest extends BaseTest {
}
if (testVariant) {
- assertNotNull(variant.runTests)
+ assertNotNull(variant.testFlavor)
} else {
- assertNull(variant.runTests)
+ assertNull(variant.testFlavor)
}
}
diff --git a/gradle/src/test/groovy/com/android/build/gradle/LibraryPluginDslTest.groovy b/gradle/src/test/groovy/com/android/build/gradle/LibraryPluginDslTest.groovy
index cdc184d..9e4a57e 100644
--- a/gradle/src/test/groovy/com/android/build/gradle/LibraryPluginDslTest.groovy
+++ b/gradle/src/test/groovy/com/android/build/gradle/LibraryPluginDslTest.groovy
@@ -89,7 +89,7 @@ public class LibraryPluginDslTest extends BaseTest {
assertNull(variant.install)
}
- assertNotNull(variant.runTests)
+ assertNotNull(variant.testFlavor)
}
private static void checkLibraryTasks(BuildVariant variant) {
@@ -107,7 +107,7 @@ public class LibraryPluginDslTest extends BaseTest {
assertNull(variant.zipAlign)
assertNull(variant.install)
assertNull(variant.uninstall)
- assertNull(variant.runTests)
+ assertNull(variant.testFlavor)
}
private static BuildVariant findVariant(Collection<BuildVariant> variants, String name) {
diff --git a/tests/flavorlib/build.gradle b/tests/flavorlib/build.gradle
index a0832c6..8090336 100644
--- a/tests/flavorlib/build.gradle
+++ b/tests/flavorlib/build.gradle
@@ -6,3 +6,5 @@ buildscript {
classpath 'com.android.tools.build:gradle:0.3-SNAPSHOT'
}
}
+
+apply plugin: 'android-reporting' \ No newline at end of file
diff --git a/tests/flavorlibWithFailedTests/build.gradle b/tests/flavorlibWithFailedTests/build.gradle
index a0832c6..8090336 100644
--- a/tests/flavorlibWithFailedTests/build.gradle
+++ b/tests/flavorlibWithFailedTests/build.gradle
@@ -6,3 +6,5 @@ buildscript {
classpath 'com.android.tools.build:gradle:0.3-SNAPSHOT'
}
}
+
+apply plugin: 'android-reporting' \ No newline at end of file