mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 21:52: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
	 Jonathan Coates
					Jonathan Coates