在bazel java tests

时间:2017-04-30 18:14:00

标签: java maven bazel

我正在尝试将大型代码库从maven迁移到bazel,并且我发现某些测试写入target/classestarget/test-classes,并且生产代码将其读取为类路径上的资源。这是因为maven surefire / failsafe默认从模块目录运行,并将target/classestarget/test-classes添加到类路径中。 对于我来说,迁移这个大型代码库,唯一合理的解决方案是创建目标,目标/类和目标/测试类文件夹,并将最后两个添加到测试的类路径中。
关于如何实现这一点的任何想法?

由于

3 个答案:

答案 0 :(得分:1)

另一种方法。与其生成测试套件,不如创建一个自定义javaagent和一个自定义类加载器。使用jvm_flags进行设置和配置。

javaagent有一个premain方法。这听起来像是一个自然的地方,可以处理发生在常规main方法之前的事情,即使它们与类检测,调试,coverage收集或任何其他javaagents常规用法没有任何关系。

自定义javaagent读取系统属性extra.dirs并创建在那里指定的目录。

需要Classloader,这样我们才能在运行时修改classpath而不会受到黑客的攻击。很大的优势是该解决方案可在Java 10上运行。

自定义类加载器读取系统属性extra.class.path并(实际上)在java.class.path中的内容之前添加了它。

以这种方式处理意味着可以使用标准的bazel规则。

已构建

runtime_classgen_dirs = ":".join([
            "target/classes",
            "target/test-classes",
])
java_test(
    ...,
    jvm_flags = [
        # agent
        "-javaagent:$(location //tools:test-agent_deploy.jar)",
        "-Dextra.dirs=" + runtime_classgen_dirs,
        # classloader
        "-Djava.system.class.loader=ResourceJavaAgent",
        "-Dextra.class.path=" + runtime_classgen_dirs,
    ],
    ,,,,
    deps = [
        # not runtime_deps, cause https://github.com/bazelbuild/bazel/issues/1566
        "//tools:test-agent_deploy.jartest-agent_deploy.jar"
    ],
    ...,
)

工具/已构建

java_binary(
    name = "test-agent",
    testonly = True,
    srcs = ["ResourceJavaAgent.java"],
    deploy_manifest_lines = ["Premain-Class: ResourceJavaAgent"],
    main_class = "ResourceJavaAgent",
    visibility = ["//visibility:public"],
)

tools / ResourceJavaAgent.java

import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

// https://stackoverflow.com/questions/60764/how-should-i-load-jars-dynamically-at-runtime
public class ResourceJavaAgent extends URLClassLoader {
    private final ClassLoader parent;

    public ResourceJavaAgent(ClassLoader parent) throws MalformedURLException {
        super(buildClassPath(), null);
        this.parent = parent; // I need the parent as backup for SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        System.out.println("initializing url classloader");
    }

    private static URL[] buildClassPath() throws MalformedURLException {
        final String JAVA_CLASS_PATH = "java.class.path";
        final String EXTRA_CLASS_PATH = "extra.class.path";
        List<String> paths = new LinkedList<>();
        paths.addAll(Arrays.asList(System.getProperty(EXTRA_CLASS_PATH, "").split(File.pathSeparator)));
        paths.addAll(Arrays.asList(System.getProperty(JAVA_CLASS_PATH, "").split(File.pathSeparator)));
        URL[] urls = new URL[paths.size()];
        for (int i = 0; i < paths.size(); i++) {
            urls[i] = Paths.get(paths.get(i)).toUri().toURL(); // important only for resource url, really: this url must be absolute, to pass getClass().getResource("/users.properties").toURI()) with uri that isOpaque == false.
//            System.out.println(urls[i]);
        }
        // this is for spawnVM functionality in tests
        System.setProperty(JAVA_CLASS_PATH, System.getProperty(EXTRA_CLASS_PATH, "") + File.pathSeparator + System.getProperty(JAVA_CLASS_PATH));
        return urls;
    }

    @Override
    public Class<?> loadClass(String s) throws ClassNotFoundException {
        try {
            return super.loadClass(s);
        } catch (ClassNotFoundException e) {
            return parent.loadClass(s);  // we search parent second, not first, as the default URLClassLoader would
        }
    }

    private static void createRequestedDirs() {
        for (String path : System.getProperty("extra.dirs", "").split(File.pathSeparator)) {
            new File(path).mkdirs();
        }
    }

    private static void createRequestedLinks() {
        String linkPaths = System.getProperty("extra.link.path", null);
        if (linkPaths == null) {
            return;
        }
        for (String linkPath : linkPaths.split(",")) {
            String[] fromTo = linkPath.split(":");
            Path from = Paths.get(fromTo[0]);
            Path to = Paths.get(fromTo[1]);
            try {
                Files.createSymbolicLink(from.toAbsolutePath(), to.toAbsolutePath());
            } catch (IOException e) {
                throw new IllegalArgumentException("Unable to create link " + linkPath, e);
            }
        }
    }

    public static void premain(String args, Instrumentation instrumentation) throws Exception {
        createRequestedDirs();
        createRequestedLinks();
    }
}

答案 1 :(得分:0)

如果您可以告诉测试在哪里编写这些文件(如果target/classestarget/test-classes是硬编码的),然后将测试运行转换为genrule,那么您可以指定生成二进制文件data规则的genrule输出为*_binary

答案 2 :(得分:0)

我解决了第一部分,创建了目录。我仍然不知道如何将后两个添加到classpath。

https://gerrit.googlesource.com/bazlets/+/master/tools/junit.bzl开始,我将其修改为读取

_OUTPUT = """import org.junit.runners.Suite;
import org.junit.runner.RunWith;
import org.junit.BeforeClass;
import java.io.File;
@RunWith(Suite.class)
@Suite.SuiteClasses({%s})
public class %s {
    @BeforeClass
    public static void setUp() throws Exception {
      new File("./target").mkdir();
    }
}
"""
_PREFIXES = ("org", "com", "edu")
# ...

我添加了@BeforeClass setUp method

我将其作为junit.bzl存储到项目的third_party目录中。

然后进入BUILD file

load("//third_party:junit.bzl", "junit_tests")

junit_tests(
    name = "my_bundled_test",
    srcs = glob(["src/test/java/**/*.java"]),
    data = glob(["src/test/resources/**"]),
resources = glob(["src/test/resources/**"]),
tags = [
    # ...
],
    runtime_deps = [
        # ...
    ],
],
    deps = [
        # ...
    ],
)

现在,测试本身包含有一个setUp方法,该方法将为我创建一个目录。我以后不会删除它们,这可能是一个不错的主意。

之所以需要目录中的测试资源(而不是bazel默认提供的jar文件中的目录)是因为我的测试将URI传递给了new FileInputStream(new File(uri))。如果文件位于JAR中,则URI将为file:/path/to/my.jar!/my.file,而其余的测试无法使用该URI。