cfdc64fcb4
Reviewed-by: liach, redestad, vromero
454 lines
16 KiB
Java
454 lines
16 KiB
Java
/*
|
|
* Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import java.lang.classfile.*;
|
|
import java.lang.classfile.attribute.*;
|
|
import java.lang.classfile.constantpool.*;
|
|
|
|
import java.io.IOException;
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.lang.reflect.Method;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.regex.Pattern;
|
|
import java.util.stream.Collectors;
|
|
|
|
import toolbox.JavacTask;
|
|
import toolbox.Task;
|
|
import toolbox.ToolBox;
|
|
|
|
public class ModuleTestBase {
|
|
protected final ToolBox tb = new ToolBox();
|
|
private final TestResult tr = new TestResult();
|
|
|
|
|
|
protected void run() throws Exception {
|
|
boolean noTests = true;
|
|
for (Method method : this.getClass().getMethods()) {
|
|
if (method.isAnnotationPresent(Test.class)) {
|
|
noTests = false;
|
|
try {
|
|
tr.addTestCase(method.getName());
|
|
method.invoke(this, Paths.get(method.getName()));
|
|
} catch (Throwable th) {
|
|
tr.addFailure(th);
|
|
}
|
|
}
|
|
}
|
|
if (noTests) throw new AssertionError("Tests are not found.");
|
|
tr.checkStatus();
|
|
}
|
|
|
|
protected void testModuleAttribute(Path modulePath, ModuleDescriptor moduleDescriptor) throws Exception {
|
|
ClassModel classFile = ClassFile.of().parse(modulePath.resolve("module-info.class"));
|
|
ModuleAttribute moduleAttribute = classFile.findAttribute(Attributes.module()).orElse(null);
|
|
assert moduleAttribute != null;
|
|
testModuleName(moduleDescriptor, moduleAttribute);
|
|
testModuleFlags(moduleDescriptor, moduleAttribute);
|
|
testRequires(moduleDescriptor, moduleAttribute);
|
|
testExports(moduleDescriptor, moduleAttribute);
|
|
testOpens(moduleDescriptor, moduleAttribute);
|
|
testProvides(moduleDescriptor, moduleAttribute);
|
|
testUses(moduleDescriptor, moduleAttribute);
|
|
}
|
|
|
|
private void testModuleName(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
tr.checkEquals(module.moduleName().name().stringValue(), moduleDescriptor.name, "Unexpected module name");
|
|
}
|
|
|
|
private void testModuleFlags(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
tr.checkEquals(module.moduleFlagsMask(), moduleDescriptor.flags, "Unexpected module flags");
|
|
}
|
|
|
|
private void testRequires(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
tr.checkEquals(module.requires().size(), moduleDescriptor.requires.size(), "Wrong amount of requires.");
|
|
|
|
List<Requires> actualRequires = new ArrayList<>();
|
|
for (ModuleRequireInfo require : module.requires()) {
|
|
actualRequires.add(new Requires(
|
|
require.requires().name().stringValue(),
|
|
require.requiresFlagsMask()));
|
|
}
|
|
tr.checkContains(actualRequires, moduleDescriptor.requires, "Lists of requires don't match");
|
|
}
|
|
|
|
private void testExports(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
tr.checkEquals(module.exports().size(), moduleDescriptor.exports.size(), "Wrong amount of exports.");
|
|
for (ModuleExportInfo export : module.exports()) {
|
|
String pkg = export.exportedPackage().name().stringValue();
|
|
if (tr.checkTrue(moduleDescriptor.exports.containsKey(pkg), "Unexpected export " + pkg)) {
|
|
Export expectedExport = moduleDescriptor.exports.get(pkg);
|
|
tr.checkEquals(expectedExport.mask, export.exportsFlagsMask(), "Wrong export flags");
|
|
List<String> expectedTo = expectedExport.to;
|
|
tr.checkEquals(export.exportsTo().size(), expectedTo.size(), "Wrong amount of exports to");
|
|
List<String> actualTo = new ArrayList<>();
|
|
for (ModuleEntry toIdx : export.exportsTo()) {
|
|
actualTo.add(toIdx.name().stringValue());
|
|
}
|
|
tr.checkContains(actualTo, expectedTo, "Lists of \"exports to\" don't match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void testOpens(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
tr.checkEquals(module.opens().size(), moduleDescriptor.opens.size(), "Wrong amount of opens.");
|
|
for (ModuleOpenInfo open : module.opens()) {
|
|
String pkg = open.openedPackage().name().stringValue();
|
|
if (tr.checkTrue(moduleDescriptor.opens.containsKey(pkg), "Unexpected open " + pkg)) {
|
|
Open expectedOpen = moduleDescriptor.opens.get(pkg);
|
|
tr.checkEquals(expectedOpen.mask, open.opensFlagsMask(), "Wrong open flags");
|
|
List<String> expectedTo = expectedOpen.to;
|
|
tr.checkEquals(open.opensTo().size(), expectedTo.size(), "Wrong amount of opens to");
|
|
List<String> actualTo = new ArrayList<>();
|
|
for (ModuleEntry toIdx : open.opensTo()) {
|
|
actualTo.add(toIdx.name().stringValue());
|
|
}
|
|
tr.checkContains(actualTo, expectedTo, "Lists of \"opens to\" don't match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void testUses(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
tr.checkEquals(module.uses().size(), moduleDescriptor.uses.size(), "Wrong amount of uses.");
|
|
List<String> actualUses = new ArrayList<>();
|
|
for (ClassEntry usesIdx : module.uses()) {
|
|
if (!usesIdx.asSymbol().isClassOrInterface()) continue; //get basename
|
|
String uses = usesIdx.asInternalName();
|
|
actualUses.add(uses);
|
|
}
|
|
tr.checkContains(actualUses, moduleDescriptor.uses, "Lists of uses don't match");
|
|
}
|
|
|
|
private void testProvides(ModuleDescriptor moduleDescriptor, ModuleAttribute module) {
|
|
int moduleProvidesCount = module.provides().stream()
|
|
.mapToInt(e -> e.providesWith().size())
|
|
.sum();
|
|
int moduleDescriptorProvidesCount = moduleDescriptor.provides.values().stream()
|
|
.mapToInt(impls -> impls.size())
|
|
.sum();
|
|
tr.checkEquals(moduleProvidesCount, moduleDescriptorProvidesCount, "Wrong amount of provides.");
|
|
Map<String, List<String>> actualProvides = new HashMap<>();
|
|
for (ModuleProvideInfo provide : module.provides()) {
|
|
if (!provide.provides().asSymbol().isClassOrInterface()) continue;
|
|
String provides = provide.provides().asInternalName();
|
|
List<String> impls = new ArrayList<>();
|
|
for (ClassEntry withEntry: provide.providesWith()) {
|
|
if (!withEntry.asSymbol().isClassOrInterface()) continue;
|
|
String with = withEntry.asInternalName();
|
|
impls.add(with);
|
|
}
|
|
actualProvides.put(provides, impls);
|
|
}
|
|
tr.checkContains(actualProvides.entrySet(), moduleDescriptor.provides.entrySet(), "Lists of provides don't match");
|
|
}
|
|
|
|
protected void compile(Path base, String... options) throws IOException {
|
|
new JavacTask(tb)
|
|
.options(options)
|
|
.files(findJavaFiles(base))
|
|
.run(Task.Expect.SUCCESS)
|
|
.writeAll();
|
|
}
|
|
|
|
private static Path[] findJavaFiles(Path src) throws IOException {
|
|
return Files.find(src, Integer.MAX_VALUE, (path, attr) -> path.toString().endsWith(".java"))
|
|
.toArray(Path[]::new);
|
|
}
|
|
|
|
@Retention(RetentionPolicy.RUNTIME)
|
|
@interface Test {
|
|
}
|
|
|
|
interface Mask {
|
|
int getMask();
|
|
}
|
|
|
|
public enum ModuleFlag implements Mask {
|
|
OPEN("open", ClassFile.ACC_OPEN);
|
|
|
|
private final String token;
|
|
private final int mask;
|
|
|
|
ModuleFlag(String token, int mask) {
|
|
this.token = token;
|
|
this.mask = mask;
|
|
}
|
|
|
|
@Override
|
|
public int getMask() {
|
|
return mask;
|
|
}
|
|
}
|
|
|
|
public enum RequiresFlag implements Mask {
|
|
TRANSITIVE("transitive", ClassFile.ACC_TRANSITIVE),
|
|
STATIC("static", ClassFile.ACC_STATIC_PHASE),
|
|
MANDATED("", ClassFile.ACC_MANDATED);
|
|
|
|
private final String token;
|
|
private final int mask;
|
|
|
|
RequiresFlag(String token, int mask) {
|
|
this.token = token;
|
|
this.mask = mask;
|
|
}
|
|
|
|
@Override
|
|
public int getMask() {
|
|
return mask;
|
|
}
|
|
}
|
|
|
|
public enum ExportsFlag implements Mask {
|
|
SYNTHETIC("", ClassFile.ACC_SYNTHETIC);
|
|
|
|
private final String token;
|
|
private final int mask;
|
|
|
|
ExportsFlag(String token, int mask) {
|
|
this.token = token;
|
|
this.mask = mask;
|
|
}
|
|
|
|
@Override
|
|
public int getMask() {
|
|
return mask;
|
|
}
|
|
}
|
|
|
|
public enum OpensFlag implements Mask {
|
|
SYNTHETIC("", ClassFile.ACC_SYNTHETIC);
|
|
|
|
private final String token;
|
|
private final int mask;
|
|
|
|
OpensFlag(String token, int mask) {
|
|
this.token = token;
|
|
this.mask = mask;
|
|
}
|
|
|
|
@Override
|
|
public int getMask() {
|
|
return mask;
|
|
}
|
|
}
|
|
|
|
private class Export {
|
|
private final String pkg;
|
|
private final int mask;
|
|
private final List<String> to = new ArrayList<>();
|
|
|
|
Export(String pkg, int mask) {
|
|
this.pkg = pkg;
|
|
this.mask = mask;
|
|
}
|
|
}
|
|
|
|
private class Open {
|
|
private final String pkg;
|
|
private final int mask;
|
|
private final List<String> to = new ArrayList<>();
|
|
|
|
Open(String pkg, int mask) {
|
|
this.pkg = pkg;
|
|
this.mask = mask;
|
|
}
|
|
}
|
|
|
|
private class Requires {
|
|
private final String module;
|
|
private final int mask;
|
|
|
|
Requires(String module, int mask) {
|
|
this.module = module;
|
|
this.mask = mask;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
Requires requires = (Requires) o;
|
|
return mask == requires.mask &&
|
|
Objects.equals(module, requires.module);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(module, mask);
|
|
}
|
|
}
|
|
|
|
protected class ModuleDescriptor {
|
|
|
|
private final String name;
|
|
private final int flags;
|
|
|
|
private final List<Requires> requires = new ArrayList<>();
|
|
|
|
{
|
|
requires.add(new Requires("java.base", computeMask(RequiresFlag.MANDATED)));
|
|
}
|
|
|
|
private final Map<String, Export> exports = new HashMap<>();
|
|
private final Map<String, Open> opens = new HashMap<>();
|
|
|
|
//List of service and implementation
|
|
private final Map<String, List<String>> provides = new LinkedHashMap<>();
|
|
private final List<String> uses = new ArrayList<>();
|
|
|
|
private static final String LINE_END = ";\n";
|
|
|
|
StringBuilder content = new StringBuilder();
|
|
|
|
public ModuleDescriptor(String moduleName, ModuleFlag... flags) {
|
|
this.name = moduleName;
|
|
this.flags = computeMask(flags);
|
|
for (ModuleFlag flag : flags) {
|
|
content.append(flag.token).append(" ");
|
|
}
|
|
content.append("module ").append(moduleName).append('{').append('\n');
|
|
}
|
|
|
|
public ModuleDescriptor requires(String module) {
|
|
this.requires.add(new Requires(module, 0));
|
|
content.append(" requires ").append(module).append(LINE_END);
|
|
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor requires(String module, RequiresFlag... flags) {
|
|
this.requires.add(new Requires(module, computeMask(flags)));
|
|
|
|
content.append(" requires ");
|
|
for (RequiresFlag flag : flags) {
|
|
content.append(flag.token).append(" ");
|
|
}
|
|
content.append(module).append(LINE_END);
|
|
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor exports(String pkg, ExportsFlag... flags) {
|
|
this.exports.put(toInternalForm(pkg), new Export(toInternalForm(pkg), computeMask(flags)));
|
|
content.append(" exports ");
|
|
for (ExportsFlag flag : flags) {
|
|
content.append(flag.token).append(" ");
|
|
}
|
|
content.append(pkg).append(LINE_END);
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor exportsTo(String pkg, String to, ExportsFlag... flags) {
|
|
List<String> tos = Pattern.compile(",")
|
|
.splitAsStream(to)
|
|
.map(String::trim)
|
|
.toList();
|
|
this.exports.compute(toInternalForm(pkg), (k,v) -> new Export(k, computeMask(flags)))
|
|
.to.addAll(tos);
|
|
|
|
content.append(" exports ");
|
|
for (ExportsFlag flag : flags) {
|
|
content.append(flag.token).append(" ");
|
|
}
|
|
content.append(pkg).append(" to ").append(to).append(LINE_END);
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor opens(String pkg, OpensFlag... flags) {
|
|
this.opens.put(toInternalForm(pkg), new Open(toInternalForm(pkg), computeMask(flags)));
|
|
content.append(" opens ");
|
|
for (OpensFlag flag : flags) {
|
|
content.append(flag.token).append(" ");
|
|
}
|
|
content.append(pkg).append(LINE_END);
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor opensTo(String pkg, String to, OpensFlag... flags) {
|
|
List<String> tos = Pattern.compile(",")
|
|
.splitAsStream(to)
|
|
.map(String::trim)
|
|
.toList();
|
|
this.opens.compute(toInternalForm(pkg), (k,v) -> new Open(toInternalForm(k), computeMask(flags)))
|
|
.to.addAll(tos);
|
|
|
|
content.append(" opens ");
|
|
for (OpensFlag flag : flags) {
|
|
content.append(flag.token).append(" ");
|
|
}
|
|
content.append(pkg).append(" to ").append(to).append(LINE_END);
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor provides(String provides, String... with) {
|
|
List<String> impls = Arrays.stream(with)
|
|
.map(this::toInternalForm)
|
|
.collect(Collectors.toList());
|
|
this.provides.put(toInternalForm(provides), impls);
|
|
content.append(" provides ")
|
|
.append(provides)
|
|
.append(" with ")
|
|
.append(String.join(",", with))
|
|
.append(LINE_END);
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor uses(String... uses) {
|
|
for (String use : uses) {
|
|
this.uses.add(toInternalForm(use));
|
|
content.append(" uses ").append(use).append(LINE_END);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
public ModuleDescriptor write(Path path) throws IOException {
|
|
String src = content.append('}').toString();
|
|
|
|
tb.createDirectories(path);
|
|
tb.writeJavaFiles(path, src);
|
|
return this;
|
|
}
|
|
|
|
private String toInternalForm(String name) {
|
|
return name.replace('.', '/');
|
|
}
|
|
|
|
private int computeMask(Mask... masks) {
|
|
return Arrays.stream(masks)
|
|
.map(Mask::getMask)
|
|
.reduce((a, b) -> a | b)
|
|
.orElseGet(() -> 0);
|
|
}
|
|
}
|
|
}
|