mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-03 23:22:59 +00:00 
			
		
		
		
	Move coverage to the Java side
While slightly irritating (requires Cobalt magic), it's much, much faster.
This commit is contained in:
		@@ -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 = ...
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user