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 /** 182 * If the provided boolean is true, assert all previous compiles succeeded, 183 * otherwise assert that a compile failed. 184 * */ 185 protected void assertCompileSucceededIff(boolean b) { 186 if (b) 187 assertCompileSucceeded(); 188 else 189 assertCompileFailed(); 190 } 191 192 /** Assert that a previous call to compile() failed */ 193 protected void assertCompileFailed() { 194 if (!diags.errorsFound()) 195 fail("Expected failed compilation"); 196 } 197 198 /** Assert that a previous call to compile() failed with a specific error key */ 199 protected void assertCompileFailed(String message) { 200 if (!diags.errorsFound()) 201 fail("Expected failed compilation: " + message); 202 } 203 204 /** Assert that a previous call to compile() failed with all of the specified error keys */ 205 protected void assertCompileErrors(String... keys) { 206 if (!diags.errorsFound()) 207 fail("Expected failed compilation"); 208 for (String k : keys) 209 if (!diags.containsErrorKey(k)) 210 fail("Expected compilation error " + k); 211 } 212 213 /** Convert an object, which may be a Template or a String, into a Template */ 214 protected Template asTemplate(Object o) { 215 if (o instanceof Template) 216 return (Template) o; 217 else if (o instanceof String) 218 return new StringTemplate((String) o); 219 else 220 return new StringTemplate(o.toString()); 221 } 222 223 /** Compile all registered source files */ 224 protected void compile() throws IOException { 225 compile(false); 226 } 227 228 /** Compile all registered source files, optionally generating class files 229 * and returning a File describing the directory to which they were written */ 230 protected File compile(boolean generate) throws IOException { 231 List<JavaFileObject> files = new ArrayList<>(); 232 for (Pair<String, Template> e : sourceFiles) 233 files.add(new FileAdapter(e.fst, asTemplate(e.snd))); 234 return compile(classpaths, files, generate); 235 } 236 237 /** Compile all registered source files, using the provided list of class paths 238 * for finding required classfiles, optionally generating class files 239 * and returning a File describing the directory to which they were written */ 240 protected File compile(List<File> classpaths, boolean generate) throws IOException { 241 List<JavaFileObject> files = new ArrayList<>(); 242 for (Pair<String, Template> e : sourceFiles) 243 files.add(new FileAdapter(e.fst, asTemplate(e.snd))); 244 return compile(classpaths, files, generate); 245 } 246 247 private File compile(List<File> classpaths, List<JavaFileObject> files, boolean generate) throws IOException { 248 JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler(); 249 try (StandardJavaFileManager fm = systemJavaCompiler.getStandardFileManager(null, null, null)) { 250 if (classpaths.size() > 0) 251 fm.setLocation(StandardLocation.CLASS_PATH, classpaths); 252 JavacTask ct = (JavacTask) systemJavaCompiler.getTask(null, fm, diags, compileOptions, null, files); 253 if (generate) { 254 File destDir = new File(root, Integer.toString(counter.incrementAndGet())); 255 // @@@ Assert that this directory didn't exist, or start counter at max+1 256 destDir.mkdirs(); 257 fm.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(destDir)); 258 ct.generate(); 259 return destDir; 260 } 261 else { 262 ct.analyze(); 263 return nullDir; 264 } 265 } 266 } 267 268 /** Load the given class using the provided list of class paths */ 269 protected Class<?> loadClass(String className, File... destDirs) { 270 try { 271 List<URL> list = new ArrayList<>(); 272 for (File f : destDirs) 273 list.add(new URL("file:" + f.toString().replace("\\", "/") + "/")); 274 return Class.forName(className, true, new URLClassLoader(list.toArray(new URL[list.size()]))); 275 } catch (ClassNotFoundException | MalformedURLException e) { 276 throw new RuntimeException("Error loading class " + className, e); 277 } 278 } 279 280 /** An implementation of Template which is backed by a String */ 281 protected class StringTemplate implements Template { 282 protected final String template; 283 284 public StringTemplate(String template) { 285 this.template = template; 286 } 287 288 public String expand(String selector) { 289 return Behavior.expandTemplate(template, currentResolver); 290 } 291 292 public String toString() { 293 return expand(""); 294 } 295 296 public StringTemplate with(final String key, final String value) { 297 return new StringTemplateWithResolver(template, new KeyResolver(key, value)); 298 } 299 300 } 301 302 /** An implementation of Template which is backed by a String and which 303 * encapsulates a Resolver for resolving embedded tags. */ 304 protected class StringTemplateWithResolver extends StringTemplate { 305 private final Resolver localResolver; 306 307 public StringTemplateWithResolver(String template, Resolver localResolver) { 308 super(template); 309 this.localResolver = localResolver; 310 } 311 312 @Override 313 public String expand(String selector) { 314 Resolver saved = currentResolver; 315 currentResolver = new ChainedResolver(currentResolver, localResolver); 316 try { 317 return super.expand(selector); 318 } 319 finally { 320 currentResolver = saved; 321 } 322 } 323 324 @Override 325 public StringTemplate with(String key, String value) { 326 return new StringTemplateWithResolver(template, new ChainedResolver(localResolver, new KeyResolver(key, value))); 327 } 328 } 329 330 /** A Resolver which uses a Map to resolve tags */ 331 private class KeyResolver implements Template.Resolver { 332 private final String key; 333 private final String value; 334 335 public KeyResolver(String key, String value) { 336 this.key = key; 337 this.value = value; 338 } 339 340 @Override 341 public Template lookup(String k) { 342 return key.equals(k) ? new StringTemplate(value) : null; 343 } 344 } 345 346 private class FileAdapter extends SimpleJavaFileObject { 347 private final String filename; 348 private final Template template; 349 350 public FileAdapter(String filename, Template template) { 351 super(URI.create("myfo:/" + filename), Kind.SOURCE); 352 this.template = template; 353 this.filename = filename; 354 } 355 356 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 357 return toString(); 358 } 359 360 public String toString() { 361 return Template.Behavior.expandTemplate(template.expand(filename), defaultResolver); 362 } 363 } 364 }