Bug 1473313 - Part 1: Set up geckoview build config for androidTest coverage runs. r=nalexander draft
authorTudor-Gabriel Vîjială <tvijiala@mozilla.com>
Tue, 24 Jul 2018 11:44:24 +0100
changeset 825868 9edc37913a3929ad045270c601c77791d122e363
parent 825800 bd79b07f57a34e0d8fe01bdc3f34815d77c01444
child 825869 b2e4f3c1c4d2313c3108ecdedb86bfbad1f02fb7
push id118194
push userbmo:tvijiala@mozilla.com
push dateThu, 02 Aug 2018 15:08:25 +0000
reviewersnalexander
bugs1473313
milestone63.0a1
Bug 1473313 - Part 1: Set up geckoview build config for androidTest coverage runs. r=nalexander This patch adds JaCoCo as a dependency for the geckoview androidTest configurations, as well as the `mach android archive-geckoview-coverage-artifacts` command, and the `--enable-java-coverage` mozconfig flag. MozReview-Commit-ID: 36jNAzK44g3
build.gradle
build/moz.configure/java.configure
mobile/android/config/mozconfigs/android-api-16-gradle-dependencies/nightly
mobile/android/config/proguard/proguard.cfg
mobile/android/geckoview/build.gradle
mobile/android/gradle.configure
mobile/android/gradle/jacoco_dependencies.gradle
mobile/android/mach_commands.py
python/mozbuild/mozbuild/test/configure/test_checks_configure.py
--- a/build.gradle
+++ b/build.gradle
@@ -47,16 +47,17 @@ buildscript {
         // For in tree plugins.
         maven {
             url "file://${gradle.mozconfig.topsrcdir}/mobile/android/gradle/m2repo"
         }
     }
 
     ext.kotlin_version = '1.2.41'
     ext.support_library_version = '26.1.0'
+    ext.jacoco_version = '0.8.1'
 
     if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
         ext.google_play_services_version = '15.0.1'
     }
 
     dependencies {
         classpath 'com.android.tools.build:gradle:3.1.0'
         classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
--- a/build/moz.configure/java.configure
+++ b/build/moz.configure/java.configure
@@ -61,8 +61,15 @@ def javac_version(javac):
                                          stderr=subprocess.STDOUT).rstrip()
         version = Version(output.split(' ')[-1])
         if version < '1.8':
             die('javac 1.8 or higher is required (found %s). '
                 'Check the JAVA_HOME environment variable.' % version)
         return version
     except subprocess.CalledProcessError as e:
         die('Failed to get javac version: %s', e.output)
+
+
+# Java Code Coverage
+# ========================================================
+option('--enable-java-coverage', env='MOZ_JAVA_CODE_COVERAGE', help='Enable Java code coverage')
+
+set_config('MOZ_JAVA_CODE_COVERAGE', depends('--enable-java-coverage')(lambda v: bool(v)))
--- a/mobile/android/config/mozconfigs/android-api-16-gradle-dependencies/nightly
+++ b/mobile/android/config/mozconfigs/android-api-16-gradle-dependencies/nightly
@@ -29,16 +29,19 @@ ac_add_options --disable-tests
 # advertise a bad API level. This may confuse people. As an example, please look at bug 1384482.
 # If you think you can't handle the whole set of changes, please reach out to the Release
 # Engineering team.
 ac_add_options --with-android-min-sdk=16
 ac_add_options --target=arm-linux-androideabi
 
 ac_add_options --with-branding=mobile/android/branding/nightly
 
+# Pull code coverage dependencies too.
+ac_add_options --enable-java-coverage
+
 export MOZILLA_OFFICIAL=1
 export MOZ_TELEMETRY_REPORTING=1
 export MOZ_ANDROID_MMA=1
 export MOZ_ANDROID_POCKET=1
 
 . "$topsrcdir/mobile/android/config/mozconfigs/common.override"
 
 # End ../android-api-16-frontend/nightly.
--- a/mobile/android/config/proguard/proguard.cfg
+++ b/mobile/android/config/proguard/proguard.cfg
@@ -160,16 +160,21 @@
 #-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
 
 -include "play-services-keeps.cfg"
 
 # Don't print spurious warnings from the support library.
 # See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl
 -dontnote android.support.**
 
+# Don't warn when classes referenced by JaCoCo are missing when running the build from android-dependencies.
+-dontwarn java.lang.instrument.**
+-dontwarn java.lang.management.**
+-dontwarn javax.management.**
+
 -include "adjust-keeps.cfg"
 
 -include "leakcanary-keeps.cfg"
 
 -include "appcompat-v7-keeps.cfg"
 
 -include "proguard-android.cfg"
 
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -367,8 +367,76 @@ task("generateSDKBindings", type: JavaEx
 
     dependsOn project(':annotations').jar
 }
 
 apply from: "${topsrcdir}/mobile/android/gradle/jacoco_dependencies.gradle"
 if (project.hasProperty('enable_code_coverage')) {
     apply from: "${topsrcdir}/mobile/android/gradle/jacoco_for_junit.gradle"
 }
