Run PolyGerrit tests with Buck

Since Buck has no native support for web tests, wrap the tests in a
Python shim that calls wct. As in other Polymer cases, we need to
prepare a set of inputs and create a directory containing exactly what
wct expects to be there. Buck exposes resources to python_tests using
the pkg_resources API, which is cumbersome to use, and easier just to
ship around zip files as we do elsewhere.

Unlike other npm binaries we've encountered, the web-component-tester
module has numerous native dependencies, up to and including Selenium.
Rather than get in the game of distributing platform-specific
binaries, punt and require `wct` to be on the user's $PATH for now.

Tests are currently excluded in .buckconfig but can be run directly
with either:

  buck test //polygerrit-ui/app:polygerrit_tests
  buck test --include web

Change-Id: Ia314213925ac27ff271374a96ed539fb2acf0187
diff --git a/.buckconfig b/.buckconfig
index 38fcc58..fc51e19 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -28,3 +28,6 @@
 [cache]
   mode = dir
   dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
+
+[test]
+  excluded_labels = manual
diff --git a/lib/js/BUCK b/lib/js/BUCK
index c683128..9f5390f 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -114,6 +114,16 @@
 )
 
 bower_component(
+  name = 'iron-test-helpers',
+  package = 'PolymerElements/iron-test-helpers',
+  version = '1.0.6',
+  semver = '~1.0.6',
+  deps = [':polymer'],
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = 'c0f7c7f010ca3c63fb08ae0d9462e400380cde2c',
+)
+
+bower_component(
   name = 'iron-validatable-behavior',
   package = 'PolymerElements/iron-validatable-behavior',
   version = '1.0.5',
@@ -152,6 +162,15 @@
 )
 
 bower_component(
+  name = 'test-fixture',
+  package = 'PolymerElements/test-fixture',
+  version = '1.0.3',
+  semver = '^1.0.0',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = '21192d554ff6ad7eea894ca751c73b6bc46867dc',
+)
+
+bower_component(
   name = 'webcomponentsjs',
   package = 'webcomponentsjs',
   version = '0.7.17',
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 00ff612..c2cd4bd 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -1,5 +1,18 @@
 # PolyGerrit
 
+## Installing [Node.js](https://nodejs.org/en/download/)
+
+```sh
+# Debian/Ubuntu
+sudo apt-get install nodejs-legacy
+
+# OS X with Homebrew
+brew install node
+```
+
+All other platforms: [download from
+nodejs.org](https://nodejs.org/en/download/).
+
 ## Local UI, Production Data
 
 To test the local UI against gerrit-review.googlesource.com:
@@ -31,7 +44,27 @@
 
 ## Running Tests
 
+One-time setup:
+
 ```sh
-npm install -g web-component-tester
-wct
+# Debian/Ubuntu
+sudo apt-get install npm
+
+# OS X with Homebrew
+brew install npm
+
+# All platforms (including those above)
+sudo npm install -g web-component-tester
+```
+
+Run all web tests:
+
+```sh
+buck test --include web
+```
+
+If you need to pass additional arguments to `wct`:
+
+```sh
+WCT_ARGS='-p --some-flag="foo bar"' buck test --no-results-cache --include web
 ```
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK
index 9da030d..317d6b2 100644
--- a/polygerrit-ui/app/BUCK
+++ b/polygerrit-ui/app/BUCK
@@ -1,5 +1,9 @@
 include_defs('//lib/js.defs')
 
+WCT_TEST_PATTERNS = ['test/**']
+PY_TEST_PATTERNS = ['polygerrit_wct_tests.py']
+APP_SRCS = glob(['**'], excludes = WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
+
 WEBJS = 'bower_components/webcomponentsjs/webcomponents-lite.js'
 
 # TODO(dborowitz): Putting these rules in this package avoids having to handle
@@ -38,7 +42,38 @@
 vulcanize(
   name = 'polygerrit',
   app = 'elements/gr-app.html',
-  srcs = glob(['**'], excludes = ['index.html']),
+  srcs = APP_SRCS,
   components = ['//polygerrit-ui:polygerrit_components'],
 )
 
+
+bower_components(
+  name = 'test_components',
+  deps = [
+    '//polygerrit-ui:polygerrit_components',
+    '//lib/js:iron-test-helpers',
+    '//lib/js:test-fixture',
+  ],
+)
+
+genrule(
+  name = 'test_resources',
+  cmd = ' && '.join([
+    'cd $TMP',
+    'unzip -q $(location :test_components)',
+    'cp -rL $SRCDIR/* .',
+    'zip -r $OUT .',
+  ]),
+  srcs = APP_SRCS + glob(WCT_TEST_PATTERNS),
+  out = 'test_resources.zip',
+)
+
+python_test(
+  name = 'polygerrit_tests',
+  srcs = glob(PY_TEST_PATTERNS),
+  resources = [':test_resources'],
+  labels = [
+    'manual',
+    'web',
+  ],
+)
diff --git a/polygerrit-ui/app/polygerrit_wct_tests.py b/polygerrit-ui/app/polygerrit_wct_tests.py
new file mode 100644
index 0000000..571dcb8
--- /dev/null
+++ b/polygerrit-ui/app/polygerrit_wct_tests.py
@@ -0,0 +1,105 @@
+# 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
+
+import atexit
+from distutils import spawn
+import json
+import os
+import pkg_resources
+import shlex
+import shutil
+import subprocess
+import sys
+import tempfile
+import unittest
+import zipfile
+
+
+def _write_wct_conf(root, exports):
+  with open(os.path.join(root, 'wct.conf.js'), 'w') as f:
+    f.write('module.exports = %s;\n' % json.dumps(exports))
+
+
+def _wct_cmd():
+  return ['wct'] + shlex.split(os.environ.get('WCT_ARGS', ''))
+
+
+class PolyGerritWctTests(unittest.TestCase):
+
+  # Should really be setUpClass/tearDownClass, but Buck's test runner doesn't
+  # produce sane stack traces from those methods. There's only one test method
+  # anyway, so just use setUp.
+
+  def _check_wct(self):
+    self.assertTrue(
+        spawn.find_executable('wct'),
+        msg='wct not found; try `npm install -g web-component-tester`')
+
+  def _extract_resources(self):
+    tmpdir = tempfile.mkdtemp()
+    atexit.register(lambda: shutil.rmtree(tmpdir))
+    root = os.path.join(tmpdir, 'polygerrit')
+    os.mkdir(root)
+
+    tr = 'test_resources.zip'
+    zip_path = os.path.join(tmpdir, tr)
+    s = pkg_resources.resource_stream(__name__, tr)
+    with open(zip_path, 'w') as f:
+      shutil.copyfileobj(s, f)
+
+    with zipfile.ZipFile(zip_path, 'r') as z:
+      z.extractall(root)
+
+    return tmpdir, root
+
+  def test_wct(self):
+    self._check_wct()
+    tmpdir, root = self._extract_resources()
+
+    cmd = _wct_cmd()
+    print('Running %s in %s' % (cmd, root), file=sys.stderr)
+
+    _write_wct_conf(root, {
+      'suites': ['test'],
+      'webserver': {
+        'pathMappings': [
+          {'/components/bower_components': 'bower_components'},
+        ],
+      },
+      'plugins': {
+        'local': {
+          # For some reason wct tries to install selenium into its node_modules
+          # directory on first run. If you've installed into /usr/local and
+          # aren't running wct as root, you're screwed. Turning this option off
+          # seems to still work, so there's that.
+          'skipSeleniumInstall': True,
+        },
+      },
+    })
+
+    p = subprocess.Popen(cmd, cwd=root,
+                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, err = p.communicate()
+    sys.stdout.write(out)
+    sys.stderr.write(err)
+    self.assertEquals(0, p.returncode)
+
+    # Only remove tmpdir if successful, to allow debugging.
+    shutil.rmtree(tmpdir)
+
+
+if __name__ == '__main__':
+  unittest.main()