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 }