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.api.peripheral.IPeripheral;
|
||||||
import dan200.computercraft.core.computer.Computer;
|
import dan200.computercraft.core.computer.Computer;
|
||||||
import dan200.computercraft.core.computer.ComputerSide;
|
import dan200.computercraft.core.computer.ComputerSide;
|
||||||
|
import dan200.computercraft.core.computer.ComputerThread;
|
||||||
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
|
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
|
||||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||||
import dan200.computercraft.core.filesystem.WritableFileMount;
|
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.core.terminal.Terminal;
|
||||||
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
import org.junit.jupiter.api.function.Executable;
|
import org.junit.jupiter.api.function.Executable;
|
||||||
import org.opentest4j.AssertionFailedError;
|
import org.opentest4j.AssertionFailedError;
|
||||||
|
import org.opentest4j.TestAbortedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 javax.annotation.Nullable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.Condition;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,7 +91,7 @@ public class ComputerTestDelegate {
|
|||||||
|
|
||||||
private final Condition hasFinished = lock.newCondition();
|
private final Condition hasFinished = lock.newCondition();
|
||||||
private boolean finished = false;
|
private boolean finished = false;
|
||||||
private Map<String, Map<Double, Double>> finishedWith;
|
private final Map<LuaString, Int2IntArrayMap> coverage = new HashMap<>();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() throws IOException {
|
public void before() throws IOException {
|
||||||
@ -99,7 +112,7 @@ public class ComputerTestDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var environment = new BasicEnvironment(mount);
|
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 = new Computer(context, environment, term, 0);
|
||||||
computer.getEnvironment().setPeripheral(ComputerSide.TOP, new FakeModem());
|
computer.getEnvironment().setPeripheral(ComputerSide.TOP, new FakeModem());
|
||||||
computer.getEnvironment().setPeripheral(ComputerSide.BOTTOM, new FakePeripheralHub());
|
computer.getEnvironment().setPeripheral(ComputerSide.BOTTOM, new FakePeripheralHub());
|
||||||
@ -137,10 +150,12 @@ public class ComputerTestDelegate {
|
|||||||
computer.shutdown();
|
computer.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finishedWith != null) {
|
if (!coverage.isEmpty()) {
|
||||||
Files.createDirectories(REPORT_PATH.getParent());
|
Files.createDirectories(REPORT_PATH.getParent());
|
||||||
try (var writer = Files.newBufferedWriter(REPORT_PATH)) {
|
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) {
|
switch (status) {
|
||||||
case "ok":
|
case "ok":
|
||||||
|
break;
|
||||||
case "pending":
|
case "pending":
|
||||||
|
runResult = new TestAbortedException("Test is pending");
|
||||||
break;
|
break;
|
||||||
case "fail":
|
case "fail":
|
||||||
runResult = new AssertionFailedError(wholeMessage.toString());
|
runResult = new AssertionFailedError(wholeMessage.toString());
|
||||||
@ -432,9 +449,7 @@ public class ComputerTestDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@LuaFunction
|
@LuaFunction
|
||||||
public final void finish(Optional<Map<?, ?>> result) {
|
public final void finish() {
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
var finishedResult = (Map<String, Map<Double, Double>>) result.orElse(null);
|
|
||||||
LOG.info("Finished");
|
LOG.info("Finished");
|
||||||
|
|
||||||
// Signal to after that execution has finished
|
// Signal to after that execution has finished
|
||||||
@ -445,7 +460,6 @@ public class ComputerTestDelegate {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
finished = true;
|
finished = true;
|
||||||
if (finishedResult != null) finishedWith = finishedResult;
|
|
||||||
|
|
||||||
hasFinished.signal();
|
hasFinished.signal();
|
||||||
} finally {
|
} 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;
|
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.IntOpenHashSet;
|
||||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||||
import org.slf4j.Logger;
|
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 MULTISHELL = ROOT.resolve("rom/programs/advanced/multishell.lua");
|
||||||
private static final Path TREASURE = ROOT.resolve("treasure");
|
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 blank;
|
||||||
private final String zero;
|
private final String zero;
|
||||||
private final String countFormat;
|
private final String countFormat;
|
||||||
|
|
||||||
LuaCoverage(Map<String, Map<Double, Double>> coverage) {
|
LuaCoverage(Map<String, Int2IntMap> coverage) {
|
||||||
this.coverage = coverage;
|
this.coverage = coverage;
|
||||||
|
|
||||||
var max = (int) coverage.values().stream()
|
var max = coverage.values().stream()
|
||||||
.flatMapToDouble(x -> x.values().stream().mapToDouble(y -> y))
|
.flatMapToInt(x -> x.values().intStream())
|
||||||
.max().orElse(0);
|
.max().orElse(0);
|
||||||
var maxLen = Math.max(1, (int) Math.ceil(Math.log10(max)));
|
var maxLen = Math.max(1, (int) Math.ceil(Math.log10(max)));
|
||||||
blank = Strings.repeat(" ", maxLen + 1);
|
blank = " ".repeat(maxLen + 1);
|
||||||
zero = Strings.repeat("*", maxLen) + "0";
|
zero = "*".repeat(maxLen) + "0";
|
||||||
countFormat = "%" + (maxLen + 1) + "d";
|
countFormat = "%" + (maxLen + 1) + "d";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,8 +62,8 @@ class LuaCoverage {
|
|||||||
);
|
);
|
||||||
var files = possiblePaths
|
var files = possiblePaths
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.flatMap(x -> x.entrySet().stream())
|
.flatMap(x -> x.int2IntEntrySet().stream())
|
||||||
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue, Double::sum));
|
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
writeCoverageFor(out, path, files);
|
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)) {
|
if (!Files.exists(fullName)) {
|
||||||
LOG.error("Cannot locate file {}", fullName);
|
LOG.error("Cannot locate file {}", fullName);
|
||||||
return;
|
return;
|
||||||
@ -96,9 +96,9 @@ class LuaCoverage {
|
|||||||
var lineNo = 0;
|
var lineNo = 0;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
lineNo++;
|
lineNo++;
|
||||||
var count = visitedLines.get((double) lineNo);
|
var count = visitedLines.get(lineNo);
|
||||||
if (count != null) {
|
if (count != null) {
|
||||||
out.write(String.format(countFormat, count.intValue()));
|
out.write(String.format(countFormat, count));
|
||||||
} else if (activeLines.contains(lineNo)) {
|
} else if (activeLines.contains(lineNo)) {
|
||||||
out.write(zero);
|
out.write(zero);
|
||||||
} else {
|
} else {
|
||||||
|
@ -521,23 +521,6 @@ end
|
|||||||
local native_co_create, native_loadfile = coroutine.create, loadfile
|
local native_co_create, native_loadfile = coroutine.create, loadfile
|
||||||
local line_counts = {}
|
local line_counts = {}
|
||||||
if cct_test then
|
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
|
local expect = require "cc.expect".expect
|
||||||
_G.native_loadfile = native_loadfile
|
_G.native_loadfile = native_loadfile
|
||||||
_G.loadfile = function(filename, mode, env)
|
_G.loadfile = function(filename, mode, env)
|
||||||
@ -557,8 +540,6 @@ if cct_test then
|
|||||||
file.close()
|
file.close()
|
||||||
return func, err
|
return func, err
|
||||||
end
|
end
|
||||||
|
|
||||||
debug.sethook(debug_hook, "l")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local arg = ...
|
local arg = ...
|
||||||
|
Loading…
Reference in New Issue
Block a user