1 /*
   2  * Copyright (c) 2018, 2019, 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 package compiler.graalunit.common;
  25 
  26 import java.io.File;
  27 import java.io.IOException;
  28 import java.util.*;
  29 import java.nio.file.*;
  30 import java.util.stream.Collectors;
  31 
  32 import jdk.test.lib.process.OutputAnalyzer;
  33 import jdk.test.lib.process.ProcessTools;
  34 import jdk.test.lib.JDKToolFinder;
  35 
  36 /*
  37  * This is helper class used to run Graal unit tests.
  38  * It accepts two arguments:
  39  *  -prefix TEST_PREFIX_TO_DEFINE_SET_OF_TESTS_TO_RUN (Ex: -prefix org.graalvm.compiler.api.test)
  40  *  -exclude EXCLUDED_TESTS_FILE_NAME
  41  */
  42 public class GraalUnitTestLauncher {
  43 
  44     static final String MXTOOL_JARFILE = "com.oracle.mxtool.junit.jar";
  45     static final String GRAAL_UNITTESTS_JARFILE = "jdk.vm.compiler.tests.jar";
  46 
  47     static final String[] GRAAL_EXTRA_JARS = {"junit-4.12.jar", "asm-5.0.4.jar", "asm-tree-5.0.4.jar",
  48                                               "hamcrest-core-1.3.jar", "java-allocation-instrumenter.jar"};
  49 
  50     static final String GENERATED_TESTCLASSES_FILENAME = "list.testclasses";
  51 
  52     // Library dir used to find Graal specific jar files.
  53     static String libsDir;
  54     static {
  55         libsDir = System.getProperty("graalunit.libs");
  56         if (libsDir == null || libsDir.isEmpty()) {
  57             libsDir = System.getenv("TEST_IMAGE_GRAAL_DIR");
  58         }
  59 
  60         if (libsDir == null || libsDir.isEmpty())
  61             throw new RuntimeException("ERROR: Graal library directory is not specified, use -Dgraalunit.libs or TEST_IMAGE_GRAAL_DIR environment variable.");
  62 
  63         System.out.println("INFO: graal libs dir is '" + libsDir + "'");
  64     }
  65 
  66     /*
  67      * Generates --add-exports <module>/<package>=<target-module> flags and
  68      * returns them as array list.
  69      *
  70      * @param moduleName
  71      *        Name of the module to update export data
  72      *
  73      * @param targetModule
  74      *        Name of the module to whom to export
  75      */
  76     static ArrayList<String> getModuleExports(String moduleName, String targetModule) {
  77         ArrayList<String> exports = new ArrayList<String>();
  78 
  79         Optional<Module> mod = ModuleLayer.boot().findModule(moduleName);
  80         Set<String> packages;
  81         if (mod.isPresent()) {
  82             packages = mod.get().getPackages();
  83 
  84             for (String pName : packages) {
  85                 exports.add("--add-exports");
  86                 exports.add(moduleName + "/" + pName + "=" + targetModule);
  87             }
  88         }
  89 
  90         return exports;
  91     }
  92 
  93     /*
  94      * Return list of tests which match specified prefix
  95      *
  96      * @param testPrefix
  97      *        String prefix to select tests
  98      */
  99     static ArrayList<String> getListOfTestsByPrefix(String testPrefix, Set<String> excludeTests) throws Exception {
 100         ArrayList<String> classes = new ArrayList<String>();
 101 
 102         final String testAnnotationName = "@Test";
 103 
 104         // return empty list in case no selection prefix specified
 105         if (testPrefix == null || testPrefix.isEmpty())
 106             return classes;
 107 
 108         // replace "." by "\." in test pattern
 109         testPrefix = testPrefix.replaceAll("\\.", "\\\\.") + ".*";
 110         System.out.println("INFO: use following pattern to find tests: " + testPrefix);
 111 
 112         String graalUnitTestFilePath = String.join(File.separator, libsDir, GRAAL_UNITTESTS_JARFILE);
 113         String classPath = String.join(File.pathSeparator, System.getProperty("java.class.path"),
 114                 String.join(File.separator, libsDir, MXTOOL_JARFILE));
 115 
 116         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(false,
 117                 "-cp",  classPath,
 118                 "com.oracle.mxtool.junit.FindClassesByAnnotatedMethods", graalUnitTestFilePath, testAnnotationName);
 119 
 120         System.out.println("INFO: run command " + String.join(" ", pb.command()));
 121 
 122         OutputAnalyzer out = new OutputAnalyzer(pb.start());
 123         int exitCode = out.getExitValue();
 124         if (exitCode != 0) {
 125             throw new Exception("Failed to find tests, VM crashed with exit code " + exitCode);
 126         }
 127 
 128         String outStr = out.getOutput().trim();
 129         System.out.println("INFO: command output: [" + outStr + "]");
 130 
 131         String[] lines = outStr.split(" ");
 132         Arrays.sort(lines);
 133 
 134         if (lines.length > 1) { // first line contains jar file name
 135             for (int i = 1; i < lines.length; i++) {
 136                 String className = lines[i];
 137 
 138                 if (testPrefix.equals(".*") || className.matches(testPrefix)) {
 139                     // add the test only in case it is not in exclude list
 140                     if (excludeTests!= null && excludeTests.contains(className)) {
 141                         System.out.println("INFO: excluded test: " + className);
 142                     } else {
 143                         classes.add(className);
 144                     }
 145                 }
 146             }
 147         }
 148 
 149         return classes;
 150     }
 151 
 152     /*
 153      * Return set of excluded tests
 154      *
 155      * @param excludeFileName
 156      *        Name of the file to read excluded test list
 157      */
 158     static Set loadExcludeList(String excludeFileName) {
 159         Set<String> excludeTests;
 160 
 161         Path excludeFilePath = Paths.get(excludeFileName);
 162         try {
 163             excludeTests = Files.readAllLines(excludeFilePath).stream()
 164                     .filter(l -> !l.trim().isEmpty())
 165                     .filter(l -> !l.trim().startsWith("#"))
 166                     .map(l -> l.split(" ")[0])
 167                     .collect(Collectors.toSet());
 168 
 169         } catch (IOException ioe) {
 170             throw new Error("TESTBUG: failed to read " + excludeFilePath);
 171         }
 172 
 173         return excludeTests;
 174     }
 175 
 176     static String getUsageString() {
 177         return "Usage: " + GraalUnitTestLauncher.class.getName() + " " +
 178                 "-prefix (org.graalvm.compiler.api.test) " +
 179                 "-exclude <ExcludedTestsFileName>" + System.lineSeparator();
 180     }
 181 
 182     public static void main(String... args) throws Exception {
 183 
 184         String testPrefix = null;
 185         String excludeFileName = null;
 186 
 187         int i=0;
 188         String arg, val;
 189         while (i+1 < args.length) {
 190             arg = args[i++];
 191             val = args[i++];
 192 
 193             switch (arg) {
 194                 case "-prefix":
 195                     testPrefix = val;
 196                     break;
 197 
 198                 case "-exclude":
 199                     excludeFileName = val;
 200                     break;
 201 
 202                 default:
 203                     System.out.println("WARN: illegal option " + arg);
 204                     break;
 205             }
 206         }
 207 
 208         if (testPrefix == null)
 209             throw new Error("TESTBUG: no tests to run specified." + System.lineSeparator() + getUsageString());
 210 
 211 
 212         Set<String> excludeTests = null;
 213         if (excludeFileName != null) {
 214             excludeTests = loadExcludeList(excludeFileName);
 215         }
 216 
 217         // Find list of tests which match provided predicate and write into GENERATED_TESTCLASSES_FILENAME file
 218         ArrayList<String> tests = getListOfTestsByPrefix(testPrefix, excludeTests);
 219         if (tests.size() > 0) {
 220             Files.write(Paths.get(GENERATED_TESTCLASSES_FILENAME), String.join(System.lineSeparator(), tests).getBytes());
 221         } else {
 222             throw new Error("TESTBUG: no tests found for prefix " + testPrefix);
 223         }
 224 
 225         ArrayList<String> javaFlags = new ArrayList<String>();
 226 
 227         // add modules and exports
 228         javaFlags.add("--add-modules");
 229         javaFlags.add("jdk.internal.vm.compiler,jdk.internal.vm.ci");
 230         javaFlags.add("--add-exports");
 231         javaFlags.add("java.base/jdk.internal.module=ALL-UNNAMED");
 232         javaFlags.addAll(getModuleExports("jdk.internal.vm.compiler", "ALL-UNNAMED"));
 233         javaFlags.addAll(getModuleExports("jdk.internal.vm.ci", "ALL-UNNAMED,jdk.internal.vm.compiler"));
 234 
 235 
 236         // add VM flags
 237         javaFlags.add("-XX:+UnlockExperimentalVMOptions");
 238         javaFlags.add("-XX:+EnableJVMCI");
 239         javaFlags.add("-Djava.awt.headless=true");
 240         javaFlags.add("-esa");
 241         javaFlags.add("-ea");
 242         // Make sure exception message is never null
 243         javaFlags.add("-XX:-OmitStackTraceInFastThrow");
 244         // set timeout factor based on jtreg harness settings
 245         javaFlags.add("-Dgraaltest.timeout.factor=" + System.getProperty("test.timeout.factor", "1.0"));
 246 
 247         // generate class path
 248         ArrayList<String> graalJars = new ArrayList<String>(Arrays.asList(GRAAL_EXTRA_JARS));
 249         graalJars.add(MXTOOL_JARFILE);
 250         graalJars.add(GRAAL_UNITTESTS_JARFILE);
 251 
 252         String graalJarsCP = graalJars.stream()
 253                                       .map(s -> String.join(File.separator, libsDir, s))
 254                                       .collect(Collectors.joining(File.pathSeparator));
 255 
 256         javaFlags.add("-cp");
 257         // Existing classpath returned by System.getProperty("java.class.path") may contain another
 258         // version of junit with which the jtreg tool is built. It may be incompatible with required
 259         // junit version. So we put graalJarsCP before existing classpath when generating a new one
 260         // to avoid incompatibility issues.
 261         javaFlags.add(String.join(File.pathSeparator, graalJarsCP, System.getProperty("java.class.path")));
 262 
 263         //
 264         javaFlags.add("com.oracle.mxtool.junit.MxJUnitWrapper");
 265         javaFlags.add("-JUnitVerbose");
 266         javaFlags.add("-JUnitEagerStackTrace");
 267         javaFlags.add("-JUnitEnableTiming");
 268 
 269         javaFlags.add("@"+GENERATED_TESTCLASSES_FILENAME);
 270 
 271         ProcessBuilder javaPB = ProcessTools.createJavaProcessBuilder(true,
 272                 javaFlags.toArray(new String[javaFlags.size()]));
 273 
 274         // Some tests rely on MX_SUBPROCESS_COMMAND_FILE env variable which contains
 275         // name of the file with java executable and java args used to launch the current process.
 276         Path cmdFile = Files.createTempFile(Path.of(""), "mx_subprocess_", ".cmd");
 277         Files.write(cmdFile, javaPB.command());
 278         javaPB.environment().put("MX_SUBPROCESS_COMMAND_FILE", cmdFile.toAbsolutePath().toString());
 279 
 280         System.out.println("INFO: run command: " + String.join(" ", javaPB.command()));
 281 
 282         OutputAnalyzer outputAnalyzer = new OutputAnalyzer(javaPB.start());
 283         System.out.println("INFO: execution result: " + outputAnalyzer.getOutput());
 284         outputAnalyzer.shouldHaveExitValue(0);
 285     }
 286 }