1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-14 12:10:30 +00:00

Move coverage to the Java side

While slightly irritating (requires Cobalt magic), it's much, much
faster.
This commit is contained in:
Jonathan Coates 2023-01-12 21:02:33 +00:00
parent 2457a31728
commit 28a55349a9
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
3 changed files with 103 additions and 39 deletions

View File

@ -12,20 +12,32 @@ import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.ComputerThread;
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.MachineEnvironment;
import dan200.computercraft.core.lua.MachineResult;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.function.Executable;
import org.opentest4j.AssertionFailedError;
import org.opentest4j.TestAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.*;
import org.squiddev.cobalt.debug.DebugFrame;
import org.squiddev.cobalt.debug.DebugHook;
import org.squiddev.cobalt.debug.DebugState;
import org.squiddev.cobalt.function.OneArgFunction;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
@ -36,6 +48,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@ -78,7 +91,7 @@ public class ComputerTestDelegate {
private final Condition hasFinished = lock.newCondition();
private boolean finished = false;
private Map<String, Map<Double, Double>> finishedWith;
private final Map<LuaString, Int2IntArrayMap> coverage = new HashMap<>();
@BeforeEach
public void before() throws IOException {
@ -99,7 +112,7 @@ public class ComputerTestDelegate {
}
var environment = new BasicEnvironment(mount);
context = new ComputerContext(environment, 1, new NoWorkMainThreadScheduler());
context = new ComputerContext(environment, new ComputerThread(1), new NoWorkMainThreadScheduler(), CoverageLuaMachine::new);
computer = new Computer(context, environment, term, 0);
computer.getEnvironment().setPeripheral(ComputerSide.TOP, new FakeModem());
computer.getEnvironment().setPeripheral(ComputerSide.BOTTOM, new FakePeripheralHub());
@ -137,10 +150,12 @@ public class ComputerTestDelegate {
computer.shutdown();
}
if (finishedWith != null) {
if (!coverage.isEmpty()) {
Files.createDirectories(REPORT_PATH.getParent());
try (var writer = Files.newBufferedWriter(REPORT_PATH)) {
new LuaCoverage(finishedWith).write(writer);
new LuaCoverage(coverage.entrySet().stream().collect(Collectors.toMap(
x -> x.getKey().substring(1).toString(), Map.Entry::getValue
))).write(writer);
}
}
}
@ -414,7 +429,9 @@ public class ComputerTestDelegate {
switch (status) {
case "ok":
break;
case "pending":
runResult = new TestAbortedException("Test is pending");
break;
case "fail":
runResult = new AssertionFailedError(wholeMessage.toString());
@ -432,9 +449,7 @@ public class ComputerTestDelegate {
}
@LuaFunction
public final void finish(Optional<Map<?, ?>> result) {
@SuppressWarnings("unchecked")
var finishedResult = (Map<String, Map<Double, Double>>) result.orElse(null);
public final void finish() {
LOG.info("Finished");
// Signal to after that execution has finished
@ -445,7 +460,6 @@ public class ComputerTestDelegate {
}
try {
finished = true;
if (finishedResult != null) finishedWith = finishedResult;
hasFinished.signal();
} finally {
@ -453,4 +467,73 @@ public class ComputerTestDelegate {
}
}
}
/**
* A subclass of {@link CobaltLuaMachine} which tracks coverage for executed files.
* <p>
* This is a super nasty hack, but is also an order of magnitude faster than tracking this in Lua.
*/
private class CoverageLuaMachine extends CobaltLuaMachine {
CoverageLuaMachine(MachineEnvironment environment) {
super(environment);
}
@Override
public MachineResult loadBios(InputStream bios) {
var result = super.loadBios(bios);
if (result != MachineResult.OK) return result;
LuaTable globals;
LuaThread mainRoutine;
try {
var globalField = CobaltLuaMachine.class.getDeclaredField("globals");
globalField.setAccessible(true);
globals = (LuaTable) globalField.get(this);
var threadField = CobaltLuaMachine.class.getDeclaredField("mainRoutine");
threadField.setAccessible(true);
mainRoutine = (LuaThread) threadField.get(this);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Cannot get internal Cobalt state", e);
}
var coverage = ComputerTestDelegate.this.coverage;
var hook = new DebugHook() {
@Override
public void onCall(LuaState state, DebugState ds, DebugFrame frame) {
}
@Override
public void onReturn(LuaState state, DebugState ds, DebugFrame frame) {
}
@Override
public void onCount(LuaState state, DebugState ds, DebugFrame frame) {
}
@Override
public void onLine(LuaState state, DebugState ds, DebugFrame frame, int newLine) {
if (frame.closure == null) return;
var proto = frame.closure.getPrototype();
if (!proto.source.startsWith('@')) return;
var map = coverage.computeIfAbsent(proto.source, x -> new Int2IntArrayMap());
map.put(newLine, map.get(newLine) + 1);
}
};
((LuaTable) globals.rawget("coroutine")).rawset("create", new OneArgFunction() {
@Override
public LuaValue call(LuaState state, LuaValue arg) throws LuaError {
var thread = new LuaThread(state, arg.checkFunction(), state.getCurrentThread().getfenv());
thread.getDebugState().setHook(hook, false, true, false, 0);
return thread;
}
});
mainRoutine.getDebugState().setHook(hook, false, true, false, 0);
return MachineResult.OK;
}
}
}

View File

@ -5,7 +5,7 @@
*/
package dan200.computercraft.core;
import com.google.common.base.Strings;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.slf4j.Logger;
@ -30,20 +30,20 @@ class LuaCoverage {
private static final Path MULTISHELL = ROOT.resolve("rom/programs/advanced/multishell.lua");
private static final Path TREASURE = ROOT.resolve("treasure");
private final Map<String, Map<Double, Double>> coverage;
private final Map<String, Int2IntMap> coverage;
private final String blank;
private final String zero;
private final String countFormat;
LuaCoverage(Map<String, Map<Double, Double>> coverage) {
LuaCoverage(Map<String, Int2IntMap> coverage) {
this.coverage = coverage;
var max = (int) coverage.values().stream()
.flatMapToDouble(x -> x.values().stream().mapToDouble(y -> y))
var max = coverage.values().stream()
.flatMapToInt(x -> x.values().intStream())
.max().orElse(0);
var maxLen = Math.max(1, (int) Math.ceil(Math.log10(max)));
blank = Strings.repeat(" ", maxLen + 1);
zero = Strings.repeat("*", maxLen) + "0";
blank = " ".repeat(maxLen + 1);
zero = "*".repeat(maxLen) + "0";
countFormat = "%" + (maxLen + 1) + "d";
}
@ -62,8 +62,8 @@ class LuaCoverage {
);
var files = possiblePaths
.filter(Objects::nonNull)
.flatMap(x -> x.entrySet().stream())
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue, Double::sum));
.flatMap(x -> x.int2IntEntrySet().stream())
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
try {
writeCoverageFor(out, path, files);
@ -78,7 +78,7 @@ class LuaCoverage {
}
}
private void writeCoverageFor(Writer out, Path fullName, Map<Double, Double> visitedLines) throws IOException {
private void writeCoverageFor(Writer out, Path fullName, Map<Integer, Integer> visitedLines) throws IOException {
if (!Files.exists(fullName)) {
LOG.error("Cannot locate file {}", fullName);
return;
@ -96,9 +96,9 @@ class LuaCoverage {
var lineNo = 0;
while ((line = reader.readLine()) != null) {
lineNo++;
var count = visitedLines.get((double) lineNo);
var count = visitedLines.get(lineNo);
if (count != null) {
out.write(String.format(countFormat, count.intValue()));
out.write(String.format(countFormat, count));
} else if (activeLines.contains(lineNo)) {
out.write(zero);
} else {

View File

@ -521,23 +521,6 @@ end
local native_co_create, native_loadfile = coroutine.create, loadfile
local line_counts = {}
if cct_test then
local string_sub, debug_getinfo = string.sub, debug.getinfo
local function debug_hook(_, line_nr)
local name = debug_getinfo(2, "S").source
if string_sub(name, 1, 1) ~= "@" then return end
name = string_sub(name, 2)
local file = line_counts[name]
if not file then file = {} line_counts[name] = file end
file[line_nr] = (file[line_nr] or 0) + 1
end
coroutine.create = function(...)
local co = native_co_create(...)
debug.sethook(co, debug_hook, "l")
return co
end
local expect = require "cc.expect".expect
_G.native_loadfile = native_loadfile
_G.loadfile = function(filename, mode, env)
@ -557,8 +540,6 @@ if cct_test then
file.close()
return func, err
end
debug.sethook(debug_hook, "l")
end
local arg = ...