1 /*
   2  * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import com.sun.tools.classfile.ClassFile;
  25 import com.sun.tools.classfile.ConstantPool;
  26 import com.sun.tools.classfile.ConstantPoolException;
  27 import com.sun.tools.classfile.Module_attribute;
  28 import com.sun.tools.javac.util.Pair;
  29 
  30 import java.io.IOException;
  31 import java.lang.annotation.Retention;
  32 import java.lang.annotation.RetentionPolicy;
  33 import java.lang.reflect.Method;
  34 import java.nio.file.Files;
  35 import java.nio.file.Path;
  36 import java.nio.file.Paths;
  37 import java.util.ArrayList;
  38 import java.util.Arrays;
  39 import java.util.Collections;
  40 import java.util.HashMap;
  41 import java.util.LinkedHashMap;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.regex.Pattern;
  45 import java.util.stream.Collectors;
  46 
  47 import toolbox.JavacTask;
  48 import toolbox.Task;
  49 import toolbox.ToolBox;
  50 
  51 public class ModuleTestBase {
  52     protected final ToolBox tb = new ToolBox();
  53     private final TestResult tr = new TestResult();
  54 
  55 
  56     protected void run() throws Exception {
  57         boolean noTests = true;
  58         for (Method method : this.getClass().getMethods()) {
  59             if (method.isAnnotationPresent(Test.class)) {
  60                 noTests = false;
  61                 try {
  62                     tr.addTestCase(method.getName());
  63                     method.invoke(this, Paths.get(method.getName()));
  64                 } catch (Throwable th) {
  65                     tr.addFailure(th);
  66                 }
  67             }
  68         }
  69         if (noTests) throw new AssertionError("Tests are not found.");
  70         tr.checkStatus();
  71     }
  72 
  73     protected void testModuleAttribute(Path modulePath, ModuleDescriptor moduleDescriptor) throws Exception {
  74         ClassFile classFile = ClassFile.read(modulePath.resolve("module-info.class"));
  75         Module_attribute moduleAttribute = (Module_attribute) classFile.getAttribute("Module");
  76         ConstantPool constantPool = classFile.constant_pool;
  77 
  78         testRequires(moduleDescriptor, moduleAttribute, constantPool);
  79         testExports(moduleDescriptor, moduleAttribute, constantPool);
  80         testProvides(moduleDescriptor, moduleAttribute, constantPool);
  81         testUses(moduleDescriptor, moduleAttribute, constantPool);
  82     }
  83 
  84     private void testRequires(ModuleDescriptor moduleDescriptor, Module_attribute module, ConstantPool constantPool) throws ConstantPoolException {
  85         tr.checkEquals(module.requires_count, moduleDescriptor.requires.size(), "Wrong amount of requires.");
  86 
  87         List<Pair<String, Integer>> actualRequires = new ArrayList<>();
  88         for (Module_attribute.RequiresEntry require : module.requires) {
  89             actualRequires.add(Pair.of(
  90                     require.getRequires(constantPool).replace('/', '.'),
  91                     require.requires_flags));
  92         }
  93         tr.checkContains(actualRequires, moduleDescriptor.requires, "Lists of requires don't match");
  94     }
  95 
  96     private void testExports(ModuleDescriptor moduleDescriptor, Module_attribute module, ConstantPool constantPool) throws ConstantPool.InvalidIndex, ConstantPool.UnexpectedEntry {
  97         tr.checkEquals(module.exports_count, moduleDescriptor.exports.size(), "Wrong amount of exports.");
  98         for (Module_attribute.ExportsEntry export : module.exports) {
  99             String pkg = constantPool.getUTF8Value(export.exports_index);
 100             if (tr.checkTrue(moduleDescriptor.exports.containsKey(pkg), "Unexpected export " + pkg)) {
 101                 Export expectedExport = moduleDescriptor.exports.get(pkg);
 102                 tr.checkEquals(expectedExport.mask, export.exports_flags, "Wrong export flags");
 103                 List<String> expectedTo = expectedExport.to;
 104                 tr.checkEquals(export.exports_to_count, expectedTo.size(), "Wrong amount of exports to");
 105                 List<String> actualTo = new ArrayList<>();
 106                 for (int toIdx : export.exports_to_index) {
 107                     actualTo.add(constantPool.getUTF8Value(toIdx).replace('/', '.'));
 108                 }
 109                 tr.checkContains(actualTo, expectedTo, "Lists of \"exports to\" don't match.");
 110             }
 111         }
 112     }
 113 
 114     private void testUses(ModuleDescriptor moduleDescriptor, Module_attribute module, ConstantPool constantPool) throws ConstantPoolException {
 115         tr.checkEquals(module.uses_count, moduleDescriptor.uses.size(), "Wrong amount of uses.");
 116         List<String> actualUses = new ArrayList<>();
 117         for (int usesIdx : module.uses_index) {
 118             String uses = constantPool.getClassInfo(usesIdx).getBaseName().replace('/', '.');
 119             actualUses.add(uses);
 120         }
 121         tr.checkContains(actualUses, moduleDescriptor.uses, "Lists of uses don't match");
 122     }
 123 
 124     private void testProvides(ModuleDescriptor moduleDescriptor, Module_attribute module, ConstantPool constantPool) throws ConstantPoolException {
 125         int moduleProvidesCount = Arrays.asList(module.provides).stream()
 126                 .mapToInt(e -> e.with_index.length)
 127                 .sum();
 128         int moduleDescriptorProvidesCount = moduleDescriptor.provides.values().stream()
 129                 .mapToInt(impls -> impls.size())
 130                 .sum();
 131         tr.checkEquals(moduleProvidesCount, moduleDescriptorProvidesCount, "Wrong amount of provides.");
 132         Map<String, List<String>> actualProvides = new HashMap<>();
 133         for (Module_attribute.ProvidesEntry provide : module.provides) {
 134             String provides = constantPool.getClassInfo(provide.provides_index).getBaseName().replace('/', '.');
 135             List<String> impls = new ArrayList<>();
 136             for (int i = 0; i < provide.with_count; i++) {
 137                 String with = constantPool.getClassInfo(provide.with_index[i]).getBaseName().replace('/', '.');
 138                 impls.add(with);
 139             }
 140             actualProvides.put(provides, impls);
 141         }
 142         tr.checkContains(actualProvides.entrySet(), moduleDescriptor.provides.entrySet(), "Lists of provides don't match");
 143     }
 144 
 145     protected void compile(Path base, String... options) throws IOException {
 146         new JavacTask(tb)
 147                 .options(options)
 148                 .files(findJavaFiles(base))
 149                 .run(Task.Expect.SUCCESS)
 150                 .writeAll();
 151     }
 152 
 153     private static Path[] findJavaFiles(Path src) throws IOException {
 154         return Files.find(src, Integer.MAX_VALUE, (path, attr) -> path.toString().endsWith(".java"))
 155                 .toArray(Path[]::new);
 156     }
 157 
 158     @Retention(RetentionPolicy.RUNTIME)
 159     @interface Test {
 160     }
 161 
 162     interface Mask {
 163         int getMask();
 164     }
 165 
 166     public enum RequiresFlag implements Mask {
 167         TRANSITIVE("transitive", Module_attribute.ACC_TRANSITIVE),
 168         STATIC("static", Module_attribute.ACC_STATIC_PHASE);
 169 
 170         private final String token;
 171         private final int mask;
 172 
 173         RequiresFlag(String token, int mask) {
 174             this.token = token;
 175             this.mask = mask;
 176         }
 177 
 178         @Override
 179         public int getMask() {
 180             return mask;
 181         }
 182     }
 183 
 184     public enum ExportFlag implements Mask {
 185         SYNTHETIC("", Module_attribute.ACC_SYNTHETIC);
 186 
 187         private final String token;
 188         private final int mask;
 189 
 190         ExportFlag(String token, int mask) {
 191             this.token = token;
 192             this.mask = mask;
 193         }
 194 
 195         @Override
 196         public int getMask() {
 197             return mask;
 198         }
 199     }
 200 
 201     private class Export {
 202         String pkg;
 203         int mask;
 204         List<String> to = new ArrayList<>();
 205 
 206         public Export(String pkg, int mask) {
 207             this.pkg = pkg;
 208             this.mask = mask;
 209         }
 210     }
 211 
 212     protected class ModuleDescriptor {
 213 
 214         private final String name;
 215         //pair is name of module and flag(public,mandated,synthetic)
 216         private final List<Pair<String, Integer>> requires = new ArrayList<>();
 217 
 218         {
 219             requires.add(new Pair<>("java.base", Module_attribute.ACC_MANDATED));
 220         }
 221 
 222         private final Map<String, Export> exports = new HashMap<>();
 223 
 224         //List of service and implementation
 225         private final Map<String, List<String>> provides = new LinkedHashMap<>();
 226         private final List<String> uses = new ArrayList<>();
 227 
 228         private static final String LINE_END = ";\n";
 229 
 230         StringBuilder content = new StringBuilder("module ");
 231 
 232         public ModuleDescriptor(String moduleName) {
 233             this.name = moduleName;
 234             content.append(name).append('{').append('\n');
 235         }
 236 
 237         public ModuleDescriptor requires(String module) {
 238             this.requires.add(Pair.of(module, 0));
 239             content.append("    requires ").append(module).append(LINE_END);
 240 
 241             return this;
 242         }
 243 
 244         public ModuleDescriptor requires(String module, RequiresFlag... flags) {
 245             this.requires.add(new Pair<>(module, computeMask(flags)));
 246 
 247             content.append("    requires ");
 248             for (RequiresFlag flag : flags) {
 249                 content.append(flag.token).append(" ");
 250             }
 251             content.append(module).append(LINE_END);
 252 
 253             return this;
 254         }
 255 
 256         public ModuleDescriptor exports(String pkg, ExportFlag... flags) {
 257             this.exports.putIfAbsent(pkg, new Export(pkg, computeMask(flags)));
 258             content.append("    exports ");
 259             for (ExportFlag flag : flags) {
 260                 content.append(flag.token).append(" ");
 261             }
 262             content.append(pkg).append(LINE_END);
 263             return this;
 264         }
 265 
 266         public ModuleDescriptor exportsTo(String pkg, String to, ExportFlag... flags) {
 267             List<String> tos = Pattern.compile(",")
 268                     .splitAsStream(to)
 269                     .map(String::trim)
 270                     .collect(Collectors.toList());
 271             this.exports.computeIfAbsent(pkg, k -> new Export(pkg, computeMask(flags)))
 272                     .to.addAll(tos);
 273 
 274             content.append("    exports ");
 275             for (ExportFlag flag : flags) {
 276                 content.append(flag.token).append(" ");
 277             }
 278             content.append(pkg).append(" to ").append(to).append(LINE_END);
 279             return this;
 280         }
 281 
 282         public ModuleDescriptor provides(String provides, String... with) {
 283             this.provides.put(provides, Arrays.asList(with));
 284             content.append("    provides ")
 285                     .append(provides)
 286                     .append(" with ")
 287                     .append(String.join(",", with))
 288                     .append(LINE_END);
 289             return this;
 290         }
 291 
 292         public ModuleDescriptor uses(String... uses) {
 293             Collections.addAll(this.uses, uses);
 294             for (String use : uses) {
 295                 content.append("    uses ").append(use).append(LINE_END);
 296             }
 297             return this;
 298         }
 299 
 300         public ModuleDescriptor write(Path path) throws IOException {
 301             String src = content.append('}').toString();
 302 
 303             tb.createDirectories(path);
 304             tb.writeJavaFiles(path, src);
 305             return this;
 306         }
 307 
 308         private int computeMask(Mask[] masks) {
 309             return Arrays.stream(masks)
 310                     .map(Mask::getMask)
 311                     .reduce((a, b) -> a | b)
 312                     .orElseGet(() -> 0);
 313         }
 314     }
 315 }