From 802b18e99045c52af43ef5d6f7c1d1b072a2422e Mon Sep 17 00:00:00 2001 From: benbierens Date: Thu, 25 Jul 2024 15:12:25 +0200 Subject: [PATCH] working transcript writer and reader --- .../OverwatchSupport/ModelExtensions.cs | 72 ++++++++++++ Tests/FrameworkTests/FrameworkTests.csproj | 1 + .../OverwatchTranscript/TranscriptTests.cs | 103 ++++++++++++++++++ Tools/OverwatchTranscript/Model.cs | 78 +++---------- .../OverwatchTranscript.csproj | 4 + .../TranscriptConstants.cs | 8 ++ Tools/OverwatchTranscript/TranscriptReader.cs | 92 ++++++++++++++++ Tools/OverwatchTranscript/TranscriptWriter.cs | 98 +++++++++++++++++ 8 files changed, 394 insertions(+), 62 deletions(-) create mode 100644 ProjectPlugins/CodexPlugin/OverwatchSupport/ModelExtensions.cs create mode 100644 Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs create mode 100644 Tools/OverwatchTranscript/TranscriptConstants.cs create mode 100644 Tools/OverwatchTranscript/TranscriptReader.cs create mode 100644 Tools/OverwatchTranscript/TranscriptWriter.cs diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/ModelExtensions.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/ModelExtensions.cs new file mode 100644 index 0000000..7b3bd63 --- /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 4bc09aa..cceced4 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 0000000..b88d332 --- /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 0069c4b..7130cc2 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 cfadb03..51eb283 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 0000000..245fe49 --- /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 0000000..2fbb1a7 --- /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 0000000..21cb372 --- /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."); + } + } +}