1 /* 2 * Copyright (c) 2015, 2017, 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.test.lib.combo; 25 26 import com.sun.source.tree.CompilationUnitTree; 27 import com.sun.source.util.JavacTask; 28 import com.sun.source.util.TaskListener; 29 import com.sun.tools.javac.api.JavacTool; 30 import com.sun.tools.javac.util.Assert; 31 import com.sun.tools.javac.util.List; 32 import jdk.test.lib.combo.ComboParameter.Resolver; 33 34 import javax.lang.model.element.Element; 35 import javax.tools.Diagnostic; 36 import javax.tools.DiagnosticListener; 37 import javax.tools.JavaFileObject; 38 import javax.tools.SimpleJavaFileObject; 39 40 import java.io.IOException; 41 import java.io.Writer; 42 import java.net.URI; 43 import java.net.URL; 44 import java.net.URLClassLoader; 45 import java.util.ArrayList; 46 import java.util.function.Consumer; 47 import java.util.function.Function; 48 import java.util.HashMap; 49 import java.util.Map; 50 import java.util.Optional; 51 import java.util.stream.Collectors; 52 import java.util.stream.StreamSupport; 53 54 /** 55 * This class represents a compilation task associated with a combo test instance. This is a small 56 * wrapper around {@link JavacTask} which allows for fluent setup style and which makes use of 57 * the shared compilation context to speedup performances. 58 */ 59 public class ComboTask { 60 61 /** Sources to be compiled in this task. */ 62 private List<JavaFileObject> sources = List.nil(); 63 64 /** Options associated with this task. */ 65 private List<String> options = List.nil(); 66 67 /** Diagnostic collector. */ 68 private DiagnosticCollector diagsCollector = new DiagnosticCollector(); 69 70 /** Output writer. */ 71 private Writer out; 72 73 /** Listeners associated with this task. */ 74 private List<TaskListener> listeners = List.nil(); 75 76 /** Underlying javac task object. */ 77 private JavacTask task; 78 79 /** Combo execution environment. */ 80 private ComboTestHelper<?>.Env env; 81 82 ComboTask(ComboTestHelper<?>.Env env) { 83 this.env = env; 84 } 85 86 /** 87 * Add a new source to this task. 88 */ 89 public ComboTask withSource(JavaFileObject comboSource) { 90 sources = sources.prepend(comboSource); 91 return this; 92 } 93 94 /** 95 * Add a new template source with given name to this task; the template is replaced with 96 * corresponding combo parameters (as defined in the combo test environment). 97 */ 98 public ComboTask withSourceFromTemplate(String name, String template) { 99 return withSource(new ComboTemplateSource(name, template)); 100 } 101 102 /** 103 * Add a new template source with default name ("Test") to this task; the template is replaced with 104 * corresponding combo parameters (as defined in the combo test environment). 105 */ 106 public ComboTask withSourceFromTemplate(String template) { 107 return withSource(new ComboTemplateSource("Test", template)); 108 } 109 110 /** 111 * Add a new template source with given name to this task; the template is replaced with 112 * corresponding combo parameters (as defined in the combo test environment). A custom resolver 113 * is used to add combo parameter mappings to the current combo test environment. 114 */ 115 public ComboTask withSourceFromTemplate(String name, String template, Resolver resolver) { 116 return withSource(new ComboTemplateSource(name, template, resolver)); 117 } 118 119 /** 120 * Add a new template source with default name ("Test") to this task; the template is replaced with 121 * corresponding combo parameters (as defined in the combo test environment). A custom resolver 122 * is used to add combo parameter mappings to the current combo test environment. 123 */ 124 public ComboTask withSourceFromTemplate(String template, Resolver resolver) { 125 return withSource(new ComboTemplateSource("Test", template, resolver)); 126 } 127 128 /** 129 * Add a new option to this task. 130 */ 131 public ComboTask withOption(String opt) { 132 options = options.append(opt); 133 return this; 134 } 135 136 /** 137 * Add a set of options to this task. 138 */ 139 public ComboTask withOptions(String[] opts) { 140 for (String opt : opts) { 141 options = options.append(opt); 142 } 143 return this; 144 } 145 146 /** 147 * Add a set of options to this task. 148 */ 149 public ComboTask withOptions(Iterable<? extends String> opts) { 150 for (String opt : opts) { 151 options = options.append(opt); 152 } 153 return this; 154 } 155 156 /** 157 * Set the output writer associated with this task. 158 */ 159 public ComboTask withWriter(Writer out) { 160 this.out = out; 161 return this; 162 } 163 164 /** 165 * Add a task listener to this task. 166 */ 167 public ComboTask withListener(TaskListener listener) { 168 listeners = listeners.prepend(listener); 169 return this; 170 } 171 172 /** 173 * Parse the sources associated with this task. 174 */ 175 public Result<Iterable<? extends CompilationUnitTree>> parse() throws IOException { 176 return new Result<>(getTask().parse()); 177 } 178 179 /** 180 * Parse and analyzes the sources associated with this task. 181 */ 182 public Result<Iterable<? extends Element>> analyze() throws IOException { 183 return new Result<>(getTask().analyze()); 184 } 185 186 /** 187 * Parse, analyze and perform code generation for the sources associated with this task. 188 */ 189 public Result<Iterable<? extends JavaFileObject>> generate() throws IOException { 190 return new Result<>(getTask().generate()); 191 } 192 193 /** 194 * Parse, analyze, perform code generation for the sources associated with this task and finally 195 * executes them 196 */ 197 public <Z> Optional<Z> execute(Function<ExecutionTask, Z> executionFunc) throws IOException { 198 Result<Iterable<? extends JavaFileObject>> generationResult = generate(); 199 Iterable<? extends JavaFileObject> jfoIterable = generationResult.get(); 200 if (generationResult.hasErrors()) { 201 // we have nothing else to do 202 return Optional.empty(); 203 } 204 java.util.List<URL> urlList = new ArrayList<>(); 205 for (JavaFileObject jfo : jfoIterable) { 206 String urlStr = jfo.toUri().toURL().toString(); 207 urlStr = urlStr.substring(0, urlStr.length() - jfo.getName().length()); 208 urlList.add(new URL(urlStr)); 209 } 210 return Optional.of( 211 executionFunc.apply( 212 new ExecutionTask(new URLClassLoader(urlList.toArray(new URL[urlList.size()]))))); 213 } 214 215 /** 216 * Fork a new compilation task; if possible the compilation context from previous executions is 217 * retained (see comments in ReusableContext as to when it's safe to do so); otherwise a brand 218 * new context is created. 219 */ 220 public JavacTask getTask() { 221 if (task == null) { 222 ReusableContext context = env.context(); 223 String opts = options == null ? "" : 224 StreamSupport.stream(options.spliterator(), false).collect(Collectors.joining()); 225 context.clear(); 226 if (!context.polluted && (context.opts == null || context.opts.equals(opts))) { 227 //we can reuse former context 228 env.info().ctxReusedCount++; 229 } else { 230 env.info().ctxDroppedCount++; 231 //it's not safe to reuse context - create a new one 232 context = env.setContext(new ReusableContext()); 233 } 234 context.opts = opts; 235 JavacTask javacTask = ((JavacTool)env.javaCompiler()).getTask(out, env.fileManager(), 236 diagsCollector, options, null, sources, context); 237 javacTask.setTaskListener(context); 238 for (TaskListener l : listeners) { 239 javacTask.addTaskListener(l); 240 } 241 task = javacTask; 242 } 243 return task; 244 } 245 246 /** 247 * This class represents an execution task. It allows the execution of one or more classes previously 248 * added to a given class loader. This class uses reflection to execute any given static public method 249 * in any given class. It's not restricted to the execution of the {@code main} method 250 */ 251 public class ExecutionTask { 252 private ClassLoader classLoader; 253 private String methodName = "main"; 254 private Class<?>[] parameterTypes = new Class<?>[]{String[].class}; 255 private Object[] args = new String[0]; 256 private Consumer<Throwable> handler; 257 private Class<?> c; 258 259 private ExecutionTask(ClassLoader classLoader) { 260 this.classLoader = classLoader; 261 } 262 263 /** 264 * Set the name of the class to be loaded. 265 */ 266 public ExecutionTask withClass(String className) { 267 Assert.check(className != null, "class name value is null, impossible to proceed"); 268 try { 269 c = classLoader.loadClass(className); 270 } catch (Throwable t) { 271 throw new IllegalStateException(t); 272 } 273 return this; 274 } 275 276 /** 277 * Set the name of the method to be executed along with the parameter types to 278 * reflectively obtain the method. 279 */ 280 public ExecutionTask withMethod(String methodName, Class<?>... parameterTypes) { 281 this.methodName = methodName; 282 this.parameterTypes = parameterTypes; 283 return this; 284 } 285 286 /** 287 * Set the arguments to be passed to the method. 288 */ 289 public ExecutionTask withArguments(Object... args) { 290 this.args = args; 291 return this; 292 } 293 294 /** 295 * Set a handler to handle any exception thrown. 296 */ 297 public ExecutionTask withHandler(Consumer<Throwable> handler) { 298 this.handler = handler; 299 return this; 300 } 301 302 /** 303 * Executes the given method in the given class. Returns true if the execution was 304 * successful, false otherwise. 305 */ 306 public Object run() { 307 try { 308 java.lang.reflect.Method meth = c.getMethod(methodName, parameterTypes); 309 meth.invoke(null, (Object)args); 310 return true; 311 } catch (Throwable t) { 312 if (handler != null) { 313 handler.accept(t); 314 } 315 return false; 316 } 317 } 318 } 319 320 /** 321 * This class is used to help clients accessing the results of a given compilation task. 322 * Contains several helper methods to inspect diagnostics generated during the task execution. 323 */ 324 public class Result<D> { 325 326 /** The underlying compilation results. */ 327 private final D data; 328 329 public Result(D data) { 330 this.data = data; 331 } 332 333 public D get() { 334 return data; 335 } 336 337 /** 338 * Did this task generate any error diagnostics? 339 */ 340 public boolean hasErrors() { 341 return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.ERROR); 342 } 343 344 /** 345 * Did this task generate any warning diagnostics? 346 */ 347 public boolean hasWarnings() { 348 return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.WARNING); 349 } 350 351 /** 352 * Did this task generate any note diagnostics? 353 */ 354 public boolean hasNotes() { 355 return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.NOTE); 356 } 357 358 /** 359 * Did this task generate any diagnostic with given key? 360 */ 361 public boolean containsKey(String key) { 362 return diagsCollector.diagsByKeys.containsKey(key); 363 } 364 365 /** 366 * Retrieve the list of diagnostics of a given kind. 367 */ 368 public List<Diagnostic<? extends JavaFileObject>> diagnosticsForKind(Diagnostic.Kind kind) { 369 List<Diagnostic<? extends JavaFileObject>> diags = diagsCollector.diagsByKind.get(kind); 370 return diags != null ? diags : List.nil(); 371 } 372 373 /** 374 * Retrieve the list of diagnostics with given key. 375 */ 376 public List<Diagnostic<? extends JavaFileObject>> diagnosticsForKey(String key) { 377 List<Diagnostic<? extends JavaFileObject>> diags = diagsCollector.diagsByKeys.get(key); 378 return diags != null ? diags : List.nil(); 379 } 380 381 /** 382 * Dump useful info associated with this task. 383 */ 384 public String compilationInfo() { 385 return "instance#" + env.info().comboCount + ":[ options = " + options 386 + ", diagnostics = " + diagsCollector.diagsByKeys.keySet() 387 + ", dimensions = " + env.bindings 388 + ", sources = \n" + sources.stream().map(s -> { 389 try { 390 return s.getCharContent(true); 391 } catch (IOException ex) { 392 return ""; 393 } 394 }).collect(Collectors.joining(",")) + "]"; 395 } 396 } 397 398 /** 399 * This class represents a Java source file whose contents are defined in terms of a template 400 * string. The holes in such template are expanded using corresponding combo parameter 401 * instances which can be retrieved using a resolver object. 402 */ 403 class ComboTemplateSource extends SimpleJavaFileObject { 404 405 String source; 406 Map<String, ComboParameter> localParametersCache = new HashMap<>(); 407 408 protected ComboTemplateSource(String name, String template) { 409 this(name, template, null); 410 } 411 412 protected ComboTemplateSource(String name, String template, Resolver resolver) { 413 super(URI.create("myfo:/" + env.info().comboCount + "/" + name + ".java"), Kind.SOURCE); 414 source = ComboParameter.expandTemplate(template, pname -> resolveParameter(pname, resolver)); 415 } 416 417 @Override 418 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 419 return source; 420 } 421 422 /** 423 * Combo parameter resolver function. First parameters are looked up in the global environment, 424 * then the local environment is looked up as a fallback. 425 */ 426 ComboParameter resolveParameter(String pname, Resolver resolver) { 427 //first search the env 428 ComboParameter parameter = env.parametersCache.get(pname); 429 if (parameter == null) { 430 //then lookup local cache 431 parameter = localParametersCache.get(pname); 432 if (parameter == null && resolver != null) { 433 //if still null and we have a custom resolution function, try that 434 parameter = resolver.lookup(pname); 435 if (parameter != null) { 436 //if a match was found, store it in the local cache to aviod redundant recomputation 437 localParametersCache.put(pname, parameter); 438 } 439 } 440 } 441 return parameter; 442 } 443 } 444 445 /** 446 * Helper class to collect all diagnostic generated during the execution of a given compilation task. 447 */ 448 class DiagnosticCollector implements DiagnosticListener<JavaFileObject> { 449 450 Map<Diagnostic.Kind, List<Diagnostic<? extends JavaFileObject>>> diagsByKind = new HashMap<>(); 451 Map<String, List<Diagnostic<? extends JavaFileObject>>> diagsByKeys = new HashMap<>(); 452 453 public void report(Diagnostic<? extends JavaFileObject> diagnostic) { 454 List<Diagnostic<? extends JavaFileObject>> diags = 455 diagsByKeys.getOrDefault(diagnostic.getCode(), List.nil()); 456 diagsByKeys.put(diagnostic.getCode(), diags.prepend(diagnostic)); 457 Diagnostic.Kind kind = diagnostic.getKind(); 458 diags = diagsByKind.getOrDefault(kind, List.nil()); 459 diagsByKind.put(kind, diags.prepend(diagnostic)); 460 } 461 } 462 }