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     /**
158      * If the provided boolean is true, assert all previous compiles succeeded,
159      * otherwise assert that a compile failed.
160      * */
161     protected void assertCompileSucceededIff(boolean b) {
162         if (b)
163             assertCompileSucceeded();
164         else
165             assertCompileFailed();
166     }
167 
168     /** Assert that a previous call to compile() failed */
169     protected void assertCompileFailed() {
170         if (!diags.errorsFound())
171             fail("Expected failed compilation");
172     }
173 
174     /** Assert that a previous call to compile() failed with a specific error key */
175     protected void assertCompileFailed(String key) {
176         if (!diags.errorsFound())
177             fail("Expected failed compilation: " + key);
178         if (!diags.containsErrorKey(key)) {
179             fail(String.format("Expected compilation error with %s, found %s", key, diags.keys()));
180         }
181     }
182 
183     protected void assertCompileFailed(String key, Consumer<Diagnostic<?>> diagConsumer) {
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         } else {
189             // for additional checks
190             diagConsumer.accept(diags.getDiagWithKey(key));
191         }
192     }
193 
194     /** Assert that a previous call to compile() failed with a specific error key */
195     protected void assertCompileFailedOneOf(String... keys) {
196         if (!diags.errorsFound())
197             fail("Expected failed compilation with one of: " + Arrays.asList(keys));
198         boolean found = false;
199         for (String k : keys)
200             if (diags.containsErrorKey(k))
201                 found = true;
202         fail(String.format("Expected compilation error with one of %s, found %s", Arrays.asList(keys), diags.keys()));
203     }
204 
205     /** Assert that a previous call to compile() failed with all of the specified error keys */
206     protected void assertCompileErrors(String... keys) {
207         if (!diags.errorsFound())
208             fail("Expected failed compilation");
209         for (String k : keys)
210             if (!diags.containsErrorKey(k))
211                 fail("Expected compilation error " + k);
212     }
213 
214     /** Compile all registered source files */
215     protected void compile() throws IOException {
216         compile(false);
217     }
218 
219     /** Compile all registered source files, optionally generating class files
220      * and returning a File describing the directory to which they were written */
221     protected File compile(boolean generate) throws IOException {
222         var files = sourceFiles.stream().map(FileAdapter::new).toList();
223         return compile(classpaths, files, generate);
224     }
225 
226     /** Compile all registered source files, using the provided list of class paths
227      * for finding required classfiles, optionally generating class files
228      * and returning a File describing the directory to which they were written */
229     protected File compile(List<File> classpaths, boolean generate) throws IOException {
230         var files = sourceFiles.stream().map(FileAdapter::new).toList();
231         return compile(classpaths, files, generate);
232     }
233 
234     private File compile(List<File> classpaths, List<? extends JavaFileObject> files, boolean generate) throws IOException {
235         JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
236         try (StandardJavaFileManager fm = systemJavaCompiler.getStandardFileManager(null, null, null)) {
237             if (classpaths.size() > 0)
238                 fm.setLocation(StandardLocation.CLASS_PATH, classpaths);
239             JavacTask ct = (JavacTask) systemJavaCompiler.getTask(null, fm, diags, compileOptions, null, files);
240             if (generate) {
241                 File destDir = new File(root, Integer.toString(counter.incrementAndGet()));
242                 // @@@ Assert that this directory didn't exist, or start counter at max+1
243                 destDir.mkdirs();
244                 fm.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(destDir));
245                 ct.generate();
246                 return destDir;
247             }
248             else {
249                 ct.analyze();
250                 return nullDir;
251             }
252         }
253     }
254 
255     /** Load the given class using the provided list of class paths */
256     protected Class<?> loadClass(String className, File... destDirs) {
257         try {
258             List<URL> list = new ArrayList<>();
259             for (File f : destDirs)
260                 list.add(new URL("file:" + f.toString().replace("\\", "/") + "/"));
261             return Class.forName(className, true, new URLClassLoader(list.toArray(new URL[list.size()])));
262         } catch (ClassNotFoundException | MalformedURLException e) {
263             throw new RuntimeException("Error loading class " + className, e);
264         }
265     }
266 
267     /** An implementation of Template which is backed by a String */
268     protected class StringTemplate implements Template {
269         protected final String template;
270 
271         public StringTemplate(String template) {
272             this.template = template;
273         }
274 
275         public String expand(String selectorIgnored) {
276             return Template.expandTemplate(template, templates);
277         }
278 
279         public String toString() {
280             return expand("");
281         }
282     }
283 
284     private class FileAdapter extends SimpleJavaFileObject {
285         private final String templateString;
286 
287         FileAdapter(SourceFile file) {
288             super(URI.create("myfo:/" + file.name()), Kind.SOURCE);
289             this.templateString = file.template();
290         }
291 
292         public CharSequence getCharContent(boolean ignoreEncodingErrors) {
293             return toString();
294         }
295 
296         public String toString() {
297             return Template.expandTemplate(templateString, templates);
298         }
299     }
300 }