Jacoco coverage support for bazel

Change-Id: Ic94304aa6fed0e18f16cecfdda388d421064d0b6
diff --git a/tools/build/bazel/deps_files.bzl b/tools/build/bazel/deps_files.bzl
new file mode 100644
index 0000000..24c65ca
--- /dev/null
+++ b/tools/build/bazel/deps_files.bzl
@@ -0,0 +1,25 @@
+def _impl(ctx):
+    output = ctx.outputs.deps_files
+
+    dep_list = []
+    for dep in ctx.files.deps:
+        dep_list += [dep.path]
+
+    cmd = [
+        "echo %s >>  %s" % (",".join(dep_list), output.path),
+    ]
+
+    ctx.action(
+        inputs = ctx.files.deps,
+        outputs = [output],
+        progress_message = "Generating deps file paths for %s" % ctx.attr.name,
+        command = ";\n".join(cmd),
+    )
+
+deps_files = rule(
+    attrs = {
+        "deps": attr.label_list(allow_files = True),
+    },
+    implementation = _impl,
+    outputs = {"deps_files": "%{name}-deps-files.txt"},
+)
diff --git a/tools/build/bazel/generate_test_rules.bzl b/tools/build/bazel/generate_test_rules.bzl
index 17f811e..4e3d5f8 100644
--- a/tools/build/bazel/generate_test_rules.bzl
+++ b/tools/build/bazel/generate_test_rules.bzl
@@ -20,6 +20,8 @@
 files.
 """
 
+load("//tools/build/bazel:deps_files.bzl", "deps_files")
+
 def testIsExcluded(exclude_tests, test):
     for excluded_test in exclude_tests:
         normalized_excluded_test = excluded_test.replace(".", "/")
@@ -64,20 +66,41 @@
             native.package_name() + "/" + _strip_right(test, ".java"),
         )
         package = java_class[:java_class.rfind(".")]
+
         native.java_test(
+            data = ["@jacoco_agent_runtime//jar"],
             name = prefix + test,
             runtime_deps = deps,
             resources = resources,
             size = test_size,
-            jvm_flags = jvm_flags,
+            #jvm_flags = jvm_flags,
             args = args,
             flaky = flaky,
             tags = tags,
             test_class = java_class,
             visibility = visibility,
             shard_count = shard_count,
+            jvm_flags = jvm_flags,
         )
 
+        jacoco_agent = "$(location @jacoco_agent_runtime//jar)"
+        native.java_test(
+            data = ["@jacoco_agent_runtime//jar"],
+            name = prefix + test + "-coverage",
+            runtime_deps = deps,
+            resources = resources,
+            size = "large",
+            #jvm_flags = jvm_flags,
+            args = args,
+            flaky = flaky,
+            tags = tags,
+            test_class = java_class,
+            visibility = visibility,
+            shard_count = shard_count,
+            jvm_flags = jvm_flags + ["-javaagent:" + jacoco_agent + "=destfile=/tmp/jacoco.exec"],
+        )
+    deps_files(name = name + "-deps", deps = deps)
+
 def _get_test_names(test_files):
     test_names = []
     for test_file in test_files:
diff --git a/tools/build/bazel/generate_workspace.bzl b/tools/build/bazel/generate_workspace.bzl
index 8a3eefc..f449018 100644
--- a/tools/build/bazel/generate_workspace.bzl
+++ b/tools/build/bazel/generate_workspace.bzl
@@ -1,4 +1,4 @@
-# ***** This file was auto-generated at Thu, 17 Jan 2019 02:17:59 GMT. Do not edit this file manually. *****
+# ***** This file was auto-generated at Sat, 19 Jan 2019 00:49:49 GMT. Do not edit this file manually. *****
 # ***** Use onos-lib-gen *****
 
 load("//tools/build/bazel:variables.bzl", "ONOS_GROUP_ID", "ONOS_VERSION")
@@ -459,6 +459,12 @@
             jar_sha256 = "4633c331f50642ebe795dc089d6a5928aff43071c9d17e7840a009eea2fe95a3",
             licenses = ["notice"],
             jar_urls = ["http://repo1.maven.org/maven2/com/squareup/okio/okio/1.14.0/okio-1.14.0.jar"],        )
+    if "jacoco_agent_runtime" not in native.existing_rules():
+        java_import_external(
+            name = "jacoco_agent_runtime",
+            jar_sha256 = "940056732802d1cbaf243840e0f8f2c786f5deb6c1dcac786637366a051257d8",
+            licenses = ["notice"],
+            jar_urls = ["http://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/0.8.2/org.jacoco.agent-0.8.2-runtime.jar"],        )
     if "jackson_annotations" not in native.existing_rules():
         java_import_external(
             name = "jackson_annotations",
@@ -1467,6 +1473,7 @@
 artifact_map["@logging_interceptor//:logging_interceptor"] = "mvn:com.squareup.okhttp3:logging-interceptor:jar:NON-OSGI:3.9.1"
 artifact_map["@moshi//:moshi"] = "mvn:com.squareup.moshi:moshi:jar:NON-OSGI:1.4.0"
 artifact_map["@okio//:okio"] = "mvn:com.squareup.okio:okio:jar:NON-OSGI:1.14.0"
+artifact_map["@jacoco_agent_runtime//:jacoco_agent_runtime"] = "mvn:org.jacoco:org.jacoco.agent:jar:runtime:NON-OSGI:0.8.2"
 artifact_map["@jackson_annotations//:jackson_annotations"] = "mvn:com.fasterxml.jackson.core:jackson-annotations:jar:2.9.5"
 artifact_map["@jackson_core//:jackson_core"] = "mvn:com.fasterxml.jackson.core:jackson-core:jar:2.9.5"
 artifact_map["@jackson_databind//:jackson_databind"] = "mvn:com.fasterxml.jackson.core:jackson-databind:jar:2.9.5"
diff --git a/tools/build/onos-prepare-sonar b/tools/build/onos-prepare-sonar
index baa7888..758693a 100755
--- a/tools/build/onos-prepare-sonar
+++ b/tools/build/onos-prepare-sonar
@@ -1,129 +1,262 @@
 #!/usr/bin/env python
 
-# This script prepares this ONOS directory so that the Sonar Scanner can be run.
-#  - Build ONOS
-#  - Run tests on a per module basis, stage surefire-reports and jacoco.exec
-#  - Generate sonar-project.properties file
+"""
+This script prepares this ONOS directory so that the Sonar Scanner can be run.
+  - Build ONOS
+  - Run coverage tests on a per module basis, stage surefire-reports and jacoco.exec
+  - Generate sonar-project.properties file
+"""
 
-import json
 import os
+import sys
+import fnmatch
 
-from shutil import copy, copytree, rmtree
-from subprocess import call, check_call, check_output
+from shutil import copy, rmtree
+from subprocess import check_output, STDOUT, CalledProcessError
 
-# FIXME pull the version from the Buck version file
 ONOS_VERSION = '2.0.0-SNAPSHOT'
 
-# SonarQube property file name and template
-FILE_NAME = 'sonar-project.properties'
+GENFILES = 'bazel-genfiles'
+SONAR_PROJECT = GENFILES + '/sonar-project'
+SUREFIRE_REPORTS = 'surefire-reports'
+SONAR_PROPERTIES_FILE_NAME = SONAR_PROJECT + '/sonar-project.properties'
+
+# Template for the sonar properties file
 ROOT_TEMPLATE = '''# Auto-generated properties file
 sonar.projectKey=%(key)s
 sonar.projectName=%(name)s
 sonar.projectVersion=%(version)s
 
-#sonar.sources=src
 sonar.sourceEncoding=UTF-8
 sonar.java.target = 1.8
 sonar.java.source = 1.8
 sonar.language=java
 
+sonar.junit.reportsPath = surefire-reports
+sonar.jacoco.reportPath = jacoco.exec
 
 sonar.modules=%(modules)s
 
 '''
 
-black_list = ["//protocols/grpc:grpc-core-repkg",
-              "//apps/openstacktelemetry:grpc-core-repkg",
-              "//web/gui2:_onos-gui2-base-jar"]
 
-# Change to $ONOS_ROOT
-ONOS_ROOT = os.environ['ONOS_ROOT']
-if ONOS_ROOT:
-    os.chdir(ONOS_ROOT)
-
-
-def splitTarget(target):
+def split_target(target):
     path, module = target.split(':', 2)
     path = path.replace('//', '', 1)
     return path, module
 
 
-def runCmd(cmd):
+def run_command(cmd):
     output = check_output(cmd).rstrip()
     return output.split('\n') if output else []
 
 
-# build ONOS
-runCmd(["bazel", "build", "onos"])
+def run_command_with_stderr(cmd):
+    output = check_output(cmd, stderr=STDOUT).rstrip()
+    return output.split('\n') if output else []
 
-# Find all onos OSGi jar file rules
-targets = runCmd(["bazel", "query", "kind('_bnd', '//...')"])
-targets = [target for target in targets if not target in black_list]
-# Uncomment this for easier debugging of a single package
-# targets = ['//core/net:onos-core-net']
 
-# Find all tests associated with onos_jar rules
-# FIXME we may want to insert kind('java_test', testsof...)
-# output = runCmd([BUCK, 'query', '--json', "testsof('%s')"] + targets)
-# test_map = json.loads(output[0])
+def make_dirs(path):
+    try:
+        os.makedirs(path)
+    except OSError:
+        pass
 
-# Flatten the values in the test target map
-# test_targets = [t for ts in test_map.values() for t in ts]
-# print test_targets
 
-# Build run tests
-# print runCmd([BUCK, 'test', '--no-cache', '--code-coverage', '--no-results-cache'] + test_targets)
+def find_bazel_project():
+    return os.path.basename(os.getcwd())
 
-# Build the sonar rules for each target
-# sonar_files = runCmd([BUCK, 'build', '--show-output'] + ['%s-sonar' % t for t in (targets + test_targets)])
-# sonar_files = dict([i.split(' ') for i in sonar_files[1:]]) # drop the first line; it's boilerplate
-# print sonar_files
+
+def find_bazel_classes_directory(module_name, path):
+    return os.getcwd() + "/bazel-out/darwin-fastbuild/bin/" + path + \
+           "/_javac/" + module_name + "-native/lib" + module_name + "-native-class_classes"
+
+
+def capture_surefire_reports(module_path):
+    matches = []
+    for root, dirnames, filenames in os.walk('bazel-testlogs/' + module_path + '/src/test/java'):
+        for filename in fnmatch.filter(filenames, '*.xml'):
+            source_path = os.path.join(root, filename)
+            matches.append(source_path)
+
+            destination_path = \
+                SONAR_PROJECT + "/" + module_path + "/" + SUREFIRE_REPORTS + "/TEST-" + \
+                source_path.replace("bazel-testlogs/", "")\
+                .replace("/test.xml", "test.xml")\
+                .replace(module_path, "")\
+                .replace("/src/test/java/", "")\
+                .replace("/", ".")
+            make_dirs(path=os.path.dirname(destination_path))
+            copy(source_path, destination_path)
+
+
+def capture_jacoco(module_path):
+    source_path = '/tmp/jacoco.exec'
+    destination_path = \
+        SONAR_PROJECT + "/" + module_path
+    make_dirs(path=os.path.dirname(destination_path))
+    copy(source_path, destination_path)
+
+
+def capture_sources(module_path):
+    source_path = module_path + '/src'
+    destination_path = SONAR_PROJECT + "/" + module_path + '/src'
+    os.symlink(os.getcwd() + '/' + source_path, destination_path)
+
+
+"""
+    Writes out the properties for a given module and stages the files needed by the scanner.
+"""
 
 
 def write_module(target, out):
-    path, module_name = splitTarget(target)
+    path, module_name = split_target(target)
+    query = 'labels(srcs, "%s-native")' % target
+
+    # get rid of previous data
+    try:
+        os.remove('/tmp/jacoco.exec')
+    except OSError:
+        pass
+
+    try:
+        # Find all the test targets in this package
+        coverage_target_query = "attr(name, .*-coverage, tests(//" + path + ":*))"
+        coverage_targets_result = run_command(["bazel", "query", coverage_target_query])
+
+        # Find the test targets that are coverage targets
+        run_coverage_command = ['bazel', 'test']
+        for coverage_target in coverage_targets_result:
+            run_coverage_command.append(str(coverage_target))
+    except CalledProcessError:
+        print "Error querying test files for target " + target
+        return
+
+    try:
+        # Use bazel to run all the coverage targets
+        run_coverage_command.append('--cache_test_results=no')
+        run_command(run_coverage_command)
+
+        # Find the source files used by the tests
+        sources = run_command(['bazel', 'query', query])
+    except CalledProcessError as exc:
+        print "Error running test files for target " + target
+        raise exc
+
+    if not os.path.exists('/tmp/jacoco.exec'):
+        # No coverage data was produced, not much to do
+        return
+
+    # Filter out non-Java files
+    sources = \
+        [source_file for source_file in sources if "package-info" not in source_file and ".java" in source_file]
+
+    # Adjust source file paths to be relative to the root
+    sources_filtered = []
+    for source in sources:
+        sources_filtered.append(source.replace('//' + path + ':', "", 1))
+
+    # create a CSL of all the source files for use in the properties file
+    sources_csl = ",".join(sources_filtered).replace("//", "").replace(":", "/")
+
+    # Write out the properties for this package
     out.write('%s.sonar.projectBaseDir=%s\n' % (module_name, path))
     out.write('%(name)s.sonar.projectName=%(name)s\n' % {'name': module_name})
-    query = 'labels(srcs, "%s-native")' % target
-    sources = runCmd(['bazel', 'query', query])
-    sources = [file for file in sources if "package-info" not in file and ".java" in file]
-    print sources
-    sources_csl = ",".join(sources).replace("//", ONOS_ROOT + "/").replace(":", "/")
     out.write('%s.sonar.sources=%s\n' % (module_name, sources_csl))
+    binaries = find_bazel_classes_directory(module_name, path)
+    out.write('%s.sonar.java.binaries = %s\n' % (module_name, binaries))
 
-    # tests = test_map[target] if target in test_map else []
+    # Get the dependencies for this package using bazel
+    deps_files = run_command_with_stderr(['bazel', 'build', '%s-tests-gen-deps' % target])
 
-    # module_targets = [target] + tests
-    # for property in [sonar_files[t+'-sonar'] for t in module_targets]:
-    #  print property
-    #  with open(property, 'r') as f:
-    #    for line in f.readlines():
-    #      out.write('%s.%s' % (module_name, line))
+    dep_file = ""
+    for source_file in deps_files:
+        if source_file.endswith(".txt"):
+            dep_file = source_file.strip()
+
+    libraries = []
+    with open(dep_file, 'r') as read_files:
+        lines = read_files.readline().split(',')
+
+        external_base_path = os.path.realpath(
+            os.path.realpath("bazel-" + find_bazel_project() + "/external") + "/../../..")
+        for line in lines:
+            library_file_name = line
+            if line.startswith("external"):
+                library_file_name = external_base_path + "/" + library_file_name
+            else:
+                library_file_name = os.getcwd() + "/" + library_file_name
+            libraries.append(library_file_name)
+
+    out.write('%s.sonar.java.libraries = %s\n' % (module_name, ",".join(libraries)))
+
+    # Capture files needed by the scanner into the staging area
+    capture_surefire_reports(path)
+    capture_jacoco(path)
+    capture_sources(path)
 
 
-#  if tests:
-#    rmtree(path + '/surefire-reports', ignore_errors=True)
-#    rmtree('surefire-reports', ignore_errors=True)
-#    runCmd([BUCK, 'test',
-#            '--no-cache', '--no-results-cache',
-#            '--code-coverage',
-#            '--no-results-cache',
-#            '--surefire-xml', 'surefire-reports'
-#            ] + tests)
-#    copy('buck-out/gen/jacoco/jacoco.exec', path)
-#    #write jacoco.exec path to out; not needed.. this is the default
-#    copytree('surefire-reports', path + '/surefire-reports')
-#    rmtree('surefire-reports')
+def _main():
+    global ONOS_ROOT, targets
+    debug = False
+    if len(sys.argv) > 1:
+        debug = True
 
-# Write the sonar properties file
-with open(FILE_NAME, 'w') as out:
-    out.write(ROOT_TEMPLATE % {
+    # Change to $ONOS_ROOT
+    ONOS_ROOT = os.environ['ONOS_ROOT']
+    if ONOS_ROOT:
+        os.chdir(ONOS_ROOT)
+
+    # build ONOS
+    run_command(["bazel", "build", "onos"])
+
+    # find the test targets to get coverage for
+    if debug:
+        # Use a predefined list of targets for debugging
+        targets = ['//core/net:onos-core-net', '//utils/misc:onlab-misc']
+    else:
+        # Query all onos OSGi jar file rules with tests from bazel
+        targets = run_command(["bazel", "query", "attr('name', '.*-tests-gen', '//...')"])
+        targets = [target.replace("-tests-gen-deps", "") for target in targets]
+
+    # Filter out targets without any tests in them
+    targets_with_tests = []
+    for target in targets:
+        colon = target.find(':')
+        base_target = target[0:colon]
+        target_query_result = run_command(['bazel', 'query', 'tests(' + base_target + ':*)'])
+        for result_line in target_query_result:
+            if "src/test" in result_line:
+                targets_with_tests.append(target)
+                break
+    targets = targets_with_tests
+
+    # Clear out any old results
+    rmtree(SONAR_PROJECT, True)
+
+    # make a directory for the new results
+    make_dirs(SONAR_PROJECT)
+
+    # fill in the template for the sonar properties
+    sonar_parameters = {
         'name': 'onos',
         'key': 'org.onosproject:onos',
         'version': ONOS_VERSION,
-        #'jacoco': '%s/buck-out/gen/jacoco/jacoco.exec' % ONOS_ROOT,
-        'modules': ','.join([splitTarget(t)[1] for t in targets])
-    })
-    for target in targets:
-        print target
-        write_module(target, out)
+        'jacoco': '/tmp/jacoco.exec',
+        'reports': 'surefire-reports',
+        'modules': ','.join([split_target(t)[1] for t in targets])
+    }
+    if debug:
+        sonar_parameters["key"] = 'org.onosproject:onos-test-sonar'
+        sonar_parameters["name"] = 'onos-test-sonar'
+
+    # Write the sonar properties file
+    with open(SONAR_PROPERTIES_FILE_NAME, 'w') as out:
+        out.write(ROOT_TEMPLATE % sonar_parameters)
+        for target in targets:
+            print "Processing coverage for target " + target
+            write_module(target, out)
+
+
+if __name__ == "__main__":
+    _main()
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index ccb8b43..769db65 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -59,7 +59,7 @@
 
 # Short-hand for ONOS build, package and test.
 alias op="SHLVL=1 bazel build //:onos"
-alias ot="bazel run //:buildifier_check && bazel query 'tests(//...)' | SHLVL=1 xargs bazel test --test_summary=terse --test_output=errors"
+alias ot="bazel run //:buildifier_check && bazel query 'tests(//...)' | grep -v "-coverage" | SHLVL=1 xargs bazel test --test_summary=terse --test_output=errors"
 alias ob="op && ot"
 alias obd="SHLVL=1 bazel build //docs:external //docs:internal"