tools/eclipse: find jar from rules_jvm_external

The Eclipse project.py script crawls through Bazel `output_base` to find
the project jar dependencies. It recognizes location written by
`maven_jar`.

When using `rules_jvm_external` artifacts are symlinked under different
paths and the utility is unable to find them. It results in a generated
`.classpath` missing most entries.

`rules_jvm_external` symlinks the cached jar under:
bazel-out/k8-fastbuild/bin/external/maven

This change adds support to find them there.

In order to have the JavaDoc displayed, the source jar have to be
retrieved as well which `rules_jvm_external` does not do by default. One
has to turn that on explicitly:

  load("@rules_jvm_external//:defs.bzl", "maven_install")
  maven_install(
    artifacts = ['x', 'y', 'z' ],
    fetch_sources = True,
  )

The source jar will then be made available with the suffix
`-sources.jar`, unlike `maven_jar` which uses `-src.jar`.

Change-Id: Ifb56b2f84b5f38a700362c1de436cd4b4714dc2f
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 93eeb3a..d92dbb6 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -116,7 +116,7 @@
     name = 'bazel-bin/tools/eclipse/' + t.split(':')[1] + '.runtime_classpath'
     return [line.rstrip('\n') for line in open(name)]
 
-  def gen_project(name, root):
+  def gen_project(self, name, root):
     p = os.path.join(root, '.project')
     with open(p, 'w') as fd:
       print("""\
@@ -159,10 +159,19 @@
     doc = make_classpath()
     src = set()
     lib = set()
+    rule_jvm_externals = set()
 
+    rule_jvm_external = re.compile('bazel-out/.*?-fastbuild/bin/external/maven/(.*[.]jar)$')
     java_library = re.compile('bazel-out/(?:.*)-fastbuild/bin(.*)/[^/]+[.]jar$')
     srcs = re.compile('(.*/external/[^/]+)/jar/(.*)[.]jar')
     for p in self._query_classpath():
+
+      m = rule_jvm_external.match(p)
+      if m and ext is not None:
+        p = os.path.join(ext, 'external/unpinned_maven', m.group(1))
+        rule_jvm_externals.add(p)
+        continue
+
       m = java_library.match(p)
       if m:
         src.add(m.group(1).lstrip('/'))
@@ -216,6 +225,10 @@
             s = p
         classpathentry('lib', j, s)
 
+    for r in sorted(rule_jvm_externals):
+      s = r.replace('.jar', '-sources.jar')
+      classpathentry('lib', r, s)
+
     classpathentry('con', JRE)
     classpathentry('output', 'eclipse-out/classes')
 
diff --git a/tools/eclipse/test_project.py b/tools/eclipse/test_project.py
index be73474..4fba1a1 100755
--- a/tools/eclipse/test_project.py
+++ b/tools/eclipse/test_project.py
@@ -11,7 +11,7 @@
 
 from io import StringIO
 import unittest
-from unittest.mock import call, patch
+from unittest.mock import call, mock_open, patch
 from subprocess import CalledProcessError
 
 import project
@@ -38,7 +38,7 @@
         project.main()
 
     self.assertIn('Interrupted by user\n', stderr.getvalue())
-    self.assertEquals(c.exception.code, 1)
+    self.assertEqual(c.exception.code, 1)
 
   @patch('sys.stderr', new_callable=StringIO)
   def test_requires_root_option(self, stderr):
@@ -55,10 +55,10 @@
       ep = EclipseProject()
       ep.parse_args(['-r', '/dev/null'])
       ep.bazel_exe = 'my_bazel'
-      self.assertEquals(ep._build_bazel_cmd(), ['my_bazel'])
+      self.assertEqual(ep._build_bazel_cmd(), ['my_bazel'])
 
       ep.parse_args(['-r', '/dev/null', '--batch'])
-      self.assertEquals(ep._build_bazel_cmd(), ['my_bazel', '--batch'])
+      self.assertEqual(ep._build_bazel_cmd(), ['my_bazel', '--batch'])
 
   def test_find_root_raises_when_no_WORKSPACE_found(self):
     with patch('os.path.exists') as exists:
@@ -127,6 +127,80 @@
     ep.parse_args(['-r' '/dev/null', '--bazel', 'my_bazel'])
     assert not ep.retrieve_ext_location().endswith('\n')
 
+class GenClassPathTestCase(unittest.TestCase):
+
+  maxDiff = None
+
+  def gen_classpath(self, classpath):
+    ep = EclipseProject()
+    ep.parse_args(['-r', '/dev/null'])
+    ep.ROOT = '/path/to'
+
+    with patch.object(ep, '_query_classpath', return_value=[classpath]):
+      opener = mock_open()
+      with patch('builtins.open', opener):
+        ep.gen_classpath(ext='ext_loc')
+
+    written = ''
+    for write_call in opener().write.mock_calls:
+      written += write_call[1][0]
+
+    return written
+
+  def test_includes_external_gerrit_plugin_api(self):
+    self.assertIn(
+      ('<classpathentry kind="lib" '
+       'path="ext_loc/external/gerrit_plugin_api/jar/gerrit-plugin-api-3.4.0.jar"'
+      ),
+      # TODO we should check the sourcepath has been detected by mocking os.path.exists
+      self.gen_classpath(
+        'external/gerrit_plugin_api/jar/gerrit-plugin-api-3.4.0.jar'
+        ),
+      msg='plugin code is included'
+      )
+
+  def test_recognizes_maven_jar_dependencies(self):
+    self.assertIn(
+      ('<classpathentry kind="lib" '
+        'path="ext_loc/external/gerrit_plugin_api/jar/'
+        'gerrit-plugin-api-X.X.X.jar"/>'),
+      self.gen_classpath(
+        'external/gerrit_plugin_api/jar/gerrit-plugin-api-X.X.X.jar',
+        ),
+      msg='maven_jar() dependency listed as a "lib" classpathentry'
+      )
+
+  def test_finds_rules_jvm_external_dependencies(self):
+    self.assertIn(
+      ('<classpathentry kind="lib" '
+       'path="ext_loc/external/unpinned_maven/v1/https/repo1.maven.org'
+       '/maven2/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar" '
+       'sourcepath="ext_loc/external/unpinned_maven/v1/https/repo1.maven.org'
+       '/maven2/com/fasterxml/classmate/1.5.1/classmate-1.5.1-sources.jar"/>'),
+      self.gen_classpath(
+        ('bazel-out/k8-fastbuild/bin/external/maven/v1/'
+         'https/repo1.maven.org/maven2/'
+         'com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar')
+      ),
+      msg='artifact on Maven Central'
+    )
+
+  def test_finds_rules_jvm_external_dependencies_out_of_maven_central(self):
+    self.assertIn(
+      ('<classpathentry kind="lib" '
+       'path="ext_loc/external/unpinned_maven/v1/https/archiva.wikimedia.org'
+       '/repository/releases/org/wikimedia/eventutilities/1.1.0/eventutilities-1.1.0.jar" '
+       'sourcepath="ext_loc/external/unpinned_maven/v1/https/archiva.wikimedia.org'
+       '/repository/releases/org/wikimedia/eventutilities/1.1.0/eventutilities-1.1.0-sources.jar"/>'
+      ),
+      self.gen_classpath(
+        ('bazel-out/k8-fastbuild/bin/external/maven/v1/'
+         'https/archiva.wikimedia.org/repository/releases/'
+         'org/wikimedia/eventutilities/1.1.0/eventutilities-1.1.0.jar')
+      ),
+      msg='artifact hosted outside of Maven Central'
+    )
+
 
 if __name__ == "__main__":
   unittest.main(verbosity=2)