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