Add scripts to use Sonarqube for plugins built with buck

Script gen_sonar_project_properties.py allows to generate
sonar-project.properties file. This script can be used in CI server
(e.g. Jenkins) to allow running a Sonarqube analysis.

Script sonar.py run a Sonarqube analysis and upload the results to the
local sonar instance, similar to what `mvn sonar:sonar` would do.

Change-Id: I8ebf76a601effe8e6c94cf9130e8946ca56ddc5e
diff --git a/tools/gen_sonar_project_properties.py b/tools/gen_sonar_project_properties.py
new file mode 100755
index 0000000..5f59854
--- /dev/null
+++ b/tools/gen_sonar_project_properties.py
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+# Copyright (C) 2015 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.
+
+from __future__ import print_function
+from optparse import OptionParser
+from os import path, walk
+from subprocess import check_output
+import re
+
+
+def guess_maven_group_id(plugin_name, plugin_dir):
+  current_dir = None
+  for dir_path, dirs, files in walk(path.join(plugin_dir, 'src', 'main', 'java')):
+    if len(files) > 0 or len(dirs) > 1 or path.basename(dir_path) == plugin_name:
+      break
+    current_dir = dir_path
+
+  if current_dir is None:
+    return str(hash(plugin_name))
+
+  group_id = []
+  while not path.basename(current_dir) == 'java':
+    group_id.append(path.basename(current_dir))
+    current_dir = path.dirname(current_dir)
+  return '.'.join(reversed(group_id))
+
+
+def get_plugin_version(plugin_dir):
+  version = '1.0'
+  version_file_path = path.join(plugin_dir, 'VERSION')
+  if path.exists(version_file_path):
+    try:
+      with open(version_file_path, "r") as version_file:
+        data = re.sub(r"\s+", '', version_file.read())
+    except Exception as err:
+      print('error reading plugin version: %s' % err)
+    else:
+      match = re.search(r"PLUGIN_VERSION='(.*?)'", data)
+      if match:
+        version = match.group(1)
+  elif path.exists(path.join(plugin_dir, '.git')):
+    version = check_output(['git', 'describe', '--always', 'HEAD'],
+                           cwd=plugin_dir)
+  return version
+
+
+def generate_project_properties(plugin_name, plugin_dir, classes_dir, test_dir,
+                                output):
+  try:
+    with open(output, 'w') as fd:
+      print("""\
+sonar.projectKey=%s
+sonar.projectName=%s
+sonar.projectVersion=%s
+
+sonar.language=java
+sonar.sources=%s
+sonar.tests=%s
+sonar.sourceEncoding=UTF-8
+sonar.java.binaries=%s
+
+sonar.junit.reportsPath=%s
+sonar.core.codeCoveragePlugin=jacoco
+sonar.jacoco.reportPath=%s\
+""" % (guess_maven_group_id(plugin_name, plugin_dir) + ":" + plugin_name,
+       plugin_name,
+       get_plugin_version(plugin_dir),
+       path.join(plugin_dir, 'src', 'main', 'java'),
+       path.join(plugin_dir, 'src', 'test', 'java'),
+       classes_dir,
+       test_dir,
+       path.join(plugin_dir, 'buck-out', 'gen', 'jacoco', 'jacoco.exec')),
+            file=fd)
+  except Exception as err:
+      print('error writing project properties file: %s' % err)
+
+
+if __name__ == '__main__':
+  opts = OptionParser()
+  opts.add_option('-n', help='plugin name')
+  opts.add_option('-c', help='classes directory')
+  opts.add_option('-t', help='test report directory')
+  opts.add_option('-o', help='output file', default='sonar-project.properties')
+  args, _ = opts.parse_args()
+
+  plugin_dir = path.abspath(__file__)
+  for _ in range(0, 3):
+    plugin_dir = path.dirname(plugin_dir)
+  generate_project_properties(args.n, plugin_dir, path.abspath(args.c),
+                              path.abspath(args.t), args.o)
diff --git a/tools/sonar.py b/tools/sonar.py
new file mode 100755
index 0000000..b13b5ce
--- /dev/null
+++ b/tools/sonar.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+# Copyright (C) 2015 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.
+
+
+# This script runs a Sonarqube analysis for a Gerrit plugin and uploads the
+# results to the local Sonarqube instance, similar to what `mvn sonar:sonar`
+# would do.
+#
+# It will build the plugin, run the tests, generate sonar-project.properties
+# file and then call sonar-runner (sonar-runner must be installed and available
+# in the path).
+#
+# This script must be called from the root folder of a gerrit plugin supporting
+# standalone buck build:
+#
+# ./bucklets/tools/sonar.py
+#
+
+from __future__ import print_function
+from os import path, makedirs
+import re
+from shutil import rmtree
+from tempfile import mkdtemp
+from subprocess import call, check_call, CalledProcessError
+from zipfile import ZipFile
+
+from gen_sonar_project_properties import generate_project_properties
+
+
+def get_plugin_name(buck_file):
+  try:
+    with open(buck_file, "r") as f:
+      data = re.sub(r"\s+", '', f.read())
+    return re.search(r"gerrit_plugin\(name='(.*?)'.*\)$", data).group(1)
+  except Exception as err:
+    exit('Failed to read plugin name from BUCK file: %s' % err)
+
+
+plugin_dir = path.abspath(__file__)
+for _ in range(0, 3):
+  plugin_dir = path.dirname(plugin_dir)
+
+plugin_name = get_plugin_name(path.join(plugin_dir, 'BUCK'))
+
+temp_dir = mkdtemp()
+try:
+  try:
+    check_call(['buck', 'build', '//:' + plugin_name])
+  except CalledProcessError as err:
+    exit(1)
+
+  classes_dir = path.join(temp_dir, 'classes')
+  with ZipFile(path.join(plugin_dir, 'buck-out', 'gen', plugin_name + '.jar'),
+               "r") as z:
+    z.extractall(classes_dir)
+
+  test_report = path.join(temp_dir, 'testReport.xml')
+  call(['buck', 'test', '--no-results-cache', '--code-coverage', '--xml',
+        test_report])
+
+  junit_test_report_dir = path.join(temp_dir, 'junitTestReport')
+  makedirs(junit_test_report_dir)
+
+  try:
+    check_call(
+      [path.join(path.abspath(path.dirname(__file__)), 'buck_to_junit.py'),
+       '-t', test_report, '-o', junit_test_report_dir])
+  except CalledProcessError as err:
+    exit(1)
+
+  sonar_project_properties = path.join(temp_dir, 'sonar-project.properties')
+
+  generate_project_properties(plugin_name, plugin_dir, classes_dir,
+                              junit_test_report_dir, sonar_project_properties)
+
+  try:
+    check_call(['sonar-runner',
+                '-Dproject.settings=' + sonar_project_properties, ])
+  except CalledProcessError as err:
+    exit(1)
+finally:
+  rmtree(path.join(plugin_dir, '.sonar'), ignore_errors=True)
+  rmtree(temp_dir, ignore_errors=True)