/* * Copyright 2014 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 org.inferred.freebuilder.processor.util.testing; import com.google.common.base.Strings; import com.google.common.collect.Multiset; import com.google.common.truth.Truth; import org.junit.Assert; import org.junit.Test; import java.util.Objects; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; /** * Simple builder API for a test method, suitable for use in {@link BehaviorTester}. See the * JavaDoc on that class for an example. * * <p>Automatically imports {@link Assert}.* and {@link Truth#assertThat}. * * <p>Does some ugly things to get meaningful file names and line numbers in the generated source. * If you invoke build() directly from your test method, the generated class name and method name * should line up with your test. Additionally, if you invoke addLine() directly from your test * method, without loops, the line numbers should line up with your test in Eclipse. * (Specifically, if you invoke addLine with line numbers that increase monotonically, and * do not include extra newlines in your code strings, then the test builder will be able to * generate a source file with those lines at the same line numbers.) Sadly, javac does not * appear to produce such specific line numbers when faced with a fluent API. * * <p>Note that there is no <b>incorrect</b> way to use the class; you will just find compiler * error messages and assertion stack traces are more useful when your test code is simple. */ public class TestBuilder { private final StringBuilder imports = new StringBuilder(); private final StringBuilder code = new StringBuilder(); private int lineNumber = 2; // Our preamble takes up the first line of the source public TestBuilder addStaticImport(Class<?> cls, String method) { return addStaticImport(cls.getCanonicalName(), method); } public TestBuilder addStaticImport(String cls, String method) { imports .append("import static ") .append(cls) .append(".") .append(method) .append("; "); return this; } public TestBuilder addImport(Class<?> cls) { return addImport(cls.getCanonicalName()); } public TestBuilder addImport(String cls) { imports .append("import ") .append(cls) .append("; "); return this; } public TestBuilder addPackageImport(String pkg) { imports .append("import ") .append(pkg) .append(".*; "); return this; } /** * Appends a formatted line of code to the source. Formatting is done by {@link String#format}, * except that {@link Class} instances use their entity's name unadorned, rather than the usual * toString implementation. */ public TestBuilder addLine(String fmt, Object... args) { StackTraceElement caller = new Exception().getStackTrace()[1]; if (caller.getLineNumber() > lineNumber) { code.append(Strings.repeat("\n", caller.getLineNumber() - lineNumber)); lineNumber = caller.getLineNumber(); } Object[] substituteArgs = new Object[args.length]; for (int i = 0; i < args.length; i++) { substituteArgs[i] = SourceBuilder.substitute(args[i]); } String text = String.format(fmt, substituteArgs); lineNumber += text.length() - text.replace("\n", "").length() + 1; code.append(text).append("\n"); return this; } /** * Returns a {@link JavaFileObject} for the test source added to the builder. */ public TestSource build() { StackTraceElement caller = new Exception().getStackTrace()[1]; return new TestSource( rootTestClassName(caller.getClassName()), caller.getMethodName(), imports.toString(), code.toString()); } /** * Creates a unique test class name. Once no longer referenced, it can subsequently be reused, * to keep compiler errors and stack traces cleaner. */ private static String rootTestClassName(String originalClassName) { int periodIndex = originalClassName.lastIndexOf('.'); if (periodIndex != -1) { return originalClassName.substring(0, periodIndex) + ".generatedcode" + originalClassName.substring(periodIndex); } else { return "com.example.test.generatedcode.Test"; } } public static class TestSource { private final String rootClassName; private final String methodName; private final String imports; private final String testCode; private TestSource(String rootClassName, String methodName, String imports, String testCode) { this.rootClassName = rootClassName; this.methodName = methodName; this.imports = imports; this.testCode = testCode; } TestFile selectName(Multiset<String> seenNames) { long id = seenNames.add(rootClassName, 1) + 1; String name = rootClassName + (id == 1 ? "" : "__" + id); return new TestFile(name, methodName, imports, testCode); } @Override public String toString() { return rootClassName + "." + methodName; } @Override public int hashCode() { return Objects.hash(rootClassName, methodName, imports, testCode); } @Override public boolean equals(Object obj) { if (!(obj instanceof TestSource)) { return false; } TestSource other = (TestSource) obj; return Objects.equals(rootClassName, other.rootClassName) && Objects.equals(methodName, other.methodName) && Objects.equals(imports, other.imports) && Objects.equals(testCode, other.testCode); } } /** * In-memory implementation of {@link javax.tools.JavaFileObject JavaFileObject} for test code. */ static class TestFile extends SimpleJavaFileObject { private final String className; private final String methodName; private final String source; private TestFile(String className, String methodName, String imports, String testCode) { super(SourceBuilder.uriForClass(className), Kind.SOURCE); this.className = className; this.methodName = methodName; int period = className.lastIndexOf('.'); this.source = "package " + className.substring(0, period) + "; " + "import static " + Assert.class.getName() + ".*; " + "import static " + Truth.class.getName() + ".assertThat; " + imports + "public class " + className.substring(period + 1) + " {" + " @" + Test.class.getName() + " public static void " + methodName + "() throws Exception {\n" + testCode + "\n }\n}"; } /** * Gets the character content of this file object. */ @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return source; } public String getClassName() { return className; } public String getMethodName() { return methodName; } } }