+
+// Set up code coverage for tests on emulators.
+if (mozconfig.substs.MOZ_JAVA_CODE_COVERAGE) {
+    apply plugin: "jacoco"
+    jacoco {
+        toolVersion = "${project.jacoco_version}"
+    }
+
+    android {
+        jacoco {
+            version = "$jacoco_version"
+        }
+        buildTypes {
+            debug {
+                testCoverageEnabled true
+            }
+        }
+    }
+
+    configurations {
+        // This configuration is used for dependencies that are not needed at compilation or
+        // runtime, but need to be exported as artifacts of the build for usage on the testing
+        // machines.
+        coverageDependency
+    }
+
+    dependencies {
+        // This is required both in the instrumented application classes and the test classes,
+        // so `api` has to be used instead of `androidTestImplementation`.
+        api "org.jacoco:org.jacoco.agent:$jacoco_version:runtime"
+
+        coverageDependency ("org.jacoco:org.jacoco.cli:$jacoco_version:nodeps") {
+            exclude group: 'org.ow2.asm', module: '*'
+        }
+    }
+
+    // This task is used by `mach android archive-geckoview-coverage-artifacts`.
+    task copyCoverageDependencies(type: Copy) {
+        from(configurations.coverageDependency) {
+            include 'org.jacoco.cli-*-nodeps.jar'
+            rename { _ -> 'target.jacoco-cli.jar' }
+        }
+        into "$buildDir/coverage"
+    }
+
+    // Generate tasks to archive compiled classfiles for later use with JaCoCo report generation.
+    // One of these tasks is used by `mach android archive-geckoview-coverage-artifacts`.
+    android.libraryVariants.all { variant ->
+        def name = variant.name
+        def compileTask = tasks.getByName("compile${name.capitalize()}JavaWithJavac")
+        task "archiveClassfiles${name.capitalize()}"(type: Zip, dependsOn: compileTask) {
+            description = "Archive compiled classfiles for $name in order to export them as code coverage artifacts."
+            def fileFilter = ['**/androidTest/**',
+                              '**/test/**',
+                              '**/R.class',
+                              '**/R$*.class',
+                              '**/BuildConfig.*',
+                              '**/Manifest*.*',
+                              '**/*Test*.*',
+                              'android/**/*.*']
+            from fileTree(dir: compileTask.destinationDir, excludes: fileFilter)
+            destinationDir = file("${buildDir}/coverage")
+            // Note: This task assumes only one variant of archiveClassfiles* will be used.
+            // Running multiple variants of this task will overwrite the output archive.
+            archiveName = 'target.geckoview_classfiles.zip'
+        }
+    }
+}
--- a/mobile/android/gradle.configure
+++ b/mobile/android/gradle.configure
@@ -232,26 +232,39 @@ def gradle_android_archive_geckoview_tas
         'geckoview_example:assemble{geckoview_example.variant.name}'.format(geckoview_example=build_config.geckoview_example),
         'geckoview_example:assemble{geckoview_example.variant.name}AndroidTest'.format(geckoview_example=build_config.geckoview_example),
         'geckoview:uploadArchives',
     ]
 
 set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS', gradle_android_archive_geckoview_tasks)
 
 
+@depends(gradle_android_build_config)
+def gradle_android_archive_geckoview_coverage_artifacts_tasks(build_config):
+    '''Gradle tasks run by |mach android archive-geckoview-coverage-artifacts|.'''
+    return [
+        'geckoview:archiveClassfiles{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
+        'geckoview:copyCoverageDependencies',
+    ]
+
+set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_COVERAGE_ARTIFACTS_TASKS',
+           gradle_android_archive_geckoview_coverage_artifacts_tasks)
+
+
 @depends(
     gradle_android_app_tasks,
     gradle_android_test_tasks,
     gradle_android_lint_tasks,
     gradle_android_checkstyle_tasks,
     gradle_android_findbugs_tasks,
     gradle_android_archive_geckoview_tasks,
     gradle_android_generate_sdk_bindings_tasks,
     gradle_android_generate_generated_jni_wrappers_tasks,
     gradle_android_generate_fennec_jni_wrappers_tasks,
+    gradle_android_archive_geckoview_coverage_artifacts_tasks,
 )
 @imports(_from='itertools', _import='imap')
 @imports(_from='itertools', _import='chain')
 @imports(_from='itertools', _import='ifilterfalse')
 def gradle_android_dependencies_tasks(*tasks):
     '''Gradle tasks run by |mach android dependencies|.'''
     # The union, plus a bit more, of all of the Gradle tasks
     # invoked by the android-* automation jobs.
