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:
parent
2457a31728
commit
28a55349a9
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 = ...
|
||||
|
Loading…
Reference in New Issue
Block a user