diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/ModelExtensions.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/ModelExtensions.cs
new file mode 100644
index 00000000..7b3bd638
--- /dev/null
+++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/ModelExtensions.cs
@@ -0,0 +1,72 @@
+namespace OverwatchTranscript
+{
+ [Serializable]
+ public partial class OverwatchHeader
+ {
+ public OverwatchCodexHeader? CodexHeader { get; set; }
+ }
+
+ [Serializable]
+ public partial class OverwatchEvent
+ {
+ public OverwatchCodexEvent? CodexEvent { get; set; }
+ }
+
+ [Serializable]
+ public class OverwatchCodexHeader
+ {
+ public int TotalNumberOfNodes { get; set; }
+ }
+
+ [Serializable]
+ public class OverwatchCodexEvent
+ {
+ public NodeStartedEvent? NodeStarted { get; set; }
+ public NodeStoppedEvent? NodeStopped { get; set; }
+ public FileUploadedEvent? FileUploaded { get; set; }
+ public FileDownloadedEvent? FileDownloaded { get; set; }
+ public BlockReceivedEvent? BlockReceived { get; set; }
+ }
+
+ #region Scenario Generated Events
+
+ [Serializable]
+ public class NodeStartedEvent
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Image { get; set; } = string.Empty;
+ public string Args { get; set; } = string.Empty;
+ }
+
+ [Serializable]
+ public class NodeStoppedEvent
+ {
+ public string Name { get; set; } = string.Empty;
+ }
+
+ [Serializable]
+ public class FileUploadedEvent
+ {
+ public ulong ByteSize { get; set; }
+ public string Cid { get; set; } = string.Empty;
+ }
+
+ [Serializable]
+ public class FileDownloadedEvent
+ {
+ public string Cid { get; set; } = string.Empty;
+ }
+
+ #endregion
+
+ #region Codex Generated Events
+
+ [Serializable]
+ public class BlockReceivedEvent
+ {
+ public string BlockAddress { get; set; } = string.Empty;
+ public string PeerId { get; set; } = string.Empty;
+ }
+
+ #endregion
+}
diff --git a/Tests/FrameworkTests/FrameworkTests.csproj b/Tests/FrameworkTests/FrameworkTests.csproj
index 4bc09aa5..cceced4b 100644
--- a/Tests/FrameworkTests/FrameworkTests.csproj
+++ b/Tests/FrameworkTests/FrameworkTests.csproj
@@ -17,6 +17,7 @@
+
diff --git a/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs b/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs
new file mode 100644
index 00000000..b88d3325
--- /dev/null
+++ b/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs
@@ -0,0 +1,103 @@
+using Newtonsoft.Json;
+using NUnit.Framework;
+using OverwatchTranscript;
+
+namespace FrameworkTests.OverwatchTranscript
+{
+ [TestFixture]
+ public class TranscriptTests
+ {
+ private const string TranscriptFilename = "testtranscript.json";
+ private const string HeaderKey = "testHeader";
+ private const string HeaderData = "abcdef";
+ private const string EventData0 = "12345";
+ private const string EventData1 = "678";
+ private const string EventData2 = "90";
+ private readonly DateTime t0 = DateTime.UtcNow;
+ private readonly DateTime t1 = DateTime.UtcNow.AddMinutes(1);
+ private readonly DateTime t2 = DateTime.UtcNow.AddMinutes(2);
+
+ [Test]
+ public void WriteAndRun()
+ {
+ var workdir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+
+ WriteTranscript(workdir);
+ ReadTranscript(workdir);
+
+ File.Delete(TranscriptFilename);
+ }
+
+ private void WriteTranscript(string workdir)
+ {
+ var writer = new TranscriptWriter(workdir);
+
+ writer.AddHeader(HeaderKey, new TestHeader
+ {
+ HeaderData = HeaderData
+ });
+
+ writer.Add(t0, new MyEvent
+ {
+ EventData = EventData0
+ });
+ writer.Add(t2, new MyEvent
+ {
+ EventData = EventData2
+ });
+ writer.Add(t1, new MyEvent
+ {
+ EventData = EventData1
+ });
+
+ writer.Write(TranscriptFilename);
+ }
+
+ private void ReadTranscript(string workdir)
+ {
+ var reader = new TranscriptReader(workdir, TranscriptFilename);
+
+ var header = reader.GetHeader(HeaderKey);
+ Assert.That(header.HeaderData, Is.EqualTo(HeaderData));
+
+ var events = new List();
+ reader.AddHandler((utc, e) =>
+ {
+ e.CheckUtc = utc;
+ events.Add(e);
+ });
+
+ Assert.That(events.Count, Is.EqualTo(0));
+ reader.Next();
+ Assert.That(events.Count, Is.EqualTo(1));
+ reader.Next();
+ Assert.That(events.Count, Is.EqualTo(2));
+ reader.Next();
+ Assert.That(events.Count, Is.EqualTo(3));
+ reader.Next();
+ Assert.That(events.Count, Is.EqualTo(3));
+
+ Assert.That(events[0].CheckUtc, Is.EqualTo(t0));
+ Assert.That(events[0].EventData, Is.EqualTo(EventData0));
+ Assert.That(events[1].CheckUtc, Is.EqualTo(t1));
+ Assert.That(events[1].EventData, Is.EqualTo(EventData1));
+ Assert.That(events[2].CheckUtc, Is.EqualTo(t2));
+ Assert.That(events[2].EventData, Is.EqualTo(EventData2));
+
+ reader.Close();
+ }
+ }
+
+ public class TestHeader
+ {
+ public string HeaderData { get; set; } = string.Empty;
+ }
+
+ public class MyEvent
+ {
+ public string EventData { get; set; } = string.Empty;
+
+ [JsonIgnore]
+ public DateTime CheckUtc { get; set; }
+ }
+}
diff --git a/Tools/OverwatchTranscript/Model.cs b/Tools/OverwatchTranscript/Model.cs
index 0069c4b8..7130cc22 100644
--- a/Tools/OverwatchTranscript/Model.cs
+++ b/Tools/OverwatchTranscript/Model.cs
@@ -1,76 +1,30 @@
namespace OverwatchTranscript
{
[Serializable]
- public class Transcript
+ public class OverwatchTranscript
{
- public Header Header { get; set; } = new();
- public Event[] Events { get; set; } = Array.Empty();
+ public OverwatchHeader Header { get; set; } = new();
+ public OverwatchEvent[] Events { get; set; } = Array.Empty();
}
[Serializable]
- public class Header
+ public class OverwatchHeader
{
- public int TotalNumberOfNodes { get; set; }
+ public OverwatchHeaderEntry[] Entries { get; set; } = Array.Empty();
}
[Serializable]
- public class Event
+ public class OverwatchHeaderEntry
+ {
+ public string Key { get; set; } = string.Empty;
+ public string Value { get; set; } = string.Empty;
+ }
+
+ [Serializable]
+ public class OverwatchEvent
{
public DateTime Utc { get; set; }
- public ScenarioFinishedEvent? ScenarioFinished { get; set; }
- public NodeStartedEvent? NodeStarted { get; set; }
- public NodeStoppedEvent? NodeStopped { get; set; }
- public FileUploadedEvent? FileUploaded { get; set; }
- public FileDownloadedEvent? FileDownloaded { get; set; }
- public BlockReceivedEvent? BlockReceived { get; set; }
+ public string Type { get; set; } = string.Empty;
+ public string Payload { get; set; } = string.Empty;
}
-
- #region Scenario Generated Events
-
- [Serializable]
- public class ScenarioFinishedEvent
- {
- public bool Success { get; set; }
- public string Result { get; set; } = string.Empty;
- }
-
- [Serializable]
- public class NodeStartedEvent
- {
- public string Name { get; set; } = string.Empty;
- public string Image { get; set; } = string.Empty;
- public string Args { get; set; } = string.Empty;
- }
-
- [Serializable]
- public class NodeStoppedEvent
- {
- public string Name { get; set; } = string.Empty;
- }
-
- [Serializable]
- public class FileUploadedEvent
- {
- public ulong ByteSize { get; set; }
- public string Cid { get; set; } = string.Empty;
- }
-
- [Serializable]
- public class FileDownloadedEvent
- {
- public string Cid { get; set; } = string.Empty;
- }
-
- #endregion
-
- #region Codex Generated Events
-
- [Serializable]
- public class BlockReceivedEvent
- {
- public string BlockAddress { get; set; } = string.Empty;
- public string PeerId { get; set; } = string.Empty;
- }
-
- #endregion
-}
\ No newline at end of file
+}
diff --git a/Tools/OverwatchTranscript/OverwatchTranscript.csproj b/Tools/OverwatchTranscript/OverwatchTranscript.csproj
index cfadb03d..51eb283c 100644
--- a/Tools/OverwatchTranscript/OverwatchTranscript.csproj
+++ b/Tools/OverwatchTranscript/OverwatchTranscript.csproj
@@ -6,4 +6,8 @@
enable
+
+
+
+
diff --git a/Tools/OverwatchTranscript/TranscriptConstants.cs b/Tools/OverwatchTranscript/TranscriptConstants.cs
new file mode 100644
index 00000000..245fe492
--- /dev/null
+++ b/Tools/OverwatchTranscript/TranscriptConstants.cs
@@ -0,0 +1,8 @@
+namespace OverwatchTranscript
+{
+ public static class TranscriptConstants
+ {
+ public const string TranscriptFilename = "transcript.json";
+ public const string ArtifactFolderName = "artifacts";
+ }
+}
diff --git a/Tools/OverwatchTranscript/TranscriptReader.cs b/Tools/OverwatchTranscript/TranscriptReader.cs
new file mode 100644
index 00000000..2fbb1a78
--- /dev/null
+++ b/Tools/OverwatchTranscript/TranscriptReader.cs
@@ -0,0 +1,92 @@
+using Newtonsoft.Json;
+using System.IO.Compression;
+
+namespace OverwatchTranscript
+{
+ public class TranscriptReader
+ {
+ private readonly string transcriptFile;
+ private readonly string artifactsFolder;
+ private readonly Dictionary> handlers = new Dictionary>();
+ private readonly string workingDir;
+ private OverwatchTranscript model = null!;
+ private int eventIndex = 0;
+ private bool closed;
+
+ public TranscriptReader(string workingDir, string inputFilename)
+ {
+ closed = false;
+ this.workingDir = workingDir;
+ transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
+ artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
+
+ if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
+ if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
+
+ LoadModel(inputFilename);
+ }
+
+ public T GetHeader(string key)
+ {
+ CheckClosed();
+ var value = model.Header.Entries.First(e => e.Key == key).Value;
+ return JsonConvert.DeserializeObject(value)!;
+ }
+
+ public void AddHandler(Action handler)
+ {
+ CheckClosed();
+ var typeName = typeof(T).FullName;
+ if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
+
+ handlers.Add(typeName, (utc, s) =>
+ {
+ handler(utc, JsonConvert.DeserializeObject(s)!);
+ });
+ }
+
+ public void Next()
+ {
+ CheckClosed();
+ if (eventIndex >= model.Events.Length) return;
+
+ var @event = model.Events[eventIndex];
+ eventIndex++;
+
+ PlayEvent(@event);
+ }
+
+ public void Close()
+ {
+ CheckClosed();
+ Directory.Delete(workingDir, true);
+ closed = true;
+ }
+
+ private void PlayEvent(OverwatchEvent @event)
+ {
+ if (!handlers.ContainsKey(@event.Type)) return;
+ var handler = handlers[@event.Type];
+
+ handler(@event.Utc, @event.Payload);
+ }
+
+ private void LoadModel(string inputFilename)
+ {
+ ZipFile.ExtractToDirectory(inputFilename, workingDir);
+
+ if (!File.Exists(transcriptFile))
+ {
+ closed = true;
+ throw new Exception("Is not a transcript file. Unzipped to: " + workingDir);
+ }
+
+ model = JsonConvert.DeserializeObject(File.ReadAllText(transcriptFile))!;
+ }
+
+ private void CheckClosed()
+ {
+ if (closed) throw new Exception("Transcript has already been written. Cannot modify or write again.");
+ }
+ }
+}
diff --git a/Tools/OverwatchTranscript/TranscriptWriter.cs b/Tools/OverwatchTranscript/TranscriptWriter.cs
new file mode 100644
index 00000000..21cb3725
--- /dev/null
+++ b/Tools/OverwatchTranscript/TranscriptWriter.cs
@@ -0,0 +1,98 @@
+using Newtonsoft.Json;
+using System.IO.Compression;
+
+namespace OverwatchTranscript
+{
+ public interface ITranscriptWriter
+ {
+ void AddHeader(string key, object value);
+ void Add(DateTime utc, object payload);
+ void IncludeArtifact(string filePath);
+ void Write(string outputFilename);
+ }
+
+ public class TranscriptWriter : ITranscriptWriter
+ {
+ private readonly string transcriptFile;
+ private readonly string artifactsFolder;
+ private readonly Dictionary header = new Dictionary();
+ private readonly SortedList buffer = new SortedList();
+ private readonly string workingDir;
+ private bool closed;
+
+ public TranscriptWriter(string workingDir)
+ {
+ closed = false;
+ this.workingDir = workingDir;
+ transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
+ artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
+
+ if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
+ if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
+ }
+
+ public void Add(DateTime utc, object payload)
+ {
+ CheckClosed();
+ var typeName = payload.GetType().FullName;
+ if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
+
+ buffer.Add(utc, new OverwatchEvent
+ {
+ Utc = utc,
+ Type = typeName,
+ Payload = JsonConvert.SerializeObject(payload)
+ });
+ }
+
+ public void AddHeader(string key, object value)
+ {
+ CheckClosed();
+ header.Add(key, JsonConvert.SerializeObject(value));
+ }
+
+ public void IncludeArtifact(string filePath)
+ {
+ CheckClosed();
+ if (!File.Exists(filePath)) throw new Exception("File not found: " + filePath);
+ var name = Path.GetFileName(filePath);
+ File.Copy(filePath, Path.Combine(artifactsFolder, name), overwrite: false);
+ }
+
+ public void Write(string outputFilename)
+ {
+ CheckClosed();
+ closed = true;
+
+ var model = new OverwatchTranscript
+ {
+ Header = new OverwatchHeader
+ {
+ Entries = header.Select(h =>
+ {
+ return new OverwatchHeaderEntry
+ {
+ Key = h.Key,
+ Value = h.Value
+ };
+ }).ToArray()
+ },
+ Events = buffer.Values.ToArray()
+ };
+
+ header.Clear();
+ buffer.Clear();
+
+ File.WriteAllText(transcriptFile, JsonConvert.SerializeObject(model, Formatting.Indented));
+
+ ZipFile.CreateFromDirectory(workingDir, outputFilename);
+
+ Directory.Delete(workingDir, true);
+ }
+
+ private void CheckClosed()
+ {
+ if (closed) throw new Exception("Transcript has already been written. Cannot modify or write again.");
+ }
+ }
+}