diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 59b69dde282..61ea7d1fe39 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using OpenDreamRuntime; using OpenDreamRuntime.Objects; +using OpenDreamRuntime.Procs.DebugAdapter; using OpenDreamRuntime.Rendering; using OpenDreamShared.Rendering; using Robust.Shared.Asynchronous; @@ -29,6 +30,7 @@ public sealed class DMTests : ContentUnitTest { [Dependency] private readonly DreamManager _dreamMan = default!; [Dependency] private readonly DreamObjectTree _objectTree = default!; [Dependency] private readonly ITaskManager _taskManager = default!; + [Dependency] private readonly IDreamDebugManager _debugManager = default!; [Flags] public enum DMTestFlags { @@ -71,7 +73,7 @@ private static void Cleanup(string? compiledFile) { } [Test, TestCaseSource(nameof(GetTests))] - public void TestFiles(string sourceFile, DMTestFlags testFlags) { + public void TestFiles(string sourceFile, string coverageFile, DMTestFlags testFlags) { string initialDirectory = Directory.GetCurrentDirectory(); TestContext.WriteLine($"--- TEST {sourceFile} | Flags: {testFlags}"); try { @@ -85,10 +87,15 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { Assert.IsTrue(compiledFile is not null && File.Exists(compiledFile), "Failed to compile DM source file"); Assert.IsTrue(_dreamMan.LoadJson(compiledFile), $"Failed to load {compiledFile}"); + + _debugManager.InitializeCoverage(coverageFile); + _dreamMan.StartWorld(); (bool successfulRun, DreamValue? returned, Exception? exception) = RunTest(); + _debugManager.Shutdown(); + if (testFlags.HasFlag(DMTestFlags.NoReturn)) { Assert.IsFalse(returned.HasValue, "proc returned unexpectedly"); } else { @@ -137,8 +144,11 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { var watch = new Stopwatch(); watch.Start(); + // hack to hopefully force spawned calls to finish + var iterationsRemaining = 100; + // Tick until our inner call has finished - while (!callTask.IsCompleted) { + while (!callTask.IsCompleted || iterationsRemaining-- > 0) { _dreamMan.Update(); _taskManager.ProcessPendingTasks(); @@ -163,6 +173,7 @@ private static IEnumerable GetTests() yield return new object[] { sourceFile2, + Path.GetFullPath($"{sourceFile[..^2]}coverage.xml"), testFlags }; } diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 3adbe5fdcdc..2cafcfa36bf 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -60,13 +60,18 @@ public override void Init() { public override void PostInit() { _commandSystem = _entitySystemManager.GetEntitySystem(); + var codeCoverageOutputFile = _configManager.GetCVar(OpenDreamCVars.CodeCoverage); + if (!String.IsNullOrWhiteSpace(codeCoverageOutputFile)) { + _debugManager.InitializeCoverage(codeCoverageOutputFile); + } + int debugAdapterPort = _configManager.GetCVar(OpenDreamCVars.DebugAdapterLaunched); if (debugAdapterPort == 0) { _dreamManager.PreInitialize(_configManager.GetCVar(OpenDreamCVars.JsonPath)); _dreamManager.StartWorld(); } else { // The debug manager is responsible for running _dreamManager.PreInitialize() and .StartWorld() - _debugManager.Initialize(debugAdapterPort); + _debugManager.InitializeDebugging(debugAdapterPort); } } diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index fc3b4d7c3aa..43fd8e6328c 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -73,12 +73,6 @@ public void LoadJson(DreamCompiledJson json) { Strings = json.Strings ?? new(); - if (json.GlobalInitProc is { } initProcDef) { - GlobalInitProc = new DMProc(0, DreamPath.Root, initProcDef, "", _dreamManager, _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler); - } else { - GlobalInitProc = null; - } - var types = json.Types ?? Array.Empty(); var procs = json.Procs; var globalProcs = json.GlobalProcs; @@ -86,6 +80,13 @@ public void LoadJson(DreamCompiledJson json) { // Load procs first so types can set their init proc's super proc LoadProcsFromJson(types, procs, globalProcs); LoadTypesFromJson(types); + + if (json.GlobalInitProc is { } initProcDef) { + GlobalInitProc = new DMProc(Procs.Count, DreamPath.Root, initProcDef, "", _dreamManager, _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler); + Procs.Add(GlobalInitProc); + } else { + GlobalInitProc = null; + } } public TreeEntry GetTreeEntry(DreamPath path) { diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Coverage/Cobertura.cs b/OpenDreamRuntime/Procs/DebugAdapter/Coverage/Cobertura.cs new file mode 100644 index 00000000000..a18ebf02220 --- /dev/null +++ b/OpenDreamRuntime/Procs/DebugAdapter/Coverage/Cobertura.cs @@ -0,0 +1,704 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// +// This source code was auto-generated by xsd, Version=4.8.3928.0. +// +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. +namespace OpenDreamRuntime.Procs.DebugAdapter.Coverage { + using System.Xml.Serialization; + + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class coverage { + + private string[] sourcesField; + + private package[] packagesField; + + private string linerateField; + + private string branchrateField; + + private string linescoveredField; + + private string linesvalidField; + + private string branchescoveredField; + + private string branchesvalidField; + + private string complexityField; + + private string versionField; + + private string timestampField; + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("source", IsNullable=false)] + public string[] sources { + get { + return this.sourcesField; + } + set { + this.sourcesField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("package", IsNullable=false)] + public package[] packages { + get { + return this.packagesField; + } + set { + this.packagesField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("line-rate")] + public string linerate { + get { + return this.linerateField; + } + set { + this.linerateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("branch-rate")] + public string branchrate { + get { + return this.branchrateField; + } + set { + this.branchrateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("lines-covered")] + public string linescovered { + get { + return this.linescoveredField; + } + set { + this.linescoveredField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("lines-valid")] + public string linesvalid { + get { + return this.linesvalidField; + } + set { + this.linesvalidField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("branches-covered")] + public string branchescovered { + get { + return this.branchescoveredField; + } + set { + this.branchescoveredField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("branches-valid")] + public string branchesvalid { + get { + return this.branchesvalidField; + } + set { + this.branchesvalidField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string complexity { + get { + return this.complexityField; + } + set { + this.complexityField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string version { + get { + return this.versionField; + } + set { + this.versionField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string timestamp { + get { + return this.timestampField; + } + set { + this.timestampField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class package { + + private @class[] classesField; + + private string nameField; + + private string linerateField; + + private string branchrateField; + + private string complexityField; + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("class", IsNullable=false)] + public @class[] classes { + get { + return this.classesField; + } + set { + this.classesField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string name { + get { + return this.nameField; + } + set { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("line-rate")] + public string linerate { + get { + return this.linerateField; + } + set { + this.linerateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("branch-rate")] + public string branchrate { + get { + return this.branchrateField; + } + set { + this.branchrateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string complexity { + get { + return this.complexityField; + } + set { + this.complexityField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class @class { + + private method[] methodsField; + + private line[] linesField; + + private string nameField; + + private string filenameField; + + private string linerateField; + + private string branchrateField; + + private string complexityField; + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("method", IsNullable=false)] + public method[] methods { + get { + return this.methodsField; + } + set { + this.methodsField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("line", IsNullable=false)] + public line[] lines { + get { + return this.linesField; + } + set { + this.linesField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string name { + get { + return this.nameField; + } + set { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string filename { + get { + return this.filenameField; + } + set { + this.filenameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("line-rate")] + public string linerate { + get { + return this.linerateField; + } + set { + this.linerateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("branch-rate")] + public string branchrate { + get { + return this.branchrateField; + } + set { + this.branchrateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string complexity { + get { + return this.complexityField; + } + set { + this.complexityField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class method { + + private line[] linesField; + + private string nameField; + + private string signatureField; + + private string linerateField; + + private string branchrateField; + + private string complexityField; + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("line", IsNullable=false)] + public line[] lines { + get { + return this.linesField; + } + set { + this.linesField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string name { + get { + return this.nameField; + } + set { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string signature { + get { + return this.signatureField; + } + set { + this.signatureField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("line-rate")] + public string linerate { + get { + return this.linerateField; + } + set { + this.linerateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("branch-rate")] + public string branchrate { + get { + return this.branchrateField; + } + set { + this.branchrateField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string complexity { + get { + return this.complexityField; + } + set { + this.complexityField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class line { + + private condition[][] conditionsField; + + private string numberField; + + private string hitsField; + + private string branchField; + + private string conditioncoverageField; + + public line() { + this.branchField = "false"; + this.conditioncoverageField = "100%"; + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("condition", typeof(condition[]), IsNullable=false)] + public condition[][] conditions { + get { + return this.conditionsField; + } + set { + this.conditionsField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string number { + get { + return this.numberField; + } + set { + this.numberField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string hits { + get { + return this.hitsField; + } + set { + this.hitsField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute("false")] + public string branch { + get { + return this.branchField; + } + set { + this.branchField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute("condition-coverage")] + [System.ComponentModel.DefaultValueAttribute("100%")] + public string conditioncoverage { + get { + return this.conditioncoverageField; + } + set { + this.conditioncoverageField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class condition { + + private string numberField; + + private string typeField; + + private string coverageField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string number { + get { + return this.numberField; + } + set { + this.numberField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string type { + get { + return this.typeField; + } + set { + this.typeField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string coverage { + get { + return this.coverageField; + } + set { + this.coverageField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class sources { + + private string[] sourceField; + + /// + [System.Xml.Serialization.XmlElementAttribute("source")] + public string[] source { + get { + return this.sourceField; + } + set { + this.sourceField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class packages { + + private package[] packageField; + + /// + [System.Xml.Serialization.XmlElementAttribute("package")] + public package[] package { + get { + return this.packageField; + } + set { + this.packageField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class classes { + + private @class[] classField; + + /// + [System.Xml.Serialization.XmlElementAttribute("class")] + public @class[] @class { + get { + return this.classField; + } + set { + this.classField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class methods { + + private method[] methodField; + + /// + [System.Xml.Serialization.XmlElementAttribute("method")] + public method[] method { + get { + return this.methodField; + } + set { + this.methodField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class lines { + + private line[] lineField; + + /// + [System.Xml.Serialization.XmlElementAttribute("line")] + public line[] line { + get { + return this.lineField; + } + set { + this.lineField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://tempuri.org/coverage-04")] + [System.Xml.Serialization.XmlRootAttribute(Namespace="http://tempuri.org/coverage-04", IsNullable=false)] + public partial class conditions { + + private condition[] conditionField; + + /// + [System.Xml.Serialization.XmlElementAttribute("condition")] + public condition[] condition { + get { + return this.conditionField; + } + set { + this.conditionField = value; + } + } + } +} diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Coverage/README.md b/OpenDreamRuntime/Procs/DebugAdapter/Coverage/README.md new file mode 100644 index 00000000000..1b2c8b9ef6b --- /dev/null +++ b/OpenDreamRuntime/Procs/DebugAdapter/Coverage/README.md @@ -0,0 +1,16 @@ +These classes were auto generated from schema + +Source dtd: http://cobertura.sourceforge.net/xml/coverage-04.dtd + +Converted to XSD using Visual Studio, then coverted to C# using `xsd.exe` in the Windows SDK. + +Command Line: +```sh +xsd.exe /c /namespace:OpenDreamRuntime.Procs.DebugAdapter.Coverage ./Cobertura.xsd +``` + +Manual edits: +- Added suppression for CS8981 on line 14 +- Changed `typeof(condition)` to `typeof(condition[])` on line 461. + +https://gcovr.com/en/stable/output/cobertura.html diff --git a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs index 9ddd53599b2..f71fa8c59bf 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs @@ -1,5 +1,8 @@ -using System.IO; +using System.IO; using System.Linq; +using System.Xml; +using System.Xml.Serialization; + using DMCompiler.Bytecode; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; @@ -39,6 +42,13 @@ public struct ThreadStepMode { public string? Granularity; } + // Coverage + /// + /// Map of Proc IDs to offsets + hit counts + /// + private ulong[][]? _coverageTracking; + private string? _coverageOutputFile; + // Breakpoint storage private const string ExceptionFilterRuntimes = "runtimes"; private bool _breakOnRuntimes = true; @@ -105,7 +115,7 @@ private int AllocVariableRef(Func> func) } // Lifecycle - public void Initialize(int port) { + public void InitializeDebugging(int port) { _sawmill = Logger.GetSawmill("opendream.debugger"); _adapter = new DebugAdapter(); @@ -124,6 +134,7 @@ public void Update() { public void Shutdown() { _breakpointIdCounter = 0; _adapter?.Shutdown(); + WriteOutCoverage(); } // Callbacks from the runtime @@ -168,7 +179,24 @@ public void HandleFirstResume(DMProcState state) { } } + public void InitializeCoverage(string outputFile) { + var allProcs = _objectTree.Procs; + var coverageTracking = new ulong[allProcs.Count][]; + for(var i = 0; i < allProcs.Count; ++i) { + if (allProcs[i] is DMProc dmProc) { + coverageTracking[i] = new ulong[dmProc.Bytecode.Length]; + } + } + + _coverageTracking = coverageTracking; + _coverageOutputFile = outputFile; + } + public void HandleInstruction(DMProcState state) { + if (_coverageTracking != null) { + ++_coverageTracking[state.Proc.Id][state.ProgramCounter]; + } + if (state.Thread.StepMode == null) return; @@ -851,10 +879,76 @@ private string EncodeInstructionPointer(DMProc proc, int pc) { _disassemblyProcs.TryGetValue((int)((ip2 & 0xffffffff00000000) >> 32), out var proc); return (proc, (uint)(ip2 & 0xffffffff)); } + + private void WriteOutCoverage() { + if (_coverageTracking == null) + return; + + // break down raw data into human parsable + var sourceHits = new Dictionary>(); + for(var i = 0; i < _coverageTracking.Length; ++i) { + var opcodeCoverage = _coverageTracking[i]; + if (opcodeCoverage == null + || _objectTree.Procs[i] is not DMProc proc) { + continue; + } + + // TODO: eventually support branch conditions 💀 + for (var code = 0; code < opcodeCoverage.Length; ++code) { + if (!proc.IsOnLineChange(code)) { + // we only care about hits on the line's first opcode (until we support branching, if ever) + continue; + } + + var (source, line) = proc.GetSourceAtOffset(code); + if (!sourceHits.TryGetValue(source, out var lineMap)) { + lineMap = new Dictionary(); + sourceHits.Add(source, lineMap); + } + + var lineHits = opcodeCoverage[code]; + if(lineMap.TryGetValue(line, out var totalLineHits)) { + lineMap[line] = totalLineHits + lineHits; + } else { + lineMap.Add(line, lineHits); + } + } + } + + var schema = new Coverage.coverage() { + packages = new Coverage.package[] { + new Coverage.package { + name = String.Empty, + classes = sourceHits + .Select(sourceKvp => new Coverage.@class { + filename = sourceKvp.Key, + lines = sourceKvp + .Value + .Select(linesKvp => new Coverage.line { + number = linesKvp.Key.ToString(), + hits = linesKvp.Value.ToString(), + }) + .ToArray(), + }) + .ToArray(), + }, + }, + sources = new string[] { String.Empty }, + }; + + var serializer = new XmlSerializer(schema.GetType()); + using (var fileStream = new FileStream(_coverageOutputFile!, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete)) { + serializer.Serialize(fileStream, schema); + } + + _coverageTracking = null; + _coverageOutputFile = null; + } } public interface IDreamDebugManager { - public void Initialize(int port); + public void InitializeCoverage(string outputFile); + public void InitializeDebugging(int port); public void Update(); public void Shutdown(); diff --git a/OpenDreamShared/OpenDreamCVars.cs b/OpenDreamShared/OpenDreamCVars.cs index 3af2886caff..8776a1031fb 100644 --- a/OpenDreamShared/OpenDreamCVars.cs +++ b/OpenDreamShared/OpenDreamCVars.cs @@ -24,5 +24,8 @@ public abstract class OpenDreamCVars { public static readonly CVarDef TopicPort = CVarDef.Create("opendream.topic_port", 25567, CVar.SERVERONLY); + + public static readonly CVarDef CodeCoverage = + CVarDef.Create("opendream.code_coverage_file", string.Empty, CVar.SERVERONLY); } }