1 /*
   2  * Copyright (c) 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 jdk.jpackage.test;
  25 
  26 import java.lang.reflect.Array;
  27 import java.lang.reflect.Method;
  28 import java.nio.file.Files;
  29 import java.nio.file.Path;
  30 import java.util.*;
  31 import java.util.function.Predicate;
  32 import java.util.function.Supplier;
  33 import java.util.stream.Collectors;
  34 import java.util.stream.Stream;
  35 import jdk.jpackage.test.Functional.ThrowingConsumer;
  36 import jdk.jpackage.test.Functional.ThrowingFunction;
  37 import jdk.jpackage.test.Functional.ThrowingRunnable;
  38 
  39 final class TestInstance implements ThrowingRunnable {
  40 
  41     static class TestDesc {
  42         private TestDesc() {
  43         }
  44 
  45         String testFullName() {
  46             StringBuilder sb = new StringBuilder();
  47             sb.append(clazz.getSimpleName());
  48             if (instanceArgs != null) {
  49                 sb.append('(').append(instanceArgs).append(')');
  50             }
  51             if (functionName != null) {
  52                 sb.append('.');
  53                 sb.append(functionName);
  54                 if (functionArgs != null) {
  55                     sb.append('(').append(functionArgs).append(')');
  56                 }
  57             }
  58             return sb.toString();
  59         }
  60 
  61         static Builder createBuilder() {
  62             return new Builder();
  63         }
  64 
  65         static final class Builder implements Supplier<TestDesc> {
  66             private Builder() {
  67             }
  68 
  69             Builder method(Method v) {
  70                 method = v;
  71                 return this;
  72             }
  73 
  74             Builder ctorArgs(Object... v) {
  75                 ctorArgs = ofNullable(v);
  76                 return this;
  77             }
  78 
  79             Builder methodArgs(Object... v) {
  80                 methodArgs = ofNullable(v);
  81                 return this;
  82             }
  83 
  84             @Override
  85             public TestDesc get() {
  86                 TestDesc desc = new TestDesc();
  87                 if (method == null) {
  88                     desc.clazz = enclosingMainMethodClass();
  89                 } else {
  90                     desc.clazz = method.getDeclaringClass();
  91                     desc.functionName = method.getName();
  92                     desc.functionArgs = formatArgs(methodArgs);
  93                     desc.instanceArgs = formatArgs(ctorArgs);
  94                 }
  95                 return desc;
  96             }
  97 
  98             private static String formatArgs(List<Object> values) {
  99                 if (values == null) {
 100                     return null;
 101                 }
 102                 return values.stream().map(v -> {
 103                     if (v != null && v.getClass().isArray()) {
 104                         return String.format("%s(length=%d)",
 105                                 Arrays.deepToString((Object[]) v),
 106                                 Array.getLength(v));
 107                     }
 108                     return String.format("%s", v);
 109                 }).collect(Collectors.joining(", "));
 110             }
 111 
 112             private static List<Object> ofNullable(Object... values) {
 113                 List<Object> result = new ArrayList();
 114                 for (var v: values) {
 115                     result.add(v);
 116                 }
 117                 return result;
 118             }
 119 
 120             private List<Object> ctorArgs;
 121             private List<Object> methodArgs;
 122             private Method method;
 123         }
 124 
 125         static TestDesc create(Method m, Object... args) {
 126             TestDesc desc = new TestDesc();
 127             desc.clazz = m.getDeclaringClass();
 128             desc.functionName = m.getName();
 129             if (args.length != 0) {
 130                 desc.functionArgs = Stream.of(args).map(v -> {
 131                     if (v.getClass().isArray()) {
 132                         return String.format("%s(length=%d)",
 133                                 Arrays.deepToString((Object[]) v),
 134                                 Array.getLength(v));
 135                     }
 136                     return String.format("%s", v);
 137                 }).collect(Collectors.joining(", "));
 138             }
 139             return desc;
 140         }
 141 
 142         private Class clazz;
 143         private String functionName;
 144         private String functionArgs;
 145         private String instanceArgs;
 146     }
 147 
 148     TestInstance(ThrowingRunnable testBody) {
 149         assertCount = 0;
 150         this.testConstructor = (unused) -> null;
 151         this.testBody = (unused) -> testBody.run();
 152         this.beforeActions = Collections.emptyList();
 153         this.afterActions = Collections.emptyList();
 154         this.testDesc = TestDesc.createBuilder().get();
 155         this.dryRun = false;
 156         this.workDir = createWorkDirName(testDesc);
 157     }
 158 
 159     TestInstance(MethodCall testBody, List<ThrowingConsumer> beforeActions,
 160             List<ThrowingConsumer> afterActions, boolean dryRun) {
 161         assertCount = 0;
 162         this.testConstructor = v -> ((MethodCall)v).newInstance();
 163         this.testBody = testBody;
 164         this.beforeActions = beforeActions;
 165         this.afterActions = afterActions;
 166         this.testDesc = testBody.createDescription();
 167         this.dryRun = dryRun;
 168         this.workDir = createWorkDirName(testDesc);
 169     }
 170 
 171     void notifyAssert() {
 172         assertCount++;
 173     }
 174 
 175     void notifySkipped(RuntimeException ex) {
 176         skippedTestException = ex;
 177     }
 178 
 179     boolean passed() {
 180         return status == Status.Passed;
 181     }
 182 
 183     boolean skipped() {
 184         return status == Status.Skipped;
 185     }
 186 
 187     boolean failed() {
 188         return status == Status.Failed;
 189     }
 190 
 191     String functionName() {
 192         return testDesc.functionName;
 193     }
 194 
 195     String baseName() {
 196         return testDesc.clazz.getSimpleName();
 197     }
 198 
 199     String fullName() {
 200         return testDesc.testFullName();
 201     }
 202 
 203     void rethrowIfSkipped() {
 204         if (skippedTestException != null) {
 205             throw skippedTestException;
 206         }
 207     }
 208 
 209     Path workDir() {
 210         return workDir;
 211     }
 212 
 213     @Override
 214     public void run() throws Throwable {
 215         final String fullName = fullName();
 216         TKit.log(String.format("[ RUN      ] %s", fullName));
 217         try {
 218             Object testInstance = testConstructor.apply(testBody);
 219             beforeActions.forEach(a -> ThrowingConsumer.toConsumer(a).accept(
 220                     testInstance));
 221             try {
 222                 if (!dryRun) {
 223                     Files.createDirectories(workDir);
 224                     testBody.accept(testInstance);
 225                 }
 226             } finally {
 227                 afterActions.forEach(a -> TKit.ignoreExceptions(() -> a.accept(
 228                         testInstance)));
 229             }
 230             status = Status.Passed;
 231         } finally {
 232             if (skippedTestException != null) {
 233                 status = Status.Skipped;
 234             } else if (status == null) {
 235                 status = Status.Failed;
 236             }
 237 
 238             if (!KEEP_WORK_DIR.contains(status)) {
 239                 if (Files.isSameFile(workDir, Path.of("."))) {
 240                     // 1. If the work directory is the current directory, don't
 241                     // delete it, just clean as deleting it would be confusing.
 242                     TKit.deleteDirectoryContentsRecursive(workDir);
 243                 } else {
 244                     TKit.deleteDirectoryRecursive(workDir);
 245                 }
 246             }
 247 
 248             TKit.log(String.format("%s %s; checks=%d", status, fullName,
 249                     assertCount));
 250         }
 251     }
 252 
 253     private static Class enclosingMainMethodClass() {
 254         StackTraceElement st[] = Thread.currentThread().getStackTrace();
 255         for (StackTraceElement ste : st) {
 256             if ("main".equals(ste.getMethodName())) {
 257                 return Functional.ThrowingSupplier.toSupplier(() -> Class.forName(
 258                         ste.getClassName())).get();
 259             }
 260         }
 261         return null;
 262     }
 263 
 264     private static boolean isCalledByJavatest() {
 265         StackTraceElement st[] = Thread.currentThread().getStackTrace();
 266         for (StackTraceElement ste : st) {
 267             if (ste.getClassName().startsWith("com.sun.javatest.")) {
 268                 return true;
 269             }
 270         }
 271         return false;
 272     }
 273 
 274     private static Path createWorkDirName(TestDesc testDesc) {
 275         Path result = Path.of(".");
 276         if (!isCalledByJavatest()) {
 277             result = result.resolve(testDesc.clazz.getSimpleName());
 278         }
 279 
 280         List<String> components = new ArrayList<>();
 281 
 282         final String testFunctionName = testDesc.functionName;
 283         if (testFunctionName != null) {
 284             components.add(testFunctionName);
 285         }
 286 
 287         final boolean isPrametrized = Stream.of(testDesc.functionArgs,
 288                 testDesc.instanceArgs).anyMatch(Objects::nonNull);
 289         if (isPrametrized) {
 290             components.add(String.format("%08x", testDesc.testFullName().hashCode()));
 291         }
 292 
 293         if (!components.isEmpty()) {
 294             result = result.resolve(String.join(".", components));
 295         }
 296 
 297         return result;
 298     }
 299 
 300     private enum Status {
 301         Passed("[       OK ]"),
 302         Failed("[  FAILED  ]"),
 303         Skipped("[  SKIPPED ]");
 304 
 305         Status(String msg) {
 306             this.msg = msg;
 307         }
 308 
 309         @Override
 310         public String toString() {
 311             return msg;
 312         }
 313 
 314         private final String msg;
 315     }
 316 
 317     private int assertCount;
 318     private Status status;
 319     private RuntimeException skippedTestException;
 320     private final TestDesc testDesc;
 321     private final ThrowingFunction testConstructor;
 322     private final ThrowingConsumer testBody;
 323     private final List<ThrowingConsumer> beforeActions;
 324     private final List<ThrowingConsumer> afterActions;
 325     private final boolean dryRun;
 326     private final Path workDir;
 327 
 328     private final static Set<Status> KEEP_WORK_DIR = Functional.identity(
 329             () -> {
 330                 final String propertyName = "keep-work-dir";
 331                 Set<String> keepWorkDir = TKit.tokenizeConfigProperty(
 332                         propertyName);
 333                 if (keepWorkDir == null) {
 334                     return Set.of(Status.Failed);
 335                 }
 336 
 337                 Predicate<Set<String>> isOneOf = options -> {
 338                     return !Collections.disjoint(keepWorkDir, options);
 339                 };
 340 
 341                 Set<Status> result = new HashSet<>();
 342                 if (isOneOf.test(Set.of("pass", "p"))) {
 343                     result.add(Status.Passed);
 344                 }
 345                 if (isOneOf.test(Set.of("fail", "f"))) {
 346                     result.add(Status.Failed);
 347                 }
 348 
 349                 return Collections.unmodifiableSet(result);
 350             }).get();
 351 
 352 }