diff options
author | Xavier Ducrohet <xav@android.com> | 2013-01-04 14:58:14 -0800 |
---|---|---|
committer | Xavier Ducrohet <xav@android.com> | 2013-01-10 18:16:40 -0800 |
commit | 06dc485552957e6b0492e269e2fa0347790498e9 (patch) | |
tree | 990d72fbe227d42a425c1913fd93fdc1ab90d467 | |
parent | 1ac02d56be392643eb31a855fb418cfa926b63f8 (diff) | |
download | build-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
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 |