1 /* 2 * Copyright (c) 2013, 2014, 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 tools.javac.combo; 25 26 import java.io.File; 27 import java.io.IOException; 28 import java.net.MalformedURLException; 29 import java.net.URI; 30 import java.net.URL; 31 import java.net.URLClassLoader; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Collections; 35 import java.util.HashMap; 36 import java.util.HashSet; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Set; 40 import java.util.concurrent.atomic.AtomicInteger; 41 import javax.tools.JavaCompiler; 42 import javax.tools.JavaFileObject; 43 import javax.tools.SimpleJavaFileObject; 44 import javax.tools.StandardJavaFileManager; 45 import javax.tools.StandardLocation; 46 import javax.tools.ToolProvider; 47 48 import com.sun.source.util.JavacTask; 49 import com.sun.tools.javac.util.Pair; 50 import org.testng.ITestResult; 51 import org.testng.annotations.AfterMethod; 52 import org.testng.annotations.AfterSuite; 53 import org.testng.annotations.BeforeMethod; 54 import org.testng.annotations.Test; 55 56 import static org.testng.Assert.fail; 57 58 /** 59 * Base class for template-driven TestNG javac tests that support on-the-fly 60 * source file generation, compilation, classloading, execution, and separate 61 * compilation. 62 * 63 * <p>Manages a set of templates (which have embedded tags of the form 64 * {@code #\{NAME\}}), source files (which are also templates), and compile 65 * options. Test cases can register templates and source files, cause them to 66 * be compiled, validate whether the set of diagnostic messages output by the 67 * compiler is correct, and optionally load and run the compiled classes. 68 * 69 * @author Brian Goetz 70 */ 71 @Test 72 public abstract class JavacTemplateTestBase { 73 private static final Set<String> suiteErrors = Collections.synchronizedSet(new HashSet<>()); 74 private static final AtomicInteger counter = new AtomicInteger(); 75 private static final File root = new File("gen"); 76 private static final File nullDir = new File("empty"); 77 78 protected final Map<String, Template> templates = new HashMap<>(); 79 protected final Diagnostics diags = new Diagnostics(); 80 protected final List<Pair<String, Template>> sourceFiles = new ArrayList<>(); 81 protected final List<String> compileOptions = new ArrayList<>(); 82 protected final List<File> classpaths = new ArrayList<>(); 83 protected final Template.Resolver defaultResolver = new MapResolver(templates); 84 85 private Template.Resolver currentResolver = defaultResolver; 86 87 /** Add a template with a specified name */ 88 protected void addTemplate(String name, Template t) { 89 templates.put(name, t); 90 } 91 92 /** Add a template with a specified name */ 93 protected void addTemplate(String name, String s) { 94 templates.put(name, new StringTemplate(s)); 95 } 96 97 /** Add a source file */ 98 protected void addSourceFile(String name, Template t) { 99 sourceFiles.add(new Pair<>(name, t)); 100 } 101 102 /** Add a File to the class path to be used when loading classes; File values 103 * will generally be the result of a previous call to {@link #compile()}. 104 * This enables testing of separate compilation scenarios if the class path 105 * is set up properly. 106 */ 107 protected void addClassPath(File path) { 108 classpaths.add(path); 109 } 110 111 /** 112 * Add a set of compilation command-line options 113 */ 114 protected void addCompileOptions(String... opts) { 115 Collections.addAll(compileOptions, opts); 116 } 117 118 /** Reset the compile options to the default (empty) value */ 119 protected void resetCompileOptions() { compileOptions.clear(); } 120 121 /** Remove all templates */ 122 protected void resetTemplates() { templates.clear(); } 123 124 /** Remove accumulated diagnostics */ 125 protected void resetDiagnostics() { diags.reset(); } 126 127 /** Remove all source files */ 128 protected void resetSourceFiles() { sourceFiles.clear(); } 129 130 /** Remove registered class paths */ 131 protected void resetClassPaths() { classpaths.clear(); } 132 133 // Before each test method, reset everything 134 @BeforeMethod 135 public void reset() { 136 resetCompileOptions(); 137 resetDiagnostics(); 138 resetSourceFiles(); 139 resetTemplates(); 140 resetClassPaths(); 141 } 142 143 // After each test method, if the test failed, capture source files and diagnostics and put them in the log 144 @AfterMethod 145 public void copyErrors(ITestResult result) { 146 if (!result.isSuccess()) { 147 suiteErrors.addAll(diags.errorKeys()); 148 149 List<Object> list = new ArrayList<>(); 150 Collections.addAll(list, result.getParameters()); 151 list.add("Test case: " + getTestCaseDescription()); 152 for (Pair<String, Template> e : sourceFiles) 153 list.add("Source file " + e.fst + ": " + e.snd); 154 if (diags.errorsFound()) 155 list.add("Compile diagnostics: " + diags.toString()); 156 result.setParameters(list.toArray(new Object[list.size()])); 157 } 158 } 159 160 @AfterSuite 161 // After the suite is done, dump any errors to output 162 public void dumpErrors() { 163 if (!suiteErrors.isEmpty()) 164 System.err.println("Errors found in test suite: " + suiteErrors); 165 } 166 167 /** 168 * Get a description of this test case; since test cases may be combinatorially 169 * generated, this should include all information needed to describe the test case 170 */ 171 protected String getTestCaseDescription() { 172 return this.toString(); 173 } 174 175 /** Assert that all previous calls to compile() succeeded */ 176 protected void assertCompileSucceeded() { 177 if (diags.errorsFound()) 178 fail("Expected successful compilation"); 179 } 180 181 /** Assert that all previous calls to compile() succeeded */ 182 protected void assertCompileSucceededWithWarning(String warning) { 183 if (diags.errorsFound()) 184 fail("Expected successful compilation"); 185 if (!diags.containsWarningKey(warning)) 186 fail("Expected compilation warning " + warning); 187 } 188 189 /** 190 * If the provided boolean is true, assert all previous compiles succeeded, 191 * otherwise assert that a compile failed. 192 * */ 193 protected void assertCompileSucceededIff(boolean b) { 194 if (b) 195 assertCompileSucceeded(); 196 else 197 assertCompileFailed(); 198 } 199 200 /** Assert that a previous call to compile() failed */ 201 protected void assertCompileFailed() { 202 if (!diags.errorsFound()) 203 fail("Expected failed compilation"); 204 } 205 206 /** Assert that a previous call to compile() failed with a specific error key */ 207 protected void assertCompileFailed(String key) { 208 if (!diags.errorsFound()) 209 fail("Expected failed compilation: " + key); 210 if (!diags.containsErrorKey(key)) 211 fail("Expected compilation error " + key); 212 } 213 214 /** Assert that a previous call to compile() failed with a specific error key */ 215 protected void assertCompileFailedOneOf(String... keys) { 216 if (!diags.errorsFound()) 217 fail("Expected failed compilation with one of: " + Arrays.asList(keys)); 218 boolean found = false; 219 for (String k : keys) 220 if (diags.containsErrorKey(k)) 221 found = true; 222 fail(String.format("Expected compilation error with one of %s, found %s", Arrays.asList(keys), diags.keys())); 223 } 224 225 /** Assert that a previous call to compile() failed with all of the specified error keys */ 226 protected void assertCompileErrors(String... keys) { 227 if (!diags.errorsFound()) 228 fail("Expected failed compilation"); 229 for (String k : keys) 230 if (!diags.containsErrorKey(k)) 231 fail("Expected compilation error " + k); 232 } 233 234 /** Convert an object, which may be a Template or a String, into a Template */ 235 protected Template asTemplate(Object o) { 236 if (o instanceof Template) 237 return (Template) o; 238 else if (o instanceof String) 239 return new StringTemplate((String) o); 240 else 241 return new StringTemplate(o.toString()); 242 } 243 244 /** Compile all registered source files */ 245 protected void compile() throws IOException { 246 compile(false); 247 } 248 249 /** Compile all registered source files, optionally generating class files 250 * and returning a File describing the directory to which they were written */ 251 protected File compile(boolean generate) throws IOException { 252 List<JavaFileObject> files = new ArrayList<>(); 253 for (Pair<String, Template> e : sourceFiles) 254 files.add(new FileAdapter(e.fst, asTemplate(e.snd))); 255 return compile(classpaths, files, generate); 256 } 257 258 /** Compile all registered source files, using the provided list of class paths 259 * for finding required classfiles, optionally generating class files 260 * and returning a File describing the directory to which they were written */ 261 protected File compile(List<File> classpaths, boolean generate) throws IOException { 262 List<JavaFileObject> files = new ArrayList<>(); 263 for (Pair<String, Template> e : sourceFiles) 264 files.add(new FileAdapter(e.fst, asTemplate(e.snd))); 265 return compile(classpaths, files, generate); 266 } 267 268 private File compile(List<File> classpaths, List<JavaFileObject> files, boolean generate) throws IOException { 269 JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler(); 270 try (StandardJavaFileManager fm = systemJavaCompiler.getStandardFileManager(null, null, null)) { 271 if (classpaths.size() > 0) 272 fm.setLocation(StandardLocation.CLASS_PATH, classpaths); 273 JavacTask ct = (JavacTask) systemJavaCompiler.getTask(null, fm, diags, compileOptions, null, files); 274 if (generate) { 275 File destDir = new File(root, Integer.toString(counter.incrementAndGet())); 276 // @@@ Assert that this directory didn't exist, or start counter at max+1 277 destDir.mkdirs(); 278 fm.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(destDir)); 279 ct.generate(); 280 return destDir; 281 } 282 else { 283 ct.analyze(); 284 return nullDir; 285 } 286 } 287 } 288 289 /** Load the given class using the provided list of class paths */ 290 protected Class<?> loadClass(String className, File... destDirs) { 291 try { 292 List<URL> list = new ArrayList<>(); 293 for (File f : destDirs) 294 list.add(new URL("file:" + f.toString().replace("\\", "/") + "/")); 295 return Class.forName(className, true, new URLClassLoader(list.toArray(new URL[list.size()]))); 296 } catch (ClassNotFoundException | MalformedURLException e) { 297 throw new RuntimeException("Error loading class " + className, e); 298 } 299 } 300 301 /** An implementation of Template which is backed by a String */ 302 protected class StringTemplate implements Template { 303 protected final String template; 304 305 public StringTemplate(String template) { 306 this.template = template; 307 } 308 309 public String expand(String selector) { 310 return Behavior.expandTemplate(template, currentResolver); 311 } 312 313 public String toString() { 314 return expand(""); 315 } 316 317 public StringTemplate with(final String key, final String value) { 318 return new StringTemplateWithResolver(template, new KeyResolver(key, value)); 319 } 320 321 } 322 323 /** An implementation of Template which is backed by a String and which 324 * encapsulates a Resolver for resolving embedded tags. */ 325 protected class StringTemplateWithResolver extends StringTemplate { 326 private final Resolver localResolver; 327 328 public StringTemplateWithResolver(String template, Resolver localResolver) { 329 super(template); 330 this.localResolver = localResolver; 331 } 332 333 @Override 334 public String expand(String selector) { 335 Resolver saved = currentResolver; 336 currentResolver = new ChainedResolver(currentResolver, localResolver); 337 try { 338 return super.expand(selector); 339 } 340 finally { 341 currentResolver = saved; 342 } 343 } 344 345 @Override 346 public StringTemplate with(String key, String value) { 347 return new StringTemplateWithResolver(template, new ChainedResolver(localResolver, new KeyResolver(key, value))); 348 } 349 } 350 351 /** A Resolver which uses a Map to resolve tags */ 352 private class KeyResolver implements Template.Resolver { 353 private final String key; 354 private final String value; 355 356 public KeyResolver(String key, String value) { 357 this.key = key; 358 this.value = value; 359 } 360 361 @Override 362 public Template lookup(String k) { 363 return key.equals(k) ? new StringTemplate(value) : null; 364 } 365 } 366 367 private class FileAdapter extends SimpleJavaFileObject { 368 private final String filename; 369 private final Template template; 370 371 public FileAdapter(String filename, Template template) { 372 super(URI.create("myfo:/" + filename), Kind.SOURCE); 373 this.template = template; 374 this.filename = filename; 375 } 376 377 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 378 return toString(); 379 } 380 381 public String toString() { 382 return Template.Behavior.expandTemplate(template.expand(filename), defaultResolver); 383 } 384 } 385 }