package com.oracle.truffle.tools.coverage.impl;
import static com.oracle.truffle.api.instrumentation.TruffleInstrument.Registration;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.function.Function;
import org.graalvm.options.OptionCategory;
import org.graalvm.options.OptionDescriptors;
import org.graalvm.options.OptionKey;
import org.graalvm.options.OptionStability;
import org.graalvm.options.OptionType;
import org.graalvm.options.OptionValues;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Instrument;
import com.oracle.truffle.api.Option;
import com.oracle.truffle.api.instrumentation.SourceSectionFilter;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.tools.coverage.CoverageTracker;
import com.oracle.truffle.tools.coverage.SourceCoverage;
@Registration(id = CoverageInstrument.ID, name = "Code Coverage", version = CoverageInstrument.VERSION, services = CoverageTracker.class)
public class CoverageInstrument extends TruffleInstrument {
public static final String ID = "coverage";
static final String VERSION = "0.1.0";
static final OptionType<Output> CLI_OUTPUT_TYPE = new OptionType<>("Output",
new Function<String, Output>() {
@Override
public Output apply(String s) {
try {
return Output.valueOf(s.toUpperCase());
} catch (IllegalArgumentException e) {
StringBuilder message = new StringBuilder("Output can be one of: ");
for (Output output : Output.values()) {
message.append(output.toString().toLowerCase());
message.append(" ");
}
throw new IllegalArgumentException(message.toString());
}
}
});
@Option(name = "", help = "Enable Coverage (default: false).", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<Boolean> ENABLED = new OptionKey<>(false);
@Option(help = "Keep count of each element's coverage (default: false).", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<Boolean> Count = new OptionKey<>(false);
@Option(name = "Output", help = "Can be: human readable 'histogram' (per file coverage summary) or 'detailed' (per line coverage summary), machine readable 'json', tool compliant 'lcov'. (default: histogram)",
category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<Output> OUTPUT = new OptionKey<>(Output.HISTOGRAM, CLI_OUTPUT_TYPE);
@Option(name = "FilterRootName", help = "Wildcard filter for program roots. (eg. Math.*, default:*).", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<Object[]> FILTER_ROOT = new OptionKey<>(new Object[0], WildcardHandler.WILDCARD_FILTER_TYPE);
@Option(name = "FilterFile", help = "Wildcard filter for source file paths. (eg. *program*.sl, default:*).", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<Object[]> FILTER_FILE = new OptionKey<>(new Object[0], WildcardHandler.WILDCARD_FILTER_TYPE);
@Option(name = "FilterMimeType", help = "Only track languages with mime-type. (eg. +, default:no filter).", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<String> FILTER_MIME_TYPE = new OptionKey<>("");
@Option(name = "FilterLanguage", help = "Only track languages with given ID. (eg. js, default:no filter).", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<String> FILTER_LANGUAGE = new OptionKey<>("");
@Option(name = "TrackInternal", help = "Track internal elements (default:false).", category = OptionCategory.INTERNAL)
static final OptionKey<Boolean> TRACK_INTERNAL = new OptionKey<>(false);
@Option(name = "OutputFile", help = "Save output to the given file. Output is printed to standard output stream by default.", category = OptionCategory.USER, stability = OptionStability.STABLE)
static final OptionKey<String> OUTPUT_FILE = new OptionKey<>("");
@Option(help = "Consider a source code line covered only if covered in it's entirety. (default: true)", category = OptionCategory.USER, stability = OptionStability.EXPERIMENTAL)
static final OptionKey<Boolean> StrictLines = new OptionKey<>(true);
private static Function<Env, CoverageTracker> factory;
static {
try {
Class.forName(CoverageTracker.class.getName(), true, CoverageTracker.class.getClassLoader());
} catch (ClassNotFoundException cannotHappen) {
throw new AssertionError();
}
}
private CoverageTracker tracker;
private Boolean enabled;
public static CoverageTracker getTracker(Engine engine) {
Instrument instrument = engine.getInstruments().get(ID);
if (instrument == null) {
throw new CoverageException("Tracker is not installed.");
}
return instrument.lookup(CoverageTracker.class);
}
public static void setFactory(Function<Env, CoverageTracker> factory) {
if (factory == null || !factory.getClass().getName().startsWith("com.oracle.truffle.tools.coverage")) {
throw new IllegalArgumentException("Wrong factory: " + factory);
}
CoverageInstrument.factory = factory;
}
private static PrintStream chooseOutputStream(TruffleInstrument.Env env, OptionKey<String> option) {
try {
if (option.hasBeenSet(env.getOptions())) {
final String outputPath = option.getValue(env.getOptions());
final File file = new File(outputPath);
if (file.exists()) {
throw new CoverageException("Cannot redirect output to an existing file!");
}
return new PrintStream(new FileOutputStream(file));
} else {
return new PrintStream(env.out());
}
} catch (FileNotFoundException e) {
throw new CoverageException("Cannot redirect output to a directory");
}
}
private static SourceSectionFilter getSourceSectionFilter(OptionValues options) {
final Object[] filterFile = FILTER_FILE.getValue(options);
final String filterMimeType = FILTER_MIME_TYPE.getValue(options);
final String filterLanguage = FILTER_LANGUAGE.getValue(options);
final Boolean internals = TRACK_INTERNAL.getValue(options);
final SourceSectionFilter.Builder builder = SourceSectionFilter.newBuilder();
builder.sourceIs(source -> {
boolean internal = (internals || !source.isInternal());
boolean file = WildcardHandler.testWildcardExpressions(source.getPath(), filterFile);
boolean mimeType = filterMimeType.equals("") || filterMimeType.equals(source.getMimeType());
final boolean languageId = filterLanguage.equals("") || filterMimeType.equals(source.getLanguage());
return internal && file && mimeType && languageId;
});
final Object[] filterRootName = FILTER_ROOT.getValue(options);
builder.rootNameIs(s -> WildcardHandler.testWildcardExpressions(s, filterRootName));
return builder.build();
}
@Override
protected void onCreate(Env env) {
tracker = factory.apply(env);
env.registerService(tracker);
final OptionValues options = env.getOptions();
enabled = ENABLED.getValue(options);
if (enabled) {
tracker.start(new CoverageTracker.Config(getSourceSectionFilter(options), Count.getValue(options)));
}
}
@Override
protected void onDispose(Env env) {
if (enabled) {
SourceCoverage[] coverage = tracker.getCoverage();
final OptionValues options = env.getOptions();
final boolean strictLines = StrictLines.getValue(options);
try (PrintStream out = chooseOutputStream(env, OUTPUT_FILE)) {
switch (OUTPUT.getValue(options)) {
case HISTOGRAM:
new CoverageCLI(out, coverage, strictLines).printHistogramOutput();
break;
case DETAILED:
new CoverageCLI(out, coverage, strictLines).printLinesOutput();
break;
case JSON:
new JSONPrinter(out, coverage).print();
break;
case LCOV:
new LCOVPrinter(out, coverage, strictLines).print();
break;
}
}
tracker.close();
}
}
@Override
protected OptionDescriptors getOptionDescriptors() {
return new CoverageInstrumentOptionDescriptors();
}
enum Output {
HISTOGRAM,
DETAILED,
JSON,
LCOV,
}
}