/******************************************************************************* * Copyright (c) 2007, 2015 BEA Systems, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * wharley@bea.com - initial API and implementation * IBM Corporation - fix for 342936 * het@google.com - Bug 415274 - Annotation processing throws a NPE in getElementsAnnotatedWith() *******************************************************************************/ package org.eclipse.jdt.compiler.apt.tests; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Platform; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.ServiceLoader; import javax.tools.DiagnosticListener; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; /** * Helper class to support compilation and results checking for tests running in batch mode. * @since 3.3 */ public class BatchTestUtils { private static final String RESOURCES_DIR = "resources"; // relative to plugin directory private static final String PROCESSOR_JAR_NAME = "lib/apttestprocessors.jar"; private static final String JLS8_PROCESSOR_JAR_NAME = "lib/apttestprocessors8.jar"; private static String _processorJarPath; private static String _jls8ProcessorJarPath; // locations to copy and generate files private static String _tmpFolder; private static JavaCompiler _eclipseCompiler; private static String _tmpSrcFolderName; private static File _tmpSrcDir; private static String _tmpBinFolderName; private static File _tmpBinDir; private static String _tmpGenFolderName; private static File _tmpGenDir; /** * Create a class that contains an annotation that generates another class, * and compile it. Verify that generation and compilation succeeded. */ public static void compileOneClass(JavaCompiler compiler, List<String> options, File inputFile) { compileOneClass(compiler, options, inputFile, false); } public static void compileOneClass(JavaCompiler compiler, List<String> options, File inputFile, boolean useJLS8Processors) { StandardJavaFileManager manager = compiler.getStandardFileManager(null, Locale.getDefault(), Charset.defaultCharset()); // create new list containing inputfile List<File> files = new ArrayList<File>(); files.add(inputFile); Iterable<? extends JavaFileObject> units = manager.getJavaFileObjectsFromFiles(files); StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); options.add("-d"); options.add(_tmpBinFolderName); options.add("-s"); options.add(_tmpGenFolderName); addProcessorPaths(options, useJLS8Processors, true); options.add("-XprintRounds"); options.add("-XprintProcessorInfo"); CompilationTask task = compiler.getTask(printWriter, manager, null, options, null, units); Boolean result = task.call(); if (!result.booleanValue()) { String errorOutput = stringWriter.getBuffer().toString(); System.err.println("Compilation failed: " + errorOutput); junit.framework.TestCase.assertTrue("Compilation failed : " + errorOutput, false); } } public static void compileTree(JavaCompiler compiler, List<String> options, File targetFolder) { compileTree(compiler, options, targetFolder, false); } public static void compileTree(JavaCompiler compiler, List<String> options, File targetFolder, DiagnosticListener<? super JavaFileObject> listener) { compileTree(compiler, options, targetFolder, false, listener); } public static void compileTree(JavaCompiler compiler, List<String> options, File targetFolder, boolean useJLS8Processors) { compileTree(compiler, options, targetFolder, useJLS8Processors, null); } public static void compileTree(JavaCompiler compiler, List<String> options, File targetFolder, boolean useJLS8Processors, DiagnosticListener<? super JavaFileObject> listener) { StandardJavaFileManager manager = compiler.getStandardFileManager(null, Locale.getDefault(), Charset.defaultCharset()); // create new list containing inputfile List<File> files = new ArrayList<File>(); findFilesUnder(targetFolder, files); Iterable<? extends JavaFileObject> units = manager.getJavaFileObjectsFromFiles(files); StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); options.add("-d"); options.add(_tmpBinFolderName); options.add("-s"); options.add(_tmpGenFolderName); addProcessorPaths(options, useJLS8Processors, true); options.add("-XprintRounds"); CompilationTask task = compiler.getTask(printWriter, manager, listener, options, null, units); Boolean result = task.call(); if (!result.booleanValue()) { String errorOutput = stringWriter.getBuffer().toString(); System.err.println("Compilation failed: " + errorOutput); junit.framework.TestCase.assertTrue("Compilation failed : " + errorOutput, false); } } /** * Compile the contents of a directory tree, collecting errors so that they can be * compared with expected errors. * @param compiler the system compiler or Eclipse compiler * @param options will be passed to the compiler * @param targetFolder the folder to compile * @param errors a StringWriter into which compiler output will be written * @return true if the compilation was successful */ public static boolean compileTreeWithErrors( JavaCompiler compiler, List<String> options, File targetFolder, DiagnosticListener<? super JavaFileObject> diagnosticListener) { return compileTreeWithErrors(compiler, options, targetFolder, diagnosticListener, false, true); } public static boolean compileTreeWithErrors( JavaCompiler compiler, List<String> options, File targetFolder, DiagnosticListener<? super JavaFileObject> diagnosticListener, boolean addProcessorsToClasspath) { return compileTreeWithErrors(compiler, options, targetFolder, diagnosticListener, false, addProcessorsToClasspath); } public static boolean compileTreeWithErrors( JavaCompiler compiler, List<String> options, File targetFolder, DiagnosticListener<? super JavaFileObject> diagnosticListener, boolean useJLS8Processors, boolean addProcessorsToClasspath) { StandardJavaFileManager manager = compiler.getStandardFileManager(null, Locale.getDefault(), Charset.defaultCharset()); // create new list containing inputfile List<File> files = new ArrayList<File>(); findFilesUnder(targetFolder, files); Iterable<? extends JavaFileObject> units = manager.getJavaFileObjectsFromFiles(files); options.add("-d"); options.add(_tmpBinFolderName); options.add("-s"); options.add(_tmpGenFolderName); addProcessorPaths(options, useJLS8Processors, addProcessorsToClasspath); // use writer to prevent System.out/err to be polluted with problems StringWriter writer = new StringWriter(); CompilationTask task = compiler.getTask(writer, manager, diagnosticListener, options, null, units); Boolean result = task.call(); return result.booleanValue(); } /** * Recursively collect all the files under some root. Ignore directories named "CVS". * Used when compiling multiple source files. * @param files a List<File> to which all the files found will be added * @return the set of Files under a root folder. */ public static void findFilesUnder(File rootFolder, List<File> files) { for (File child : rootFolder.listFiles()) { if ("CVS".equals(child.getName())) { continue; } if (child.isDirectory()) { findFilesUnder(child, files); } else { files.add(child); } } } /** @return the name of the folder where class files will be saved */ public static String getBinFolderName() { return _tmpBinFolderName; } public static JavaCompiler getEclipseCompiler() { return _eclipseCompiler; } /** @return the name of the folder where generated files will be placed */ public static String getGenFolderName() { return _tmpGenFolderName; } /** @return the name of the folder where source files will be found during compilation */ public static String getSrcFolderName() { return _tmpSrcFolderName; } public static String getResourceFolderName() { return RESOURCES_DIR; } /** * Load Eclipse compiler and create temporary directories on disk */ public static void init() { _tmpFolder = System.getProperty("java.io.tmpdir"); if (_tmpFolder.endsWith(File.separator)) { _tmpFolder += "eclipse-temp"; } else { _tmpFolder += (File.separator + "eclipse-temp"); } _tmpBinFolderName = _tmpFolder + File.separator + "bin"; _tmpBinDir = new File(_tmpBinFolderName); BatchTestUtils.deleteTree(_tmpBinDir); // remove existing contents _tmpBinDir.mkdirs(); assert _tmpBinDir.exists() : "couldn't mkdirs " + _tmpBinFolderName; _tmpGenFolderName = _tmpFolder + File.separator + "gen-src"; _tmpGenDir = new File(_tmpGenFolderName); BatchTestUtils.deleteTree(_tmpGenDir); // remove existing contents _tmpGenDir.mkdirs(); assert _tmpGenDir.exists() : "couldn't mkdirs " + _tmpGenFolderName; _tmpSrcFolderName = _tmpFolder + File.separator + "src"; _tmpSrcDir = new File(_tmpSrcFolderName); BatchTestUtils.deleteTree(_tmpSrcDir); // remove existing contents _tmpSrcDir.mkdirs(); assert _tmpSrcDir.exists() : "couldn't mkdirs " + _tmpSrcFolderName; try { _processorJarPath = setupProcessorJar(PROCESSOR_JAR_NAME, _tmpFolder); _jls8ProcessorJarPath = setupProcessorJar(JLS8_PROCESSOR_JAR_NAME, _tmpFolder); } catch (IOException e) { e.printStackTrace(); } junit.framework.TestCase.assertNotNull("No processor jar path set", _processorJarPath); File processorJar = new File(_processorJarPath); junit.framework.TestCase.assertTrue("Couldn't find processor jar at " + processorJar.getAbsolutePath(), processorJar.exists()); ServiceLoader<JavaCompiler> javaCompilerLoader = ServiceLoader.load(JavaCompiler.class);//, EclipseCompiler.class.getClassLoader()); Class<?> c = null; try { c = Class.forName("org.eclipse.jdt.internal.compiler.tool.EclipseCompiler"); } catch (ClassNotFoundException e) { // ignore } if (c == null) { junit.framework.TestCase.assertTrue("Eclipse compiler is not available", false); } int compilerCounter = 0; for (JavaCompiler javaCompiler : javaCompilerLoader) { compilerCounter++; if (c.isInstance(javaCompiler)) { _eclipseCompiler = javaCompiler; } } junit.framework.TestCase.assertEquals("Only one compiler available", 1, compilerCounter); junit.framework.TestCase.assertNotNull("No Eclipse compiler found", _eclipseCompiler); } private static void addProcessorPaths(List<String> options, boolean useJLS8Processors, boolean addToNormalClasspath) { String path = useJLS8Processors ? _jls8ProcessorJarPath : _processorJarPath; if (addToNormalClasspath) { options.add("-cp"); options.add(_tmpSrcFolderName + File.pathSeparator + _tmpGenFolderName + File.pathSeparator + path); } options.add("-processorpath"); options.add(path); } public static void tearDown() { new File(_processorJarPath).deleteOnExit(); new File(_jls8ProcessorJarPath).deleteOnExit(); BatchTestUtils.deleteTree(new File(_tmpFolder)); } protected static String getPluginDirectoryPath() { try { if (Platform.isRunning()) { URL platformURL = Platform.getBundle("org.eclipse.jdt.compiler.apt.tests").getEntry("/"); return new File(FileLocator.toFileURL(platformURL).getFile()).getAbsolutePath(); } return new File(System.getProperty("user.dir")).getAbsolutePath(); } catch (IOException e) { e.printStackTrace(); } return null; } public static byte[] read(java.io.File file) throws java.io.IOException { int fileLength; byte[] fileBytes = new byte[fileLength = (int) file.length()]; java.io.FileInputStream stream = null; try { stream = new java.io.FileInputStream(file); int bytesRead = 0; int lastReadSize = 0; while ((lastReadSize != -1) && (bytesRead != fileLength)) { lastReadSize = stream.read(fileBytes, bytesRead, fileLength - bytesRead); bytesRead += lastReadSize; } } finally { if (stream != null) { stream.close(); } } return fileBytes; } /** * @return true if this file's end-of-line delimiters should be replaced with * a platform-independent value, e.g. for compilation. */ public static boolean shouldConvertToIndependentLineDelimiter(File file) { return file.getName().endsWith(".java"); } /** * Copy a file from one location to another, unless the destination file already exists and has * the same timestamp and file size. Create the destination location if necessary. Convert line * delimiters according to {@link #shouldConvertToIndependentLineDelimiter(File)}. * * @param src * the full path to the resource location. * @param destFolder * the full path to the destination location. * @throws IOException */ public static void copyResource(File src, File dest) throws IOException { if (dest.exists() && src.lastModified() < dest.lastModified() && src.length() == dest.length()) { return; } // read source bytes byte[] srcBytes = null; srcBytes = read(src); if (shouldConvertToIndependentLineDelimiter(src)) { String contents = new String(srcBytes); contents = TestUtils.convertToIndependentLineDelimiter(contents); srcBytes = contents.getBytes(); } File destFolder = dest.getParentFile(); if (!destFolder.exists()) { if (!destFolder.mkdirs()) { throw new IOException("Unable to create directory " + destFolder); } } // write bytes to dest FileOutputStream out = null; try { out = new FileOutputStream(dest); out.write(srcBytes); out.flush(); } finally { if (out != null) { out.close(); } } } /** * Copy a resource that is located under the <code>resources</code> folder of the plugin to a * corresponding location under the specified target folder. Convert line delimiters according * to {@link #shouldConvertToIndependentLineDelimiter(File)}. * * @param resourcePath * the relative path under <code>[plugin-root]/resources</code> of the resource to * be copied * @param targetFolder * the absolute path of the folder under which the resource will be copied. Folder * and subfolders will be created if necessary. * @return a file representing the copied resource * @throws IOException */ public static File copyResource(String resourcePath, File targetFolder) throws IOException { File resDir = new File(getPluginDirectoryPath(), RESOURCES_DIR); File resourceFile = new File(resDir, resourcePath); File targetFile = new File(targetFolder, resourcePath); copyResource(resourceFile, targetFile); return targetFile; } /** * Copy all the files under the directory specified by src to the directory * specified by dest. The src and dest directories must exist; child directories * under dest will be created as required. Existing files in dest will be * overwritten. Newlines will be converted according to * {@link #shouldConvertToIndependentLineDelimiter(File)}. Directories * named "CVS" will be ignored. * @param resourceFolderName the name of the source folder, relative to * <code>[plugin-root]/resources</code> * @param the absolute path of the destination folder * @throws IOException */ public static void copyResources(String resourceFolderName, File destFolder) throws IOException { File resDir = new File(getPluginDirectoryPath(), RESOURCES_DIR); File resourceFolder = new File(resDir, resourceFolderName); copyResources(resourceFolder, destFolder); } private static void copyResources(File resourceFolder, File destFolder) throws IOException { if (resourceFolder == null) { return; } // Copy all resources in this folder String[] children = resourceFolder.list(); if (null == children) { return; } // if there are any children, (recursively) copy them for (String child : children) { if ("CVS".equals(child)) { continue; } File childRes = new File(resourceFolder, child); File childDest = new File(destFolder, child); if (childRes.isDirectory()) { copyResources(childRes, childDest); } else { copyResource(childRes, childDest); } } } public static String setupProcessorJar(String processorJar, String tmpDir) throws IOException { File libDir = new File(getPluginDirectoryPath()); File libFile = new File(libDir, processorJar); File destinationDir = new File(tmpDir); File destinationFile = new File(destinationDir, processorJar); copyResource(libFile, destinationFile); return destinationFile.getCanonicalPath(); } /** * Recursively delete the contents of a directory, including any subdirectories. * This is not optimized to handle very large or deep directory trees efficiently. * @param f is either a normal file (which will be deleted) or a directory * (which will be emptied and then deleted). */ public static void deleteTree(File f) { if (null == f) { return; } File[] children = f.listFiles(); if (null != children) { // if f has any children, (recursively) delete them for (File child : children) { deleteTree(child); } } // At this point f is either a normal file or an empty directory f.delete(); } /** * Check the contents of a file. * @return true if the contents of <code>genTextFile</code> exactly match <code>string</code> */ public static boolean fileContentsEqualText(File genTextFile, String input) { long length = genTextFile.length(); if (length != input.length()) { return false; } char[] contents = new char[512]; Reader r = null; try { r = new BufferedReader(new FileReader(genTextFile)); int ofs = 0; while (length > 0) { int read = r.read(contents); String match = input.substring(ofs, ofs + read); if (!match.contentEquals(new String(contents, 0, read))) { return false; } ofs += read; length -= read; } return true; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (r != null) try { r.close(); } catch (IOException e) { e.printStackTrace(); } } } }