diff --git a/google-cloud-testing/google-cloud-conformance-tests/pom.xml b/google-cloud-testing/google-cloud-conformance-tests/pom.xml
new file mode 100644
index 000000000000..1e21f5ccbd5a
--- /dev/null
+++ b/google-cloud-testing/google-cloud-conformance-tests/pom.xml
@@ -0,0 +1,129 @@
+
+
+ 4.0.0
+ google-cloud-conformance-tests
+ 0.1.0-SNAPSHOT
+ jar
+
+
+ com.google.cloud
+ google-cloud-testing
+ 0.98.1-alpha-SNAPSHOT
+
+
+
+ 3.7.1
+
+
+
+
+ junit
+ junit
+ test
+
+
+ com.google.truth
+ truth
+
+
+ com.google.api.grpc
+ proto-google-cloud-firestore-v1
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.0.0
+
+
+ add-main-resource
+ generate-resources
+
+ add-resource
+
+
+
+
+ src/main/proto
+
+
+
+
+
+
+
+
+
+
+
+ gen-conformance-protos
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+
+ generate-main
+
+ compile
+
+ generate-sources
+
+
+ generate-test
+
+ test-compile
+
+ generate-test-sources
+
+
+
+ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+
+
+
+ com.coveo
+ fmt-maven-plugin
+ 2.9
+
+
+ format-main
+
+ format
+
+ process-sources
+
+
+ format-test
+
+ format
+
+ process-test-sources
+
+
+
+
+ target/generated-sources/protobuf/java
+
+
+
+
+
+
+
+
+
diff --git a/google-cloud-testing/google-cloud-conformance-tests/src/main/java/com/google/cloud/conformance/ConformanceTestLocator.java b/google-cloud-testing/google-cloud-conformance-tests/src/main/java/com/google/cloud/conformance/ConformanceTestLocator.java
new file mode 100644
index 000000000000..ab3df337670d
--- /dev/null
+++ b/google-cloud-testing/google-cloud-conformance-tests/src/main/java/com/google/cloud/conformance/ConformanceTestLocator.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.
+ */
+
+package com.google.cloud.conformance;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+public final class ConformanceTestLocator {
+
+ private ConformanceTestLocator() {}
+
+ /**
+ * Given the provided {@link MatchPattern matchPattern} list resources on the classpath starting
+ * from {@link MatchPattern#getBaseResourcePath() MatchPattern.getBaseResourcePath} and for each
+ * resources resource test that it is a file and that matches according to {@link
+ * MatchPattern#matches(String) MatchPattern.matches(String)}.
+ *
+ * Resolution of resources paths will be against the current threads context class loader
+ * ({@link Thread#currentThread()}.{@link Thread#getContextClassLoader() getContextClassLoader()}
+ *
+ * @param matchPattern The {@link MatchPattern} to match against
+ * @return The list of all resources on the classpath that match the specified {@code
+ * matchPattern}
+ * @throws IOException If there is an error attempting to read a classpath resource (listing the
+ * contents of a directory or jar).
+ * @throws URISyntaxException If there is an error translating a classpath URL into a filesystem
+ * URI.
+ */
+ public static List findAllResourcePaths(final MatchPattern matchPattern)
+ throws IOException, URISyntaxException {
+ return findAllResourcePaths(matchPattern, Thread.currentThread().getContextClassLoader());
+ }
+
+ /**
+ * Given the provided {@link MatchPattern matchPattern} list resources on the classpath starting
+ * from {@link MatchPattern#getBaseResourcePath() MatchPattern.getBaseResourcePath} and for each
+ * resources resource test that it is a file and that matches according to {@link
+ * MatchPattern#matches(String) MatchPattern.matches(String)}.
+ *
+ * Resolution of resources paths will be against the parameter {@code classLoader}
+ *
+ * @param matchPattern The {@link MatchPattern} to match against
+ * @param classLoader The classLoader to scan for resources
+ * @return The list of all resources on the classpath that match the specified {@code
+ * matchPattern}
+ * @throws IOException If there is an error attempting to read a classpath resource (listing the
+ * contents of a directory or jar).
+ * @throws URISyntaxException If there is an error translating a classpath URL into a filesystem
+ * URI.
+ */
+ public static List findAllResourcePaths(
+ final MatchPattern matchPattern, ClassLoader classLoader)
+ throws IOException, URISyntaxException {
+ final List resourcePaths = new ArrayList<>();
+ final Enumeration pkgDir = classLoader.getResources(matchPattern.getBaseResourcePath());
+ while (pkgDir.hasMoreElements()) {
+ URL url = pkgDir.nextElement();
+ if (url != null) {
+ final String scheme = url.getProtocol();
+ switch (scheme) {
+ case "file":
+ final List cf = handleFileScheme(url, matchPattern);
+ resourcePaths.addAll(cf);
+ break;
+ case "jar":
+ final List cj = handleJarScheme(url, matchPattern);
+ resourcePaths.addAll(cj);
+ break;
+ default:
+ throw new IllegalStateException("Unable to scan scheme '" + scheme + "'");
+ }
+ }
+ }
+
+ return resourcePaths;
+ }
+
+ private static List handleFileScheme(final URL url, MatchPattern mp)
+ throws IOException, URISyntaxException {
+ final Path path = Paths.get(url.toURI());
+ return handleFileScheme(mp, path);
+ }
+
+ private static List handleFileScheme(final MatchPattern mp, final Path path)
+ throws IOException {
+ final List resourcePaths = new ArrayList<>();
+ try (final DirectoryStream paths = Files.newDirectoryStream(path)) {
+ for (Path p : paths) {
+ if (Files.isDirectory(p)) {
+ resourcePaths.addAll(handleFileScheme(mp, p));
+ } else {
+ final String filePath = normalizePath(p);
+ final String resourcePath =
+ filePath.substring(filePath.indexOf(mp.getBaseResourcePath()));
+ if (mp.matches(resourcePath)) {
+ resourcePaths.add(resourcePath);
+ }
+ }
+ }
+ }
+ return resourcePaths;
+ }
+
+ private static List handleJarScheme(final URL url, MatchPattern mp) throws IOException {
+ final String urlPath = url.getPath();
+ final String jarPath = urlPath.substring(5, urlPath.indexOf("!"));
+
+ final List resourcePaths = new ArrayList<>();
+ final JarFile jarFile = new JarFile(jarPath);
+ final Enumeration jarEntries = jarFile.entries();
+ while (jarEntries.hasMoreElements()) {
+ JarEntry je = jarEntries.nextElement();
+ if (!je.isDirectory() && mp.matches(je.getName())) {
+ resourcePaths.add(je.getName());
+ }
+ }
+ return resourcePaths;
+ }
+
+ private static String normalizePath(Path p) {
+ final String s = p.normalize().toString();
+ if (File.separatorChar == '\\') {
+ return s.replace('\\', '/');
+ } else {
+ return s;
+ }
+ }
+
+ /**
+ * Factory method to create a relatively simple {@link MatchPattern}.
+ *
+ * @param baseResourcePath The non-null base path to start scanning for resources on the classpath
+ * @param suffix The non-null suffix to match found elements against for inclusion
+ * @return A new {@link MatchPattern} where classpath scanning will start from {@code
+ * baseResourcePath} walking the classpath and matching to elements that end in {@code
+ * suffix}. Suffix matching is a simple match (non-regex).
+ */
+ public static MatchPattern newMatchPattern(final String baseResourcePath, final String suffix) {
+ if (baseResourcePath == null) {
+ throw new IllegalArgumentException("baseResourcePath must be non-null");
+ }
+ if (suffix == null || suffix.isEmpty()) {
+ throw new IllegalArgumentException("suffix must be non-null and non-empty");
+ }
+ // when listing the resources from the classpath, a leading slash will result in resources
+ // not being found. If the baseResourcePath passed in here has a leading slash, detect and
+ // remove it.
+ int begin = 0;
+ if (baseResourcePath.startsWith("/")) {
+ begin = 1;
+ }
+ return new SimpleMatchPattern(baseResourcePath.substring(begin), suffix);
+ }
+
+ public interface MatchPattern {
+ String getBaseResourcePath();
+
+ boolean matches(String resourcePath);
+ }
+
+ private static final class SimpleMatchPattern implements MatchPattern {
+ private final String baseResourcePath;
+ private final String suffix;
+
+ SimpleMatchPattern(final String baseResourcePath, final String suffix) {
+ // parameter construction validation is performed in the factory method #newMatchPattern
+ this.baseResourcePath = baseResourcePath;
+ this.suffix = suffix;
+ }
+
+ @Override
+ public String getBaseResourcePath() {
+ return baseResourcePath;
+ }
+
+ @Override
+ public boolean matches(final String resourcePath) {
+ return resourcePath.startsWith(baseResourcePath) && resourcePath.endsWith(suffix);
+ }
+ }
+}
diff --git a/google-cloud-testing/google-cloud-conformance-tests/src/test/java/com/google/cloud/conformance/ConformanceTestLocatorTest.java b/google-cloud-testing/google-cloud-conformance-tests/src/test/java/com/google/cloud/conformance/ConformanceTestLocatorTest.java
new file mode 100644
index 000000000000..9e060cc7a27f
--- /dev/null
+++ b/google-cloud-testing/google-cloud-conformance-tests/src/test/java/com/google/cloud/conformance/ConformanceTestLocatorTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.
+ */
+
+package com.google.cloud.conformance;
+
+import static com.google.cloud.conformance.ConformanceTestLocator.newMatchPattern;
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Sets.newHashSet;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.conformance.ConformanceTestLocator.MatchPattern;
+import com.google.common.base.Joiner;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+
+public class ConformanceTestLocatorTest {
+
+ private static final Set ALL_RESOURCES_ORG_JUNIT_VALIDATOR =
+ newHashSet(
+ "org/junit/validator/AnnotationsValidator.class",
+ "org/junit/validator/AnnotationsValidator$1.class",
+ "org/junit/validator/AnnotationsValidator$AnnotatableValidator.class",
+ "org/junit/validator/AnnotationsValidator$ClassValidator.class",
+ "org/junit/validator/AnnotationsValidator$FieldValidator.class",
+ "org/junit/validator/AnnotationsValidator$MethodValidator.class",
+ "org/junit/validator/AnnotationValidator.class",
+ "org/junit/validator/AnnotationValidatorFactory.class",
+ "org/junit/validator/PublicClassValidator.class",
+ "org/junit/validator/TestClassValidator.class",
+ "org/junit/validator/ValidateWith.class");
+
+ private static final Set ALL_RESOURCES_JUNIT_RUNNER_GIF =
+ newHashSet(
+ "junit/runner/smalllogo.gif", // in junit:junit:4.12
+ "junit/runner/logo.gif", // in junit:junit:4.12
+ "junit/runner/next-2019-hashtag.gif" // in ../src/test/resources
+ );
+
+ @Test
+ public void load_all__org_junit_validator() throws IOException, URISyntaxException {
+ doTest(newMatchPattern("org/junit/validator", ".class"), ALL_RESOURCES_ORG_JUNIT_VALIDATOR);
+ }
+
+ @Test
+ public void load_all__load_across_dir_and_jars() throws IOException, URISyntaxException {
+ doTest(newMatchPattern("junit", ".gif"), ALL_RESOURCES_JUNIT_RUNNER_GIF);
+ }
+
+ @Test
+ public void newMatchPattern__validation_suffix__null() {
+ try {
+ newMatchPattern("something", null);
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageThat().isEqualTo("suffix must be non-null and non-empty");
+ }
+ }
+
+ @Test
+ public void newMatchPattern__validation_suffix__empty() {
+ try {
+ newMatchPattern("something", "");
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageThat().isEqualTo("suffix must be non-null and non-empty");
+ }
+ }
+
+ @Test
+ public void newMatchPattern__validation_baseResourcePath__null() {
+ try {
+ newMatchPattern(null, "*.class");
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageThat().isEqualTo("baseResourcePath must be non-null");
+ }
+ }
+
+ @Test
+ public void newMatchPattern__ensure_no_leading_slash() {
+ final MatchPattern matchPattern = newMatchPattern("/something", ".class");
+ assertThat(matchPattern.getBaseResourcePath()).isEqualTo("something");
+ }
+
+ private void doTest(final MatchPattern mp, Collection expectedResources)
+ throws IOException, URISyntaxException {
+ final List expected = newArrayList(expectedResources);
+
+ final List actual = ConformanceTestLocator.findAllResourcePaths(mp);
+
+ Collections.sort(expected);
+ Collections.sort(actual);
+
+ final Joiner joiner = Joiner.on("\n");
+ final String expectedS = joiner.join(expected);
+ final String actualS = joiner.join(actual);
+ assertEquals(expectedS, actualS);
+ }
+}
diff --git a/google-cloud-testing/google-cloud-conformance-tests/src/test/resources/junit/runner/next-2019-hashtag.gif b/google-cloud-testing/google-cloud-conformance-tests/src/test/resources/junit/runner/next-2019-hashtag.gif
new file mode 100644
index 000000000000..8c7f70b439a9
Binary files /dev/null and b/google-cloud-testing/google-cloud-conformance-tests/src/test/resources/junit/runner/next-2019-hashtag.gif differ
diff --git a/google-cloud-testing/google-cloud-conformance-tests/src/test/resources/junit/runner/next-2019-hashtag_readme.md b/google-cloud-testing/google-cloud-conformance-tests/src/test/resources/junit/runner/next-2019-hashtag_readme.md
new file mode 100644
index 000000000000..eb34c75d11c1
--- /dev/null
+++ b/google-cloud-testing/google-cloud-conformance-tests/src/test/resources/junit/runner/next-2019-hashtag_readme.md
@@ -0,0 +1,7 @@
+The resource `junit/runner/next-2019-hashtag.gif` is a resource used in the tests of
+`ConformanceTestLoader`. It is specifically defined here in `test/resources` to verify that
+all resources matching the pattern `junit/runner/*.gif` are returned from multiple classpath
+elements.
+
+Found on: https://events.google.com/io/ (2019-06-05 1555 EDT)
+Downloaded url: https://storage.googleapis.com/io-19-assets/images/global/hashtag.gif
diff --git a/google-cloud-testing/pom.xml b/google-cloud-testing/pom.xml
index 2ae4dae7b870..341c464a983d 100644
--- a/google-cloud-testing/pom.xml
+++ b/google-cloud-testing/pom.xml
@@ -26,6 +26,7 @@
google-cloud-managedtest
google-cloud-gcloud-maven-plugin
google-cloud-bigtable-emulator
+ google-cloud-conformance-tests