/*
* Copyright 2011 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.devtools.j2objc;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.google.devtools.j2objc.ast.CompilationUnit;
import com.google.devtools.j2objc.ast.MethodDeclaration;
import com.google.devtools.j2objc.ast.Statement;
import com.google.devtools.j2objc.ast.TreeVisitor;
import com.google.devtools.j2objc.file.RegularInputFile;
import com.google.devtools.j2objc.gen.GenerationUnit;
import com.google.devtools.j2objc.gen.SourceBuilder;
import com.google.devtools.j2objc.gen.StatementGenerator;
import com.google.devtools.j2objc.pipeline.GenerationBatch;
import com.google.devtools.j2objc.pipeline.InputFilePreprocessor;
import com.google.devtools.j2objc.pipeline.ProcessingContext;
import com.google.devtools.j2objc.pipeline.TranslationProcessor;
import com.google.devtools.j2objc.util.CodeReferenceMap;
import com.google.devtools.j2objc.util.ElementUtil;
import com.google.devtools.j2objc.util.ErrorUtil;
import com.google.devtools.j2objc.util.FileUtil;
import com.google.devtools.j2objc.util.NameTable;
import com.google.devtools.j2objc.util.Parser;
import com.google.devtools.j2objc.util.TimeTracker;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.framework.TestCase;
/**
* Tests code generation. A string containing the source code for a list of Java
* statements is parsed and translated, then iOS code is generated for one or
* more of those statements for comparison in a specific generation test.
*
* @author Tom Ball
*/
public class GenerationTest extends TestCase {
protected File tempDir;
protected Parser parser;
protected Options options;
private CodeReferenceMap deadCodeMap = null;
static {
// Prevents errors and warnings from being printed to the console.
ErrorUtil.setTestMode();
ClassLoader cl = GenerationTest.class.getClassLoader();
cl.setPackageAssertionStatus("com.google.devtools.j2objc", true);
}
@Override
protected void setUp() throws IOException {
tempDir = FileUtil.createTempDir("testout");
loadOptions();
createParser();
}
@Override
protected void tearDown() throws Exception {
options = null;
if (parser != null) {
parser.close();
}
FileUtil.deleteTempDir(tempDir);
ErrorUtil.reset();
}
protected void loadOptions() throws IOException {
options = new Options();
options.load(new String[]{
"-d", tempDir.getAbsolutePath(),
"-sourcepath", tempDir.getAbsolutePath(),
"-q", // Suppress console output.
"-encoding", "UTF-8" // Translate strings correctly when encodings are nonstandard.
});
}
protected void createParser() {
parser = initializeParser(tempDir, options);
}
protected static Parser initializeParser(File tempDir, Options options) {
Parser parser = Parser.newParser(options);
parser.addClasspathEntries(getComGoogleDevtoolsJ2objcPath());
parser.addSourcepathEntry(tempDir.getAbsolutePath());
return parser;
}
protected void setDeadCodeMap(CodeReferenceMap deadCodeMap) {
this.deadCodeMap = deadCodeMap;
}
protected void addSourcesToSourcepaths() throws IOException {
options.fileUtil().getSourcePathEntries().add(tempDir.getCanonicalPath());
}
/**
* Translate a string of Java statement(s) into a list of
* JDT DOM Statements. Although JDT has support for statement
* parsing, it doesn't resolve them. The statements are therefore
* wrapped in a type declaration so they having bindings.
*/
protected List<Statement> translateStatements(String stmts) {
// Wrap statements in test class, so type resolution works.
String source = "public class Test { void test() { " + stmts + "}}";
CompilationUnit unit = translateType("Test", source);
final List<Statement> statements = Lists.newArrayList();
unit.accept(new TreeVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
if (ElementUtil.getName(node.getExecutableElement()).equals("test")) {
statements.addAll(node.getBody().getStatements());
}
return false;
}
});
return statements;
}
/**
* Translates Java source, as contained in a source file.
*
* @param typeName the name of the public type being declared
* @param source the source code
* @return the translated compilation unit
*/
protected CompilationUnit translateType(String typeName, String source) {
CompilationUnit newUnit = compileType(typeName, source);
TranslationProcessor.applyMutations(newUnit, deadCodeMap, TimeTracker.noop());
return newUnit;
}
/**
* Compiles Java source, as contained in a source file.
*
* @param name the name of the public type being declared
* @param source the source code
* @return the parsed compilation unit
*/
protected CompilationUnit compileType(String name, String source) {
String mainTypeName = name.substring(name.lastIndexOf('.') + 1);
String path = name.replace('.', '/') + ".java";
int errors = ErrorUtil.errorCount();
parser.setEnableDocComments(options.docCommentsEnabled());
CompilationUnit unit = parser.parse(mainTypeName, path, source);
if (ErrorUtil.errorCount() > errors) {
int newErrorCount = ErrorUtil.errorCount() - errors;
String info = String.format(
"%d test compilation error%s", newErrorCount, (newErrorCount == 1 ? "" : "s"));
failWithMessages(info, ErrorUtil.getErrorMessages().subList(errors, ErrorUtil.errorCount()));
}
return unit;
}
protected static List<String> getComGoogleDevtoolsJ2objcPath() {
ClassLoader loader = GenerationTest.class.getClassLoader();
List<String> classpath = Lists.newArrayList();
if (loader instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) GenerationTest.class.getClassLoader()).getURLs();
String encoding = System.getProperty("file.encoding");
for (int i = 0; i < urls.length; i++) {
try {
classpath.add(URLDecoder.decode(urls[i].getFile(), encoding));
} catch (UnsupportedEncodingException e) {
throw new AssertionError("System doesn't have the default encoding");
}
}
}
return classpath;
}
protected String generateStatement(Statement statement) {
return StatementGenerator.generate(statement, SourceBuilder.BEGINNING_OF_FILE).trim();
}
/**
* Asserts that translated source contains a specified string.
*/
protected void assertTranslation(String translation, String expected) {
if (!translation.contains(expected)) {
fail("expected:\"" + expected + "\" in:\n" + translation);
}
}
protected void assertNotInTranslation(String translation, String notExpected) {
if (translation.contains(notExpected)) {
fail("NOT expected:\"" + notExpected + "\" in:\n" + translation);
}
}
/**
* Asserts that translated source contains an ordered, consecutive list of lines
* (each line's leading and trailing whitespace is ignored).
*/
protected void assertTranslatedLines(String translation, String... expectedLines)
throws IOException {
int nLines = expectedLines.length;
if (nLines < 2) {
assertTranslation(translation, nLines == 1 ? expectedLines[0] : null);
return;
}
int unmatchedLineIndex = unmatchedLineIndex(translation, expectedLines);
if (unmatchedLineIndex != -1) {
fail("unmatched:\n\"" + expectedLines[unmatchedLineIndex] + "\"\n" + "expected lines:\n\""
+ Joiner.on('\n').join(expectedLines) + "\"\nin:\n" + translation);
}
}
private int unmatchedLineIndex(String s, String[] lines) throws IOException {
int index = s.indexOf(lines[0]);
if (index == -1) {
return 0;
}
BufferedReader in = new BufferedReader(new StringReader(s.substring(index)));
try {
for (int i = 0; i < lines.length; i++) {
String nextLine = in.readLine();
if (nextLine == null) {
return i;
}
if (!nextLine.trim().equals(lines[i].trim())) {
// Check if there is a subsequent match.
int subsequentMatch = unmatchedLineIndex(s.substring(index + 1), lines);
if (subsequentMatch == -1) {
return -1;
}
return Math.max(i, subsequentMatch);
}
}
return -1;
} finally {
in.close();
}
}
/**
* Asserts that translated source contains a list of strings in order, but not necessarily
* consecutive. Differs from assertTranslatedLines in that it doesn't match entire lines, and that
* matches may occur anywhere forward in the string from the last match, not solely in the next
* line.
*/
protected void assertTranslatedSegments(String translation, String... expectedLines)
throws IOException {
int nLines = expectedLines.length;
if (nLines < 2) {
assertTranslation(translation, nLines == 1 ? expectedLines[0] : null);
return;
}
String incorrectSegment = firstIncorrectSegment(translation, expectedLines);
if (incorrectSegment != null) {
fail("unmatched:\n\"" + incorrectSegment + "\"\n" + "expected segments:\n\""
+ Joiner.on('\n').join(expectedLines) + "\"\nin:\n" + translation);
}
}
private String firstIncorrectSegment(String s, String[] lines) {
int index = 0;
for (int i = 0; i < lines.length; i++) {
index = s.indexOf(lines[i], index);
if (index == -1) {
return lines[i];
} else {
index += lines[i].length();
}
}
return null;
}
protected void assertOccurrences(String translation, String expected, int times) {
Matcher matcher = Pattern.compile(Pattern.quote(expected)).matcher(translation);
int count = 0;
for (; matcher.find(); count++) {}
if (count != times) {
fail("expected:\"" + expected + "\" " + times + " times in:\n" + translation);
}
}
/**
* Translate a Java method into a JDT DOM MethodDeclaration. Although JDT
* has support for parsing methods, it doesn't resolve them. The statements
* are therefore wrapped in a type declaration so they having bindings.
*/
protected MethodDeclaration translateMethod(String method) {
// Wrap statements in test class, so type resolution works.
String source = "public class Test { " + method + " }";
CompilationUnit unit = translateType("Test", source);
final MethodDeclaration[] result = new MethodDeclaration[1];
unit.accept(new TreeVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
String name = ElementUtil.getName(node.getExecutableElement());
if (name.equals(NameTable.INIT_NAME)
|| name.equals(NameTable.FINALIZE_METHOD)
|| name.equals(NameTable.DEALLOC_METHOD)) {
return false;
}
assert result[0] == null;
result[0] = node;
return false;
}
});
return result[0];
}
/**
* Translate a Java source file contents, returning the contents of either
* the generated header or implementation file.
*
* @param typeName the name of the main type defined by this source file
* @param fileName the name of the file whose contents should be returned,
* which is either the Obj-C header or implementation file
*/
protected String translateSourceFile(String typeName, String fileName) throws IOException {
String source = getTranslatedFile(typeName.replace('.', '/') + ".java");
return translateSourceFile(source, typeName, fileName);
}
/**
* Translate a Java source file contents, returning the contents of either
* the generated header or implementation file.
*
* @param source the source file contents
* @param typeName the name of the main type defined by this source file
* @param fileName the name of the file whose contents should be returned,
* which is either the Obj-C header or implementation file
*/
protected String translateSourceFile(String source, String typeName, String fileName)
throws IOException {
return generateFromUnit(translateType(typeName, source), fileName);
}
protected String generateFromUnit(CompilationUnit unit, String filename) throws IOException {
GenerationUnit genUnit = new GenerationUnit(unit.getSourceFilePath(), options);
genUnit.incrementInputs();
genUnit.addCompilationUnit(unit);
TranslationProcessor.generateObjectiveCSource(genUnit);
return getTranslatedFile(filename);
}
protected String translateCombinedFiles(String outputPath, String extension, String... sources)
throws IOException {
List<ProcessingContext> inputs = new ArrayList<>();
GenerationUnit genUnit = GenerationUnit.newCombinedJarUnit(outputPath + ".testfile", options);
for (String sourceFile : sources) {
inputs.add(new ProcessingContext(
new RegularInputFile(tempDir + "/" + sourceFile, sourceFile), genUnit));
}
parser.setEnableDocComments(options.docCommentsEnabled());
new InputFilePreprocessor(parser).processInputs(inputs);
new TranslationProcessor(parser, CodeReferenceMap.builder().build()).processInputs(inputs);
return getTranslatedFile(outputPath + extension);
}
protected void runPipeline(String... files) {
J2ObjC.run(Arrays.asList(files), options);
assertErrorCount(0);
assertWarningCount(0);
}
protected void loadHeaderMappings() {
options.getHeaderMap().loadMappings();
}
protected void preprocessFiles(String... fileNames) {
GenerationBatch batch = new GenerationBatch(options);
for (String fileName : fileNames) {
batch.addSource(new RegularInputFile(
tempDir.getPath() + File.separatorChar + fileName, fileName));
}
new InputFilePreprocessor(parser).processInputs(batch.getInputs());
}
protected String addSourceFile(String source, String fileName) throws IOException {
File file = new File(tempDir, fileName);
file.getParentFile().mkdirs();
Files.write(source, file, options.fileUtil().getCharset());
return file.getPath();
}
/**
* Return the contents of a previously translated file, made by a call to
* {@link #translateMethod} above.
*/
protected String getTranslatedFile(String fileName) throws IOException {
File f = new File(tempDir, fileName);
assertTrue(fileName + " not generated", f.exists());
return Files.toString(f, options.fileUtil().getCharset());
}
/**
* When running Java tests in a build, there is no formal guarantee that these resources
* be available as filesystem files. This copies a resource to a file in the temp dir,
* and returns the new path.
* The given resource name is relative to the class to which this method belongs
* (which might be a subclass of GenerationTest, in a different package).
*/
public String getResourceAsFile(String resourceName) throws IOException {
URL url;
try {
url = getClass().getResource(resourceName).toURI().toURL();
} catch (URISyntaxException e) {
throw new IOException(e);
}
File file = new File(tempDir + "/resources/"
+ getClass().getPackage().getName().replace('.', File.separatorChar)
+ File.separatorChar + resourceName);
file.getParentFile().mkdirs();
OutputStream ostream = new FileOutputStream(file);
Resources.copy(url, ostream);
return file.getPath();
}
/**
* Asserts that the correct number of warnings were reported during the
* last translation.
*/
protected void assertWarningCount(int expectedCount) {
if (expectedCount != ErrorUtil.warningCount()) {
failWithMessages(
String.format("Wrong number of warnings. Expected:%d but was:%d",
expectedCount, ErrorUtil.warningCount()),
ErrorUtil.getWarningMessages());
}
}
/**
* Asserts that the correct number of errors were reported during the
* last translation.
*/
protected void assertErrorCount(int expectedCount) {
if (expectedCount != ErrorUtil.errorCount()) {
failWithMessages(
String.format("Wrong number of errors. Expected:%d but was:%d",
expectedCount, ErrorUtil.errorCount()),
ErrorUtil.getErrorMessages());
}
}
private void failWithMessages(String info, List<String> messages) {
StringBuilder sb = new StringBuilder(info + "\n");
for (String msg : messages) {
sb.append(msg).append('\n');
}
fail(sb.toString());
}
protected String getTempDir() {
return tempDir.getPath();
}
protected File getTempFile(String filename) {
return new File(tempDir, filename);
}
protected void addJarFile(String jarFileName, String... sources) throws IOException {
File jarFile = getTempFile(jarFileName);
jarFile.getParentFile().mkdirs();
options.fileUtil().appendSourcePath(jarFile.getPath());
JarOutputStream jar = new JarOutputStream(new FileOutputStream(jarFile));
try {
for (int i = 0; i < sources.length; i += 2) {
String name = sources[i];
String source = sources[i + 1];
JarEntry fooEntry = new JarEntry(name);
jar.putNextEntry(fooEntry);
jar.write(source.getBytes(Charset.defaultCharset()));
jar.closeEntry();
}
} finally {
jar.close();
}
}
// Empty test so Bazel won't report a "no tests" error.
public void testNothing() {}
}