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