/*
* Joinery -- Data frames for Java
* Copyright (c) 2014, 2015 IBM Corp.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package joinery.doctest;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.security.SecureClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticListener;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.Doclet;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
public class DocTestSuite
extends Suite {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface DocTestSourceDirectory {
public String value();
}
public static final class DocTestDoclet
extends Doclet {
private static Map<String, Runner> doctests = new LinkedHashMap<>();
public static LanguageVersion languageVersion() {
return LanguageVersion.JAVA_1_5;
}
private static void generateRunner(
final ClassDoc cls, final ProgramElementDoc member, final Tag tag) {
final List<String> lines = new LinkedList<>();
final StringBuilder sb = new StringBuilder();
String assertion = null;
try (BufferedReader reader = new BufferedReader(new StringReader(tag.text()))) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
line = line.trim();
final int index = line.indexOf(">") + 1;
if (index > 0) {
lines.add(line.substring(index));
} else if (lines.size() > 0) {
if (line.length() > 0) {
// record the assertion value
assertion = line.trim();
// inject return before last statement
int last;
for (last = lines.size() - 1; last > 0; last--) {
final String check = lines.get(last - 1);
if (check.contains(";") && !check.contains("return")) {
break;
}
}
lines.set(last, "return " + lines.get(last));
}
break;
}
}
} catch (final IOException ex) {
throw new IllegalStateException("error reading string", ex);
}
// TODO extract imports and insert at beginning of generated class
for (final String line : lines) {
sb.append(line).append("\n");
}
if (assertion == null) {
sb.append("return null;\n");
}
final String expected = assertion;
doctests.put(member.toString(), new Runner() {
@Override
public Description getDescription() {
final String sig = member instanceof ExecutableMemberDoc ?
ExecutableMemberDoc.class.cast(member).flatSignature() : "";
return Description.createTestDescription(
cls.qualifiedName(), member.name() + sig);
}
@Override
public void run(final RunNotifier notifier) {
notifier.fireTestStarted(getDescription());
Object value = null;
try {
final String name = String.format("%sDocTest", cls.name());
final String source =
"import " + cls.qualifiedName() + ";\n" +
"import " + cls.qualifiedName() + ".*;\n" +
"import java.util.*;\n" +
"public class " + name + "\n" +
"implements java.util.concurrent.Callable<Object> {\n" +
" public Object call() throws Exception {\n" +
sb.toString() + "\n" +
" }\n" +
"}\n";
final URI uri = URI.create(name + Kind.SOURCE.extension);
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
final JavaFileManager mgr = new ForwardingJavaFileManager<JavaFileManager>(
compiler.getStandardFileManager(null, null, null)) {
private final Map<String, ByteArrayOutputStream> classes =
new HashMap<>();
@Override
public ClassLoader getClassLoader(final Location location) {
return new SecureClassLoader() {
@Override
protected Class<?> findClass(final String name)
throws ClassNotFoundException {
final ByteArrayOutputStream bytes = classes.get(name);
return super.defineClass(
name, bytes.toByteArray(), 0, bytes.size());
}
};
}
@Override
public JavaFileObject getJavaFileForOutput(
final Location location, final String className,
final Kind kind, final FileObject sibling)
throws IOException {
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
classes.put(className, bytes);
return new SimpleJavaFileObject(URI.create(className), kind) {
@Override
public OutputStream openOutputStream()
throws IOException {
return bytes;
}
};
}
};
final List<JavaFileObject> files =
Collections.<JavaFileObject>singletonList(
new SimpleJavaFileObject(uri, Kind.SOURCE) {
@Override
public CharSequence getCharContent(final boolean ignore) {
return source;
}
});
final StringBuilder error = new StringBuilder();
final DiagnosticListener<FileObject> diags =
new DiagnosticListener<FileObject>() {
@Override
public void report(final Diagnostic<? extends FileObject> diagnostic) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
error.append("\t");
error.append(diagnostic.getMessage(null));
error.append("\n");
}
}
};
compiler.getTask(null, mgr, diags, null, null, files).call();
if (error.length() > 0) {
throw new Exception("Doctest failed to compile:\n" + error.toString());
}
final Class<?> cls = mgr.getClassLoader(null).loadClass(name);
value = Callable.class.cast(cls.newInstance()).call();
if (expected != null) {
org.junit.Assert.assertEquals(expected, String.valueOf(value));
}
} catch (final AssertionError err) {
notifier.fireTestFailure(new Failure(getDescription(), err));
} catch (final Exception ex) {
notifier.fireTestFailure(new Failure(getDescription(), ex));
} finally {
notifier.fireTestFinished(getDescription());
}
}
});
}
public static boolean start(final RootDoc root) {
for (final ClassDoc cls : root.classes()) {
final List<ProgramElementDoc> elements = new LinkedList<>();
elements.add(cls);
elements.addAll(Arrays.asList(cls.constructors()));
elements.addAll(Arrays.asList(cls.methods()));
for (final ProgramElementDoc elem : elements) {
for (final Tag tag : elem.inlineTags()) {
final String name = tag.name();
if (name.equals("@code") && tag.text().trim().startsWith(">")) {
generateRunner(cls, elem, tag);
}
}
}
}
return true;
}
}
private static List<Runner> generateDocTestClasses(final Class<?> cls)
throws InitializationError {
final DocTestSourceDirectory dir = cls.getAnnotation(DocTestSourceDirectory.class);
final SuiteClasses suiteClasses = cls.getAnnotation(SuiteClasses.class);
if (suiteClasses == null) {
throw new InitializationError(String.format(
"class '%s' must have a SuiteClasses annotation",
cls.getName()
));
}
for (final Class<?> c : suiteClasses.value()) {
final String source = dir.value() + "/" +
c.getName().replaceAll("\\.", "/") +
Kind.SOURCE.extension;
com.sun.tools.javadoc.Main.execute(
DocTestDoclet.class.getSimpleName(),
DocTestDoclet.class.getName(),
new String[] { source }
);
}
return new LinkedList<>(DocTestDoclet.doctests.values());
}
public DocTestSuite(final Class<?> cls, final RunnerBuilder builder)
throws InitializationError {
super(cls, generateDocTestClasses(cls));
}
}