From fbf71e9fe81e997f8ba5ecd9e3565757b6421876 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 1 Oct 2024 13:38:08 +0200 Subject: [PATCH] wip --- Framework/KubernetesWorkflow/Configuration.cs | 3 + Framework/KubernetesWorkflow/CrashWatcher.cs | 7 +- Framework/KubernetesWorkflow/K8sController.cs | 12 +- Framework/KubernetesWorkflow/LogHandler.cs | 8 +- .../KubernetesWorkflow/StartupWorkflow.cs | 13 +- .../KubernetesWorkflow/WorkflowCreator.cs | 2 +- .../RewarderBotContainerRecipe.cs | 2 +- .../CodexPlugin/CodexContainerRecipe.cs | 3 +- .../BasicTests/EventLogginHandler.cs | 57 +++++++++ Tests/CodexTests/BasicTests/ExampleTests.cs | 43 +++++++ .../CodexTests/BasicTests/MarketplaceTests.cs | 114 ++++++++++++++++++ .../CodexTests/BasicTests/ThreeClientTest.cs | 20 +++ Tests/DistTestCore/Configuration.cs | 7 +- Tests/DistTestCore/DistTest.cs | 2 +- Tests/DistTestCore/TestLifecycle.cs | 8 +- .../OverwatchTranscript/TranscriptTests.cs | 1 + 16 files changed, 277 insertions(+), 25 deletions(-) create mode 100644 Tests/CodexTests/BasicTests/EventLogginHandler.cs diff --git a/Framework/KubernetesWorkflow/Configuration.cs b/Framework/KubernetesWorkflow/Configuration.cs index 62146820..2870b48f 100644 --- a/Framework/KubernetesWorkflow/Configuration.cs +++ b/Framework/KubernetesWorkflow/Configuration.cs @@ -21,5 +21,8 @@ namespace KubernetesWorkflow [JsonIgnore] public IK8sHooks Hooks { get; set; } = new DoNothingK8sHooks(); + + [JsonIgnore] + public Func Replacer { get; set; } = s => s; } } diff --git a/Framework/KubernetesWorkflow/CrashWatcher.cs b/Framework/KubernetesWorkflow/CrashWatcher.cs index 7f38cfb5..80a6943d 100644 --- a/Framework/KubernetesWorkflow/CrashWatcher.cs +++ b/Framework/KubernetesWorkflow/CrashWatcher.cs @@ -11,11 +11,13 @@ namespace KubernetesWorkflow private readonly string podName; private readonly string recipeName; private readonly string k8sNamespace; + private readonly Func replacer; private CancellationTokenSource cts; private Task? worker; private Exception? workerException; - public CrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace) + public CrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace, + Func replacer) { this.log = log; this.config = config; @@ -23,6 +25,7 @@ namespace KubernetesWorkflow this.podName = podName; this.recipeName = recipeName; this.k8sNamespace = k8sNamespace; + this.replacer = replacer; cts = new CancellationTokenSource(); } @@ -92,7 +95,7 @@ namespace KubernetesWorkflow { using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true); var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName); - handler.Log(stream); + handler.Log(stream, replacer); } } } diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index e5fac7c4..49430ba2 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -12,10 +12,11 @@ namespace KubernetesWorkflow private readonly ILog log; private readonly K8sCluster cluster; private readonly WorkflowNumberSource workflowNumberSource; + private readonly Func replacer; private readonly K8sClient client; public const string PodLabelKey = "pod-uuid"; - public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace) + public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace, Func replacer) { this.log = log; this.cluster = cluster; @@ -23,6 +24,7 @@ namespace KubernetesWorkflow client = new K8sClient(cluster.GetK8sClientConfig()); K8sNamespace = k8sNamespace; + this.replacer = replacer; } public void Dispose() @@ -64,7 +66,7 @@ namespace KubernetesWorkflow if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment); } - public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines, bool? previous) + public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines, bool? previous, Func replacer) { log.Debug(); @@ -72,7 +74,7 @@ namespace KubernetesWorkflow var recipeName = container.Recipe.Name; using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines, previous: previous)); - logHandler.Log(stream); + logHandler.Log(stream, replacer); } public string ExecuteCommand(RunningContainer container, string command, params string[] args) @@ -906,7 +908,7 @@ namespace KubernetesWorkflow var msg = $"Pod crash detected for deployment {deploymentName} (pod:{podName})"; log.Error(msg); - DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true); + DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true, replacer); throw new Exception(msg); } @@ -952,7 +954,7 @@ namespace KubernetesWorkflow var podName = GetPodName(container); var recipeName = container.Recipe.Name; - return new CrashWatcher(log, cluster.GetK8sClientConfig(), containerName, podName, recipeName, K8sNamespace); + return new CrashWatcher(log, cluster.GetK8sClientConfig(), containerName, podName, recipeName, K8sNamespace, replacer); } private V1Pod[] FindPodsByLabel(string podLabel) diff --git a/Framework/KubernetesWorkflow/LogHandler.cs b/Framework/KubernetesWorkflow/LogHandler.cs index f185ba17..dfd86e6c 100644 --- a/Framework/KubernetesWorkflow/LogHandler.cs +++ b/Framework/KubernetesWorkflow/LogHandler.cs @@ -4,19 +4,19 @@ namespace KubernetesWorkflow { public interface ILogHandler { - void Log(Stream log); + void Log(Stream log, Func replacer); } public abstract class LogHandler : ILogHandler { - public void Log(Stream log) + public void Log(Stream log, Func replacer) { using var reader = new StreamReader(log); var line = reader.ReadLine(); while (line != null) { - ProcessLine(line); - line = reader.ReadLine(); + line = replacer(reader.ReadLine()); + if (line != null) ProcessLine(line); } } diff --git a/Framework/KubernetesWorkflow/StartupWorkflow.cs b/Framework/KubernetesWorkflow/StartupWorkflow.cs index 529a48ff..4e130042 100644 --- a/Framework/KubernetesWorkflow/StartupWorkflow.cs +++ b/Framework/KubernetesWorkflow/StartupWorkflow.cs @@ -28,16 +28,17 @@ namespace KubernetesWorkflow private readonly WorkflowNumberSource numberSource; private readonly K8sCluster cluster; private readonly string k8sNamespace; + private readonly Func replacer; private readonly RecipeComponentFactory componentFactory = new RecipeComponentFactory(); private readonly LocationProvider locationProvider; - internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, string k8sNamespace) + internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, string k8sNamespace, Func replacer) { this.log = log; this.numberSource = numberSource; this.cluster = cluster; this.k8sNamespace = k8sNamespace; - + this.replacer = replacer; locationProvider = new LocationProvider(log, K8s); } @@ -119,7 +120,7 @@ namespace KubernetesWorkflow { K8s(controller => { - controller.DownloadPodLog(container, logHandler, tailLines, previous); + controller.DownloadPodLog(container, logHandler, tailLines, previous, replacer); }); } @@ -131,7 +132,7 @@ namespace KubernetesWorkflow K8s(controller => { - controller.DownloadPodLog(container, logHandler, tailLines, previous); + controller.DownloadPodLog(container, logHandler, tailLines, previous, replacer); }); return new DownloadedLog(logHandler, container.Name); @@ -257,7 +258,7 @@ namespace KubernetesWorkflow { try { - var controller = new K8sController(log, cluster, numberSource, k8sNamespace); + var controller = new K8sController(log, cluster, numberSource, k8sNamespace, replacer); action(controller); controller.Dispose(); } @@ -272,7 +273,7 @@ namespace KubernetesWorkflow { try { - var controller = new K8sController(log, cluster, numberSource, k8sNamespace); + var controller = new K8sController(log, cluster, numberSource, k8sNamespace, replacer); var result = action(controller); controller.Dispose(); return result; diff --git a/Framework/KubernetesWorkflow/WorkflowCreator.cs b/Framework/KubernetesWorkflow/WorkflowCreator.cs index d70b0bcf..cdadb078 100644 --- a/Framework/KubernetesWorkflow/WorkflowCreator.cs +++ b/Framework/KubernetesWorkflow/WorkflowCreator.cs @@ -25,7 +25,7 @@ namespace KubernetesWorkflow var workflowNumberSource = new WorkflowNumberSource(numberSource.GetNextNumber(), containerNumberSource); - return new StartupWorkflow(log, workflowNumberSource, cluster, GetNamespace(namespaceOverride)); + return new StartupWorkflow(log, workflowNumberSource, cluster, GetNamespace(namespaceOverride), configuration.Replacer); } private string GetNamespace(string? namespaceOverride) diff --git a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs index b35195b3..ad7ca5f4 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin public class RewarderBotContainerRecipe : ContainerRecipeFactory { public override string AppName => "discordbot-rewarder"; - public override string Image => "codexstorage/codex-rewarderbot:sha-8033da1"; + public override string Image => "codexstorage/codex-rewarderbot:sha-fb25372"; protected override void Initialize(StartupConfig startupConfig) { diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index cd833f65..8a79e54c 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -7,7 +7,8 @@ namespace CodexPlugin { public class CodexContainerRecipe : ContainerRecipeFactory { - private const string DefaultDockerImage = "codexstorage/nim-codex:0.1.4"; + private const string DefaultDockerImage = "thatbenbierens/nim-codex:netpeerdebug6"; + //"codexstorage/nim-codex:0.1.4"; public const string ApiPortTag = "codex_api_port"; public const string ListenPortTag = "codex_listen_port"; public const string MetricsPortTag = "codex_metrics_port"; diff --git a/Tests/CodexTests/BasicTests/EventLogginHandler.cs b/Tests/CodexTests/BasicTests/EventLogginHandler.cs new file mode 100644 index 00000000..87349231 --- /dev/null +++ b/Tests/CodexTests/BasicTests/EventLogginHandler.cs @@ -0,0 +1,57 @@ +using CodexContractsPlugin.ChainMonitor; +using GethPlugin; +using Logging; +using System.Numerics; + +namespace CodexTests.BasicTests +{ + public class EventLogginHandler : IChainStateChangeHandler + { + private readonly ILog log; + + public EventLogginHandler(ILog log) + { + this.log = log; + } + + public void OnNewRequest(RequestEvent requestEvent) + { + Log(nameof(OnNewRequest), requestEvent); + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + Log(nameof(OnRequestCancelled), requestEvent); + } + + public void OnRequestFailed(RequestEvent requestEvent) + { + Log(nameof(OnRequestFailed), requestEvent); + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + Log(nameof(OnRequestFinished), requestEvent); + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + Log(nameof(OnRequestFulfilled), requestEvent); + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + Log(nameof(OnSlotFilled), requestEvent, host.ToString(), slotIndex.ToString()); + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + Log(nameof(OnNewRequest), requestEvent, slotIndex.ToString()); + } + + private void Log(string name, object o, params string[] str) + { + log.Log(name + ": " + o.ToString() + " - " + string.Join(",", str)); + } + } +} \ No newline at end of file diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index f9d5b89e..f26b2b55 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -2,6 +2,7 @@ using DistTestCore; using GethPlugin; using MetricsPlugin; +using Nethereum.JsonRpc.Client; using NUnit.Framework; using Utils; @@ -10,6 +11,18 @@ namespace CodexTests.BasicTests [TestFixture] public class ExampleTests : CodexDistTest { + [Test] + public void A() + { + var oneMb = GenerateTestFile(1.MB(), "oneMB"); + var fiveMb = GenerateTestFile(5.MB(), "fiveMb"); + var tenMb = GenerateTestFile(10.MB(), "tenMb"); + var hundredMb = GenerateTestFile(100.MB(), "hundredMb"); + var oneGb = GenerateTestFile(1.GB(), "oneGb"); + + var a = 0; + } + [Test] public void CodexLogExample() { @@ -20,11 +33,41 @@ namespace CodexTests.BasicTests var localDatasets = primary.LocalFiles(); CollectionAssert.Contains(localDatasets.Content.Select(c => c.Cid), cid); + var nameMap = new Dictionary(); + AddNameMapping(nameMap, primary); + + Get().Replacer = line => + { + if (line == null) return null; + foreach (var pair in nameMap) + { + line = line.Replace(pair.Key, pair.Value); + } + return line; + }; + + var log = Ci.DownloadLog(primary); log.AssertLogContains("Uploaded file"); } + + private void AddNameMapping(Dictionary nameMap, ICodexNode node) + { + var name = node.GetName(); + var info = node.GetDebugInfo(); + var nodeId = info.Table.LocalNode.NodeId; + var peerId = info.Table.LocalNode.PeerId; + + nameMap.Add(nodeId, name); + nameMap.Add(peerId, name); + nameMap.Add(CodexUtils.ToShortId(nodeId), name); + nameMap.Add(CodexUtils.ToShortId(peerId), name); + nameMap.Add(CodexUtils.ToNodeIdShortId(nodeId), name); + nameMap.Add(CodexUtils.ToNodeIdShortId(peerId), name); + } + [Test] public void TwoMetricsExample() { diff --git a/Tests/CodexTests/BasicTests/MarketplaceTests.cs b/Tests/CodexTests/BasicTests/MarketplaceTests.cs index 9bb0f6f1..77b53883 100644 --- a/Tests/CodexTests/BasicTests/MarketplaceTests.cs +++ b/Tests/CodexTests/BasicTests/MarketplaceTests.cs @@ -1,4 +1,5 @@ using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; using CodexContractsPlugin.Marketplace; using CodexPlugin; using FileUtils; @@ -113,6 +114,119 @@ namespace CodexTests.BasicTests Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished)); } + [Test] + [Combinatorial] + public void FindBug( + [Values(64)] int numBlocks, + [Values(0)] int plusSizeKb, + [Values(0)] int plusSizeBytes + ) + { + var numberOfHosts = 15; + + var hostInitialBalance = 234.Tst(); + var clientInitialBalance = 100000.Tst(); + var fileSize = new ByteSize( + numBlocks * (64 * 1024) + + plusSizeKb * 1024 + + plusSizeBytes + ); + + var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); + var contracts = Ci.StartCodexContracts(geth); + + var hosts = StartCodex(numberOfHosts, s => s + .WithName("Host") + .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) + { + ContractClock = CodexLogLevel.Trace, + }) + .WithStorageQuota(11.GB()) + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), hostInitialBalance) + .AsStorageNode() + .AsValidator())); + + foreach (var host in hosts) + { + AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance)); + + var availability = new StorageAvailability( + totalSpace: 10.GB(), + maxDuration: TimeSpan.FromMinutes(30), + minPriceForTotalSpace: 1.TstWei(), + maxCollateral: 20.TstWei() + ); + host.Marketplace.MakeStorageAvailable(availability); + } + + var client = StartCodex(s => s + .WithName("Client") + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), clientInitialBalance))); + + var nameMap = new Dictionary(); + AddNameMapping(nameMap, client); + foreach (var host in hosts) AddNameMapping(nameMap, host); + + Get().Replacer = line => + { + if (line == null) return null; + foreach (var pair in nameMap) + { + line = line.Replace(pair.Key, pair.Value); + } + return line; + }; + + var handler = new EventLogginHandler(GetTestLog()); + var chainState = new ChainState(GetTestLog(), contracts, handler, GetTestRunTimeRange().From); + + Task.Run(() => + { + while (true) + { + chainState.Update(); + Thread.Sleep(2000); + } + }); + + while (true) + { + var testFile = CreateFile(fileSize); + var uploadCid = client.UploadFile(testFile); + + var purchase = new StoragePurchaseRequest(uploadCid) + { + PricePerSlotPerSecond = 2.TstWei(), + RequiredCollateral = 10.TstWei(), + MinRequiredNumberOfNodes = 5, + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = TimeSpan.FromMinutes(20), + Expiry = TimeSpan.FromMinutes(10) + }; + + var purchaseContract = client.Marketplace.RequestStorage(purchase); + purchaseContract.WaitForStorageContractStarted(); + } + } + + private void AddNameMapping(Dictionary nameMap, ICodexNode node) + { + var name = node.GetName(); + var info = node.GetDebugInfo(); + var nodeId = info.Table.LocalNode.NodeId; + var peerId = info.Table.LocalNode.PeerId; + + nameMap.Add(nodeId, name); + nameMap.Add(peerId, name); + nameMap.Add(CodexUtils.ToShortId(nodeId), name); + nameMap.Add(CodexUtils.ToShortId(peerId), name); + nameMap.Add(CodexUtils.ToNodeIdShortId(nodeId), name); + nameMap.Add(CodexUtils.ToNodeIdShortId(peerId), name); + } + private TrackedFile CreateFile(ByteSize fileSize) { var segmentSize = new ByteSize(fileSize.SizeInBytes / 4); diff --git a/Tests/CodexTests/BasicTests/ThreeClientTest.cs b/Tests/CodexTests/BasicTests/ThreeClientTest.cs index e6f9428e..78a721e7 100644 --- a/Tests/CodexTests/BasicTests/ThreeClientTest.cs +++ b/Tests/CodexTests/BasicTests/ThreeClientTest.cs @@ -22,6 +22,26 @@ namespace CodexTests.BasicTests testFile.AssertIsEqual(downloadedFile); } + [Test] + public void FindBug() + { + var uploader = StartCodex(); + var downloaders = StartCodex(10); + + var start = DateTime.UtcNow; + while ((DateTime.UtcNow - start) < TimeSpan.FromMinutes(15)) + { + var cid = uploader.UploadFile(GenerateTestFile(5.MB())); + + var loop = Parallel.ForEach(downloaders, d => + { + d.DownloadContent(cid); + }); + + Assert.That(loop.IsCompleted); + } + } + [Test] public void DownloadingUnknownCidDoesNotCauseCrash() { diff --git a/Tests/DistTestCore/Configuration.cs b/Tests/DistTestCore/Configuration.cs index e7e50822..050535cb 100644 --- a/Tests/DistTestCore/Configuration.cs +++ b/Tests/DistTestCore/Configuration.cs @@ -29,12 +29,12 @@ namespace DistTestCore /// public bool AlwaysDownloadContainerLogs { get; set; } - public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet, string k8sNamespace) + public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet, string k8sNamespace, Func replacer) { - return GetK8sConfiguration(timeSet, new DoNothingK8sHooks(), k8sNamespace); + return GetK8sConfiguration(timeSet, new DoNothingK8sHooks(), k8sNamespace, replacer); } - public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet, IK8sHooks hooks, string k8sNamespace) + public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet, IK8sHooks hooks, string k8sNamespace, Func replacer) { var config = new KubernetesWorkflow.Configuration( kubeConfigFile: kubeConfigFile, @@ -45,6 +45,7 @@ namespace DistTestCore config.AllowNamespaceOverride = false; config.Hooks = hooks; + config.Replacer = replacer; return config; } diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 09330df6..65695d60 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -35,7 +35,7 @@ namespace DistTestCore fixtureLog = new FixtureLog(logConfig, startTime, deployId); statusLog = new StatusLog(logConfig, startTime, "dist-tests", deployId); - globalEntryPoint = new EntryPoint(fixtureLog, configuration.GetK8sConfiguration(new DefaultTimeSet(), TestNamespacePrefix), configuration.GetFileManagerFolder()); + globalEntryPoint = new EntryPoint(fixtureLog, configuration.GetK8sConfiguration(new DefaultTimeSet(), TestNamespacePrefix, s => s), configuration.GetFileManagerFolder()); Initialize(fixtureLog); } diff --git a/Tests/DistTestCore/TestLifecycle.cs b/Tests/DistTestCore/TestLifecycle.cs index 675be8c7..d8890a5c 100644 --- a/Tests/DistTestCore/TestLifecycle.cs +++ b/Tests/DistTestCore/TestLifecycle.cs @@ -25,7 +25,7 @@ namespace DistTestCore TestNamespace = testNamespace; TestStart = DateTime.UtcNow; - entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(timeSet, this, testNamespace), configuration.GetFileManagerFolder(), timeSet); + entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(timeSet, this, testNamespace, InternalReplacer), configuration.GetFileManagerFolder(), timeSet); metadata = entryPoint.GetPluginMetadata(); CoreInterface = entryPoint.CreateInterface(); this.deployId = deployId; @@ -33,6 +33,11 @@ namespace DistTestCore log.WriteLogTag(); } + private string? InternalReplacer(string? arg) + { + return Replacer(arg); + } + public DateTime TestStart { get; } public TestLog Log { get; } public Configuration Configuration { get; } @@ -40,6 +45,7 @@ namespace DistTestCore public string TestNamespace { get; } public bool WaitForCleanup { get; } public CoreInterface CoreInterface { get; } + public Func Replacer { get; set; } = s => s; public void DeleteAllResources() { diff --git a/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs b/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs index cd9199ca..22752a29 100644 --- a/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs +++ b/Tests/FrameworkTests/OverwatchTranscript/TranscriptTests.cs @@ -21,6 +21,7 @@ namespace FrameworkTests.OverwatchTranscript [Test] public void WriteAndRun() { + // unstable. WriteTranscript(); ReadTranscript();