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.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;
}
}
} }

View File

@ -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 {

View File

@ -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 = ...