Merge "Add "target_suffix" to gerrit_plugin()"
diff --git a/buckversion b/buckversion
index 46408a5..6686020 100644
--- a/buckversion
+++ b/buckversion
@@ -1 +1 @@
-8204fddf60b25a3c2090f3ef0742fca5d466d562
+d1be554f51fb9b2f090a85fcdbcef3b4dbbef8d7
diff --git a/gerrit_plugin.bucklet b/gerrit_plugin.bucklet
index 4b8f418..922bd77 100644
--- a/gerrit_plugin.bucklet
+++ b/gerrit_plugin.bucklet
@@ -49,8 +49,9 @@
   '-XdisableCastChecking',
 ]
 
-GERRIT_PLUGIN_API = ['//lib/gerrit:plugin-api']
 GERRIT_GWT_API = ['//lib/gerrit:gwtui-api']
+GERRIT_PLUGIN_API = ['//lib/gerrit:plugin-api']
+GERRIT_TESTS = ['//lib/gerrit:acceptance-framework']
 
 GWT_DEPS = [
   '//lib/gwt:user',
diff --git a/tools/buckToJUnit.xsl b/tools/buckToJUnit.xsl
new file mode 100644
index 0000000..424c050
--- /dev/null
+++ b/tools/buckToJUnit.xsl
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<!--
+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.
+-->
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns:func="com.google.gerrit" xmlns:xs="http://www.w3.org/2001/XMLSchema"
+    exclude-result-prefixes="func" version="2.0">
+  <xsl:output method="xml" omit-xml-declaration="no" indent="yes" encoding="UTF-8"/>
+  <xsl:strip-space elements="*"/>
+
+  <xsl:template match="/tests">
+    <xsl:apply-templates/>
+  </xsl:template>
+
+  <xsl:template match="test">
+    <xsl:variable name="filename" select="concat('TEST-', @name, '.xml')"/>
+    <xsl:result-document href="{$filename}" method="xml">
+      <xsl:variable name="testCount" select="count(testresult)"/>
+      <xsl:variable name="nonEmptyStacks" select="count(testresult[stacktrace != ''])"/>
+      <xsl:variable name="failures"
+          select="count(testresult[contains(stacktrace, 'java.lang.AssertionError')])"/>
+      <xsl:variable name="errors" select="$nonEmptyStacks - $failures"/>
+      <testsuite failures="{$failures}" time="{func:toMS(@time)}" errors="{$errors}" skipped="0"
+          tests="{$testCount}" name="{@name}">
+        <xsl:apply-templates/>
+      </testsuite>
+    </xsl:result-document>
+  </xsl:template>
+
+  <xsl:template match="testresult">
+    <testcase time="{func:toMS(@time)}" classname="{../@name}" name="{@name}">
+      <xsl:apply-templates/>
+    </testcase>
+  </xsl:template>
+
+  <xsl:template match="message"/>
+
+  <xsl:template match="stacktrace[. != '']">
+    <failure message="{../message}" type="{substring-before(., ':')}">
+      <xsl:value-of select="."/>
+    </failure>
+  </xsl:template>
+
+  <xsl:function name="func:toMS">
+    <xsl:param name="sec" as="xs:decimal"/>
+    <xsl:value-of select="$sec div 1000"/>
+  </xsl:function>
+</xsl:stylesheet>
diff --git a/tools/buck_to_junit.py b/tools/buck_to_junit.py
new file mode 100755
index 0000000..f40a4ff
--- /dev/null
+++ b/tools/buck_to_junit.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env 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 optparse import OptionParser
+from os import path, chdir
+from os.path import abspath
+from shutil import rmtree
+from subprocess import check_call, CalledProcessError
+from tempfile import mkdtemp
+
+opts = OptionParser()
+opts.add_option('-t', help='test report to convert')
+opts.add_option('-o', help='output directory')
+args, _ = opts.parse_args()
+temp_dir = mkdtemp()
+try:
+  try:
+    check_call(
+      ['curl', '--proxy-anyauth', '-sfo', path.join(temp_dir, 'saxon.jar'),
+       'http://central.maven.org/maven2/net/sf/saxon/Saxon-HE/9.6.0-6/Saxon-HE-9.6.0-6.jar'])
+  except OSError as err:
+    print('could not invoke curl: %s\nis curl installed?' % err)
+    exit(1)
+  except CalledProcessError as err:
+    print('error using curl: %s' % err)
+    exit(1)
+
+  buck_report = abspath(args.t)
+  buck_to_junit_xsl = abspath(
+    path.join(path.abspath(path.dirname(__file__)), 'buckToJUnit.xsl'))
+
+  chdir(args.o)
+  try:
+    check_call(
+      ['java', '-jar', path.join(temp_dir, 'saxon.jar'), '-s:' + buck_report,
+       '-xsl:' + buck_to_junit_xsl])
+  except CalledProcessError as err:
+    print('error converting to junit: %s' % err)
+    exit(1)
+finally:
+  rmtree(temp_dir, ignore_errors=True)
diff --git a/tools/download_all.py b/tools/download_all.py
index a70cbda..17e7ab0 100755
--- a/tools/download_all.py
+++ b/tools/download_all.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/download_file.py b/tools/download_file.py
index ab8647d..49f6c4d 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,9 +25,14 @@
 from zipfile import ZipFile, BadZipfile, LargeZipFile
 
 GERRIT_HOME = path.expanduser('~/.gerritcodereview')
-CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
+CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache', 'downloaded-artifacts')
+# LEGACY_CACHE_DIR is only used to allow existing workspaces to move already
+# downloaded files to the new cache directory.
+# Please remove after 3 months (2015-10-07).
+LEGACY_CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
 LOCAL_PROPERTIES = 'local.properties'
 
+
 def hashfile(p):
   d = sha1()
   with open(p, 'rb') as f:
@@ -38,6 +43,7 @@
       d.update(b)
   return d.hexdigest()
 
+
 def safe_mkdirs(d):
   if path.isdir(d):
     return
@@ -47,6 +53,7 @@
     if not path.isdir(d):
       raise err
 
+
 def download_properties(root_dir):
   """ Get the download properties.
 
@@ -73,14 +80,26 @@
       pass
   return p
 
+
 def cache_entry(args):
   if args.v:
     h = args.v
   else:
-    h = sha1(args.u).hexdigest()
+    h = sha1(args.u.encode('utf-8')).hexdigest()
   name = '%s-%s' % (path.basename(args.o), h)
   return path.join(CACHE_DIR, name)
 
+
+# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
+def legacy_cache_entry(args):
+  if args.v:
+    h = args.v
+  else:
+    h = sha1(args.u.encode('utf-8')).hexdigest()
+  name = '%s-%s' % (path.basename(args.o), h)
+  return path.join(LEGACY_CACHE_DIR, name)
+
+
 opts = OptionParser()
 opts.add_option('-o', help='local output file')
 opts.add_option('-u', help='URL to download')
@@ -98,8 +117,19 @@
 
 redirects = download_properties(root_dir)
 cache_ent = cache_entry(args)
+legacy_cache_ent = legacy_cache_entry(args)
 src_url = resolve_url(args.u, redirects)
 
+# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
+if not path.exists(cache_ent) and path.exists(legacy_cache_ent):
+  try:
+    safe_mkdirs(path.dirname(cache_ent))
+  except OSError as err:
+    print('error creating directory %s: %s' %
+          (path.dirname(cache_ent), err), file=stderr)
+    exit(1)
+  shutil.move(legacy_cache_ent, cache_ent)
+
 if not path.exists(cache_ent):
   try:
     safe_mkdirs(path.dirname(cache_ent))
@@ -137,30 +167,24 @@
   exclude += args.x
 if args.exclude_java_sources:
   try:
-    zf = ZipFile(cache_ent, 'r')
-    try:
+    with ZipFile(cache_ent, 'r') as zf:
       for n in zf.namelist():
         if n.endswith('.java'):
           exclude.append(n)
-    finally:
-      zf.close()
   except (BadZipfile, LargeZipFile) as err:
-    print('error opening %s: %s'  % (cache_ent, err), file=stderr)
+    print('error opening %s: %s' % (cache_ent, err), file=stderr)
     exit(1)
 
 if args.unsign:
   try:
-    zf = ZipFile(cache_ent, 'r')
-    try:
+    with ZipFile(cache_ent, 'r') as zf:
       for n in zf.namelist():
         if (n.endswith('.RSA')
             or n.endswith('.SF')
             or n.endswith('.LIST')):
           exclude.append(n)
-    finally:
-      zf.close()
   except (BadZipfile, LargeZipFile) as err:
-    print('error opening %s: %s'  % (cache_ent, err), file=stderr)
+    print('error opening %s: %s' % (cache_ent, err), file=stderr)
     exit(1)
 
 safe_mkdirs(path.dirname(args.o))
diff --git a/tools/eclipse.py b/tools/eclipse.py
index 502d234..3195b2a 100755
--- a/tools/eclipse.py
+++ b/tools/eclipse.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -148,7 +148,7 @@
 
   try:
     targets = ['//bucklets/tools:buck.properties'] + MAIN
-    check_call(['buck', 'build'] + targets)
+    check_call(['buck', 'build', '--deep'] + targets)
   except CalledProcessError as err:
     exit(1)
 except KeyboardInterrupt:
diff --git a/tools/gen_sonar_project_properties.py b/tools/gen_sonar_project_properties.py
new file mode 100755
index 0000000..2ef679f
--- /dev/null
+++ b/tools/gen_sonar_project_properties.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env 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/mvn.py b/tools/mvn.py
old mode 100644
new mode 100755
index 107db60..a155e16
--- a/tools/mvn.py
+++ b/tools/mvn.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/pack_war.py b/tools/pack_war.py
index 09ff054..cfd7963 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/sonar.py b/tools/sonar.py
new file mode 100755
index 0000000..7a405a1
--- /dev/null
+++ b/tools/sonar.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env 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)