1 /*
  2  * Copyright (c) 2013, 2023, 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.List;
 37 import java.util.Map;
 38 import java.util.concurrent.atomic.AtomicInteger;
 39 import java.util.function.Consumer;
 40 import javax.tools.Diagnostic;
 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 org.junit.jupiter.api.BeforeEach;
 50 import org.junit.jupiter.api.extension.ExtendWith;
 51 
 52 import static org.junit.jupiter.api.Assertions.fail;
 53 
 54 /**
 55  * Base class for template-driven JUnit javac tests that support on-the-fly
 56  * source file generation, compilation, classloading, execution, and separate
 57  * compilation.
 58  *
 59  * <p>Manages a set of templates (which have embedded tags of the form
 60  * {@code #\{NAME\}}), source files (which are also templates), and compile
 61  * options.  Test cases can register templates and source files, cause them to
 62  * be compiled, validate whether the set of diagnostic messages output by the
 63  * compiler is correct, and optionally load and run the compiled classes.
 64  *
 65  * @author Brian Goetz
 66  */
 67 @ExtendWith(ComboWatcher.class)
 68 public abstract class JavacTemplateTestBase {
 69     private static final AtomicInteger counter = new AtomicInteger();
 70     private static final File root = new File("gen");
 71     private static final File nullDir = new File("empty");
 72 
 73     protected final Map<String, Template> templates = new HashMap<>();
 74     protected final Diagnostics diags = new Diagnostics();
 75     protected final List<SourceFile> sourceFiles = new ArrayList<>();
 76     protected final List<String> compileOptions = new ArrayList<>();
 77     protected final List<File> classpaths = new ArrayList<>();
 78 
 79     /** Add a template with a specified name */
 80     protected void addTemplate(String name, Template t) {
 81         templates.put(name, t);
 82     }
 83 
 84     /** Add a template with a specified name */
 85     protected void addTemplate(String name, String s) {
 86         templates.put(name, new StringTemplate(s));
 87     }
 88 
 89     /** Add a source file */
 90     protected void addSourceFile(String name, String template) {
 91         sourceFiles.add(new SourceFile(name, template));
 92     }
 93 
 94     /** Add a File to the class path to be used when loading classes; File values
 95      * will generally be the result of a previous call to {@link #compile()}.
 96      * This enables testing of separate compilation scenarios if the class path
 97      * is set up properly.
 98      */
 99     protected void addClassPath(File path) {
100         classpaths.add(path);
101     }
102 
103     /**
104      * Add a set of compilation command-line options
105      */
106     protected void addCompileOptions(String... opts) {
107         Collections.addAll(compileOptions, opts);
108     }
109 
110     /** Reset the compile options to the default (empty) value */
111     protected void resetCompileOptions() { compileOptions.clear(); }
112 
113     /** Remove all templates */
114     protected void resetTemplates() { templates.clear(); }
115 
116     /** Remove accumulated diagnostics */
117     protected void resetDiagnostics() { diags.reset(); }
118 
119     /** Remove all source files */
120     protected void resetSourceFiles() { sourceFiles.clear(); }
121 
122     /** Remove registered class paths */
123     protected void resetClassPaths() { classpaths.clear(); }
124 
125     // Before each test method, reset everything
126     @BeforeEach
127     public void reset() {
128         resetCompileOptions();
129         resetDiagnostics();
130         resetSourceFiles();
131         resetTemplates();
132         resetClassPaths();
133     }
134 
135     /** Assert that all previous calls to compile() succeeded */
136     protected void assertCompileSucceeded() {
137         if (diags.errorsFound())
138             fail("Expected successful compilation");
139     }
140 
141     /** Assert that all previous calls to compile() succeeded, also accepts a diagnostics consumer */
142     protected void assertCompileSucceeded(Consumer<Diagnostic<?>> diagConsumer) {
143         if (diags.errorsFound())
144             fail("Expected successful compilation");
145         diags.getAllDiags().stream().forEach(diagConsumer);
146     }
147 
148     /** Assert that all previous calls to compile() succeeded */
149     protected void assertCompileSucceededWithWarning(String warning) {
150         if (diags.errorsFound())
151             fail("Expected successful compilation");
152         if (!diags.containsWarningKey(warning)) {
153             fail(String.format("Expected compilation warning with %s, found %s", warning, diags.keys()));
154         }
155     }
156 
157     protected void assertCompileSucceededWithWarning(String warning, int numberOfWarnings) {
158         if (diags.errorsFound())
159             fail("Expected successful compilation");
160         if (!diags.containsWarningKey(warning, numberOfWarnings)) {
161             fail(String.format("Expected compilation warning with %s, found %s", warning, diags.keys()));
162         }
163     }
164 
165     /**
166      * If the provided boolean is true, assert all previous compiles succeeded,
167      * otherwise assert that a compile failed.
168      * */
169     protected void assertCompileSucceededIff(boolean b) {
170         if (b)
171             assertCompileSucceeded();
172         else
173             assertCompileFailed();
174     }
175 
176     /** Assert that a previous call to compile() failed */
177     protected void assertCompileFailed() {
178         if (!diags.errorsFound())
179             fail("Expected failed compilation");
180     }
181 
182     /** Assert that a previous call to compile() failed with a specific error key */
183     protected void assertCompileFailed(String key) {
184         if (!diags.errorsFound())
185             fail("Expected failed compilation: " + key);
186         if (!diags.containsErrorKey(key)) {
187             fail(String.format("Expected compilation error with %s, found %s", key, diags.keys()));
188         }
189     }
190 
191     protected void assertCompileFailed(String key, Consumer<Diagnostic<?>> diagConsumer) {
192         if (!diags.errorsFound())
193             fail("Expected failed compilation: " + key);
194         if (!diags.containsErrorKey(key)) {
195             fail(String.format("Expected compilation error with %s, found %s", key, diags.keys()));
196         } else {
197             // for additional checks
198             diagConsumer.accept(diags.getDiagWithKey(key));
199         }
200     }
201 
202     /** Assert that a previous call to compile() failed with a specific error key */
203     protected void assertCompileFailedOneOf(String... keys) {
204         if (!diags.errorsFound())
205             fail("Expected failed compilation with one of: " + Arrays.asList(keys));
206         boolean found = false;
207         for (String k : keys)
208             if (diags.containsErrorKey(k))
209                 found = true;
210         fail(String.format("Expected compilation error with one of %s, found %s", Arrays.asList(keys), diags.keys()));
211     }
212 
213     /** Assert that a previous call to compile() failed with all of the specified error keys */
214     protected void assertCompileErrors(String... keys) {
215         if (!diags.errorsFound())
216             fail("Expected failed compilation");
217         for (String k : keys)
218             if (!diags.containsErrorKey(k))
219                 fail("Expected compilation error " + k);
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         var files = sourceFiles.stream().map(FileAdapter::new).toList();
231         return compile(classpaths, files, generate);
232     }
233 
234     /** Compile all registered source files, using the provided list of class paths
235      * for finding required classfiles, optionally generating class files
236      * and returning a File describing the directory to which they were written */
237     protected File compile(List<File> classpaths, boolean generate) throws IOException {
238         var files = sourceFiles.stream().map(FileAdapter::new).toList();
239         return compile(classpaths, files, generate);
240     }
241 
242     private File compile(List<File> classpaths, List<? extends JavaFileObject> files, boolean generate) throws IOException {
243         JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
244         try (StandardJavaFileManager fm = systemJavaCompiler.getStandardFileManager(null, null, null)) {
245             if (classpaths.size() > 0)
246                 fm.setLocation(StandardLocation.CLASS_PATH, classpaths);
247             JavacTask ct = (JavacTask) systemJavaCompiler.getTask(null, fm, diags, compileOptions, null, files);
248             if (generate) {
249                 File destDir = new File(root, Integer.toString(counter.incrementAndGet()));
250                 // @@@ Assert that this directory didn't exist, or start counter at max+1
251                 destDir.mkdirs();
252                 fm.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(destDir));
253                 ct.generate();
254                 return destDir;
255             }
256             else {
257                 ct.analyze();
258                 return nullDir;
259             }
260         }
261     }
262 
263     /** Load the given class using the provided list of class paths */
264     protected Class<?> loadClass(String className, File... destDirs) {
265         try {
266             List<URL> list = new ArrayList<>();
267             for (File f : destDirs)
268                 list.add(new URL("file:" + f.toString().replace("\\", "/") + "/"));
269             return Class.forName(className, true, new URLClassLoader(list.toArray(new URL[list.size()])));
270         } catch (ClassNotFoundException | MalformedURLException e) {
271             throw new RuntimeException("Error loading class " + className, e);
272         }
273     }
274 
275     /** An implementation of Template which is backed by a String */
276     protected class StringTemplate implements Template {
277         protected final String template;
278 
279         public StringTemplate(String template) {
280             this.template = template;
281         }
282 
283         public String expand(String selectorIgnored) {
284             return Template.expandTemplate(template, templates);
285         }
286 
287         public String toString() {
288             return expand("");
289         }
290     }
291 
292     private class FileAdapter extends SimpleJavaFileObject {
293         private final String templateString;
294 
295         FileAdapter(SourceFile file) {
296             super(URI.create("myfo:/" + file.name()), Kind.SOURCE);
297             this.templateString = file.template();
298         }
299 
300         public CharSequence getCharContent(boolean ignoreEncodingErrors) {
301             return toString();
302         }
303 
304         public String toString() {
305             return Template.expandTemplate(templateString, templates);
306         }
307     }
308 }