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                 TKit.deleteDirectoryRecursive(workDir);
 240             }
 241 
 242             TKit.log(String.format("%s %s; checks=%d", status, fullName,
 243                     assertCount));
 244         }
 245     }
 246 
 247     private static Class enclosingMainMethodClass() {
 248         StackTraceElement st[] = Thread.currentThread().getStackTrace();
 249         for (StackTraceElement ste : st) {
 250             if ("main".equals(ste.getMethodName())) {
 251                 return Functional.ThrowingSupplier.toSupplier(() -> Class.forName(
 252                         ste.getClassName())).get();
 253             }
 254         }
 255         return null;
 256     }
 257 
 258     private static boolean isCalledByJavatest() {
 259         StackTraceElement st[] = Thread.currentThread().getStackTrace();
 260         for (StackTraceElement ste : st) {
 261             if (ste.getClassName().startsWith("com.sun.javatest.")) {
 262                 return true;
 263             }
 264         }
 265         return false;
 266     }
 267 
 268     private static Path createWorkDirName(TestDesc testDesc) {
 269         Path result = Path.of(".");
 270         if (!isCalledByJavatest()) {
 271             result = result.resolve(testDesc.clazz.getSimpleName());
 272         }
 273 
 274         List<String> components = new ArrayList<>();
 275 
 276         final String testFunctionName = testDesc.functionName;
 277         if (testFunctionName != null) {
 278             components.add(testFunctionName);
 279         }
 280 
 281         final boolean isPrametrized = Stream.of(testDesc.functionArgs,
 282                 testDesc.instanceArgs).anyMatch(Objects::nonNull);
 283         if (isPrametrized) {
 284             components.add(String.format("%08x", testDesc.testFullName().hashCode()));
 285         }
 286 
 287         if (!components.isEmpty()) {
 288             result = result.resolve(String.join(".", components));
 289         }
 290 
 291         return result;
 292     }
 293 
 294     private enum Status {
 295         Passed("[       OK ]"),
 296         Failed("[  FAILED  ]"),
 297         Skipped("[  SKIPPED ]");
 298 
 299         Status(String msg) {
 300             this.msg = msg;
 301         }
 302 
 303         @Override
 304         public String toString() {
 305             return msg;
 306         }
 307 
 308         private final String msg;
 309     }
 310 
 311     private int assertCount;
 312     private Status status;
 313     private RuntimeException skippedTestException;
 314     private final TestDesc testDesc;
 315     private final ThrowingFunction testConstructor;
 316     private final ThrowingConsumer testBody;
 317     private final List<ThrowingConsumer> beforeActions;
 318     private final List<ThrowingConsumer> afterActions;
 319     private final boolean dryRun;
 320     private final Path workDir;
 321 
 322     private final static Set<Status> KEEP_WORK_DIR = Functional.identity(
 323             () -> {
 324                 final String propertyName = "keep-work-dir";
 325                 Set<String> keepWorkDir = TKit.tokenizeConfigProperty(
 326                         propertyName);
 327                 if (keepWorkDir == null) {
 328                     return Set.of(Status.Failed);
 329                 }
 330 
 331                 Predicate<Set<String>> isOneOf = options -> {
 332                     return !Collections.disjoint(keepWorkDir, options);
 333                 };
 334 
 335                 Set<Status> result = new HashSet<>();
 336                 if (isOneOf.test(Set.of("pass", "p"))) {
 337                     result.add(Status.Passed);
 338                 }
 339                 if (isOneOf.test(Set.of("fail", "f"))) {
 340                     result.add(Status.Failed);
 341                 }
 342 
 343                 return Collections.unmodifiableSet(result);
 344             }).get();
 345 
 346 }