--- a/mobile/android/gradle/jacoco_dependencies.gradle
+++ b/mobile/android/gradle/jacoco_dependencies.gradle
@@ -1,13 +1,11 @@
 /* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-project.ext.jacoco_version = "0.7.8"
-
 dependencies {
-    testImplementation "org.jacoco:org.jacoco.agent:${project.jacoco_version}"
-    testImplementation "org.jacoco:org.jacoco.ant:${project.jacoco_version}"
-    testImplementation "org.jacoco:org.jacoco.core:${project.jacoco_version}"
-    testImplementation "org.jacoco:org.jacoco.report:${project.jacoco_version}"
+    testImplementation "org.jacoco:org.jacoco.agent:$jacoco_version"
+    testImplementation "org.jacoco:org.jacoco.ant:$jacoco_version"
+    testImplementation "org.jacoco:org.jacoco.core:$jacoco_version"
+    testImplementation "org.jacoco:org.jacoco.report:$jacoco_version"
 }
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -408,16 +408,25 @@ class MachCommands(MachCommandBase):
         # We don't want to gate producing dependency archives on clean
         # lint or checkstyle, particularly because toolchain versions
         # can change the outputs for those processes.
         self.gradle(self.substs['GRADLE_ANDROID_DEPENDENCIES_TASKS'] +
                     ["--continue"] + args, verbose=True)
 
         return 0
 
+    @SubCommand('android', 'archive-geckoview-coverage-artifacts',
+                """Archive compiled geckoview classfiles to be used later in generating code coverage reports.""")  # NOQA: E501
+    @CommandArgument('args', nargs=argparse.REMAINDER)
+    def android_archive_geckoview_classfiles(self, args):
+        self.gradle(self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_COVERAGE_ARTIFACTS_TASKS'] +
+                    ["--continue"] + args, verbose=True)
+
+        return 0
+
     @SubCommand('android', 'archive-geckoview',
                 """Create GeckoView archives.
         See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""")  # NOQA: E501
     @CommandArgument('args', nargs=argparse.REMAINDER)
     def android_archive_geckoview(self, args):
         ret = self.gradle(
             self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS'] + ["--continue"] + args,
             verbose=True)
--- a/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
@@ -507,16 +507,17 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': java,
             'JAVAH': javah,
             'JAVAC': javac,
             'JAR': jar,
             'JARSIGNER': jarsigner,
             'KEYTOOL': keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
@@ -550,16 +551,17 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': alt_java,
             'JAVAH': alt_javah,
             'JAVAC': alt_javac,
             'JAR': alt_jar,
             'JARSIGNER': alt_jarsigner,
             'KEYTOOL': alt_keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
@@ -579,16 +581,17 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': alt_java,
             'JAVAH': alt_javah,
             'JAVAC': alt_javac,
             'JAR': alt_jar,
             'JARSIGNER': alt_jarsigner,
             'KEYTOOL': alt_keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
@@ -609,28 +612,49 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': alt_java,
             'JAVAH': alt_javah,
             'JAVAC': alt_javac,
             'JAR': alt_jar,
             'JARSIGNER': alt_jarsigner,
             'KEYTOOL': alt_keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
              checking for javac version... 1.8
         ''' % (alt_java, alt_javah, alt_jar, alt_jarsigner,
                alt_keytool, alt_javac)))
 
+        # --enable-java-coverage should set MOZ_JAVA_CODE_COVERAGE.
+        config, out, status = self.get_result(
+            args=['--enable-java-coverage'],
+            includes=includes,
+            extra_paths=paths,
+            environ={
+                'PATH': mozpath.dirname(java),
+                'JAVA_HOME': mozpath.dirname(mozpath.dirname(java)),
+            })
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {
+            'JAVA': java,
+            'JAVAH': javah,
+            'JAVAC': javac,
+            'JAR': jar,
+            'JARSIGNER': jarsigner,
+            'KEYTOOL': keytool,
+            'MOZ_JAVA_CODE_COVERAGE': True,
+        })
+
         def mock_old_javac(_, args):
             if len(args) == 1 and args[0] == '-version':
                 return 0, '1.6.9', ''
             self.fail("Unexpected arguments to mock_old_javac: %s" % args)
 
         # An old javac is fatal.
         paths[javac] = mock_old_javac
         config, out, status = self.get_result(includes=includes,