Compare commits

..
Author SHA1 Message Date
Corbo12 f330a40ec1 Uncommented tests 2023-07-26 14:32:09 +02:00
Corbo12 a46fd3b447 Merge branch 'master' into feature/tests 2023-07-18 12:38:19 +02:00
Corbo12 a08ef47906 Merge branch 'master' into feature/tests 2023-06-27 15:23:34 +02:00
Corbo12 21292456aa Merge branch 'master' into feature/tests 2023-06-07 14:14:26 +02:00
Corbo12 5da06e08f3 ADD simple upload / download tests 2023-06-07 14:14:03 +02:00
Corbo12 bf33c971a5 UPD mixed tests 2023-05-30 18:55:09 +02:00
Corbo12 d4644a8298 UPD mixed tests 2023-05-30 18:53:03 +02:00
Corbo12 e1953c4177 FIX update membership async 2023-05-30 18:45:23 +02:00
Corbo12 e1302941d2 FIX async membership download tests 2023-05-30 15:35:34 +02:00
Corbo12 bf172be809 Merge branch 'master' into feature/tests 2023-05-30 12:37:06 +02:00
Corbo12 8402bcaa4c FIX download tests 2023-05-24 18:38:38 +02:00
Corbo12 f11645734a FIX download tests 2023-05-24 18:38:08 +02:00
Corbo12 579c1fc6ae DEL extra comments 2023-05-24 18:20:43 +02:00
Corbo12 7124f1dc4d ADD comments and updated upload tests 2023-05-22 15:25:26 +02:00
Corbo12 38e42f2ce9 Merge branch 'master' into feature/tests 2023-05-09 12:04:48 +02:00
Corbo12 27e511f455 ADD basic membership tests 2023-05-03 13:25:27 +02:00
45 changed files with 882 additions and 907 deletions
+5 -6
View File
@@ -15,19 +15,19 @@ on:
workflow_dispatch:
inputs:
branch:
description: Branch (master)
description: Branch
required: false
type: string
source:
description: Repository with tests (current)
description: Repository with tests
required: false
type: string
nameprefix:
description: Runner prefix (cs-codex-dist-tests)
description: Runner job/pod name prefix
required: false
type: string
namespace:
description: Runner namespace (cs-codex-dist-tests)
description: Kubernetes namespace for runner
required: false
type: string
@@ -56,8 +56,6 @@ jobs:
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.nameprefix }}" ]] && echo "NAMEPREFIX=${{ inputs.nameprefix }}" >>"$GITHUB_ENV" || echo "NAMEPREFIX=${{ env.NAMEPREFIX }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
echo "RUNID=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
uses: azure/setup-kubectl@v3
@@ -71,4 +69,5 @@ jobs:
- name: Kubectl - Create Job
run: |
export RUNID=$(date +%Y%m%d-%H%M%S)
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
+1 -1
View File
@@ -88,7 +88,7 @@ namespace CodexNetDeployer
if (!setup.MetricsEnabled) return null;
Log("Starting metrics service...");
var runningContainers = new[] { new RunningContainers(null!, null!, codexContainers.ToArray()) };
var runningContainers = new RunningContainers(null!, null!, codexContainers.ToArray());
return lifecycle.PrometheusStarter.CollectMetricsFor(runningContainers).Containers.Single();
}
+4 -4
View File
@@ -33,10 +33,10 @@ public class Program
}
Console.WriteLine("Using images:" + nl +
$"\tCodex image: '{new CodexContainerRecipe().Image}'" + nl +
$"\tCodexContracts image: '{new CodexContractsContainerRecipe().Image}'" + nl +
$"\tPrometheus image: '{new PrometheusContainerRecipe().Image}'" + nl +
$"\tGeth image: '{new GethContainerRecipe().Image}'" + nl);
$"\tCodex image: '{CodexContainerRecipe.DockerImage}'" + nl +
$"\tCodex Contracts image: '{CodexContractsContainerRecipe.DockerImage}'" + nl +
$"\tPrometheus image: '{PrometheusContainerRecipe.DockerImage}'" + nl +
$"\tGeth image: '{GethContainerRecipe.DockerImage}'" + nl);
if (!args.Any(a => a == "-y"))
{
@@ -3,11 +3,10 @@ dotnet run \
--kube-namespace=codex-continuous-tests \
--nodes=5 \
--validators=3 \
--log-level=Trace \
--storage-quota=2048 \
--storage-sell=1024 \
--min-price=1024 \
--max-collateral=1024 \
--max-duration=3600000 \
--block-ttl=120 \
-y
--block-ttl=120
+2 -7
View File
@@ -25,9 +25,6 @@ namespace ContinuousTests
[Uniform("stop", "s", "STOPONFAIL", false, "If true, runner will stop on first test failure and download all cluster container logs. False by default.")]
public bool StopOnFailure { get; set; } = false;
[Uniform("dl-logs", "dl", "DLLOGS", false, "If true, runner will periodically download and save/append container logs to the log path.")]
public bool DownloadContainerLogs { get; set; } = false;
public CodexDeployment CodexDeployment { get; set; } = null!;
public TestRunnerLocation RunnerLocation { get; set; } = TestRunnerLocation.InternalToCluster;
@@ -60,11 +57,9 @@ namespace ContinuousTests
private static void PrintHelp()
{
var nl = Environment.NewLine;
Console.WriteLine("ContinuousTests will run a set of tests against a codex deployment given a codex-deployment.json file." + nl +
"The tests will run in an endless loop unless otherwise specified, using the test-specific timing values." + nl);
Console.WriteLine("CodexNetDownloader lets you download all container logs given a codex-deployment.json file." + nl);
Console.WriteLine("ContinuousTests assumes you are running this tool from *inside* the Kubernetes cluster. " +
Console.WriteLine("CodexNetDownloader assumes you are running this tool from *inside* the Kubernetes cluster. " +
"If you are not running this from a container inside the cluster, add the argument '--external'." + nl);
}
}
@@ -1,99 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using KubernetesWorkflow;
namespace ContinuousTests
{
public class ContinuousLogDownloader
{
private readonly TestLifecycle lifecycle;
private readonly CodexDeployment deployment;
private readonly string outputPath;
private readonly CancellationToken cancelToken;
public ContinuousLogDownloader(TestLifecycle lifecycle, CodexDeployment deployment, string outputPath, CancellationToken cancelToken)
{
this.lifecycle = lifecycle;
this.deployment = deployment;
this.outputPath = outputPath;
this.cancelToken = cancelToken;
}
public void Run()
{
while (!cancelToken.IsCancellationRequested)
{
UpdateLogs();
cancelToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(15));
}
// After testing has stopped, we wait a little bit and fetch the logs one more time.
// If our latest fetch was not recent, interesting test-related log activity might
// not have been captured yet.
Thread.Sleep(TimeSpan.FromSeconds(10));
UpdateLogs();
}
private void UpdateLogs()
{
foreach (var container in deployment.CodexContainers)
{
UpdateLog(container);
}
}
private void UpdateLog(RunningContainer container)
{
var filepath = Path.Combine(outputPath, GetLogName(container));
if (!File.Exists(filepath))
{
File.WriteAllLines(filepath, new[] { container.Name });
}
var appender = new LogAppender(filepath);
lifecycle.CodexStarter.DownloadLog(container, appender);
}
private static string GetLogName(RunningContainer container)
{
return container.Name
.Replace("<", "")
.Replace(">", "")
+ ".log";
}
}
public class LogAppender : ILogHandler
{
private readonly string filename;
public LogAppender(string filename)
{
this.filename = filename;
}
public void Log(Stream log)
{
using var reader = new StreamReader(log);
var lines = File.ReadAllLines(filename);
var lastLine = lines.Last();
var recording = lines.Length < 3;
var line = reader.ReadLine();
while (line != null)
{
if (recording)
{
File.AppendAllLines(filename, new[] { line });
}
else
{
recording = line == lastLine;
}
line = reader.ReadLine();
}
}
}
}
+1 -16
View File
@@ -24,14 +24,12 @@ namespace ContinuousTests
startupChecker.Check();
var taskFactory = new TaskFactory();
var overviewLog = new FixtureLog(new LogConfig(config.LogPath, false), DateTime.UtcNow, "Overview");
var overviewLog = new FixtureLog(new LogConfig(config.LogPath, false), "Overview");
overviewLog.Log("Continuous tests starting...");
var allTests = testFactory.CreateTests();
ClearAllCustomNamespaces(allTests, overviewLog);
StartLogDownloader(taskFactory);
var testLoops = allTests.Select(t => new TestLoop(taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, cancelToken)).ToArray();
foreach (var testLoop in testLoops)
@@ -63,18 +61,5 @@ namespace ContinuousTests
var (workflowCreator, _) = k8SFactory.CreateFacilities(config.KubeConfigFile, config.LogPath, config.DataPath, test.CustomK8sNamespace, new DefaultTimeSet(), log, config.RunnerLocation);
workflowCreator.CreateWorkflow().DeleteTestResources();
}
private void StartLogDownloader(TaskFactory taskFactory)
{
if (!config.DownloadContainerLogs) return;
var path = Path.Combine(config.LogPath, "containers");
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
var (_, lifecycle) = k8SFactory.CreateFacilities(config.KubeConfigFile, config.LogPath, config.DataPath, config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), new NullLog(), config.RunnerLocation);
var downloader = new ContinuousLogDownloader(lifecycle, config.CodexDeployment, path, cancelToken);
taskFactory.Run(downloader.Run);
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ namespace ContinuousTests
this.handle = handle;
this.cancelToken = cancelToken;
testName = handle.Test.GetType().Name;
fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), DateTime.UtcNow, testName);
fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), testName);
nodes = CreateRandomNodes(handle.Test.RequiredNumberOfNodes);
dataFolder = config.DataPath + "-" + Guid.NewGuid();
+1 -1
View File
@@ -19,7 +19,7 @@ namespace ContinuousTests
public void Check()
{
var log = new FixtureLog(new LogConfig(config.LogPath, false), DateTime.UtcNow, "StartupChecks");
var log = new FixtureLog(new LogConfig(config.LogPath, false), "StartupChecks");
log.Log("Starting continuous test run...");
log.Log("Checking configuration...");
PreflightCheck(config);
+35
View File
@@ -60,5 +60,40 @@
// file.AssertIsEqual(result);
// });
// }
// private void WaitForContractToStart(CodexAccess codexAccess, string purchaseId)
// {
// var lastState = "";
// var waitStart = DateTime.UtcNow;
// var filesizeInMb = fileSize.SizeInBytes / (1024 * 1024);
// var maxWaitTime = TimeSpan.FromSeconds(filesizeInMb * 10.0);
// Log.Log($"{nameof(WaitForContractToStart)} for {Time.FormatDuration(maxWaitTime)}");
// while (lastState != "started")
// {
// CancelToken.ThrowIfCancellationRequested();
// var purchaseStatus = codexAccess.Node.GetPurchaseStatus(purchaseId);
// var statusJson = JsonConvert.SerializeObject(purchaseStatus);
// if (purchaseStatus != null && purchaseStatus.state != lastState)
// {
// lastState = purchaseStatus.state;
// Log.Log("Purchase status: " + statusJson);
// }
// Thread.Sleep(2000);
// if (lastState == "errored")
// {
// Assert.Fail("Contract start failed: " + statusJson);
// }
// if (DateTime.UtcNow - waitStart > maxWaitTime)
// {
// Assert.Fail($"Contract was not picked up within {maxWaitTime.TotalSeconds} seconds timeout: {statusJson}");
// }
// }
// Log.Log("Contract started.");
// }
// }
//}
@@ -1,38 +0,0 @@
# Codex Continuous Test-net Report
Date: 02-08-2023
Report for: 07-2023
## Test-net Status
- Start of month: Offline - faulted
- End of month: Offline - faulted
(Faulted: Tests fail with such frequency that the information gathered does not justify the cost of leaving the test-net running.)
## Deployment Configuration
Continous Test-net is deployed to the kubernetes cluster with the following configuration:
5x Codex Nodes:
- Log-level: Trace
- Storage quota: 2048 MB
- Storage sell: 1024 MB
- Min price: 1024
- Max collateral: 1024
- Max duration: 3600000 seconds
- Block-TTL: 120 seconds
3 of these 5 nodes have:
- Validator: true
Kubernetes namespace: 'codex-continuous-tests'
## Test Overview
| Changes | Test | Description | Status | Results |
|----------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|--------------------------------------------------------------------------------------|
| New in 07-2023 | Two-client test | Every 30 seconds, two nodes are chosen at random. A 10 MB file is generated and uploaded to one. 10 seconds later, it is downloaded from the other. File contents are asserted to be equal. | Faulted | Test reliably fails after 30 to 45 minutes. Both upload and download failures occur. |
| New in 07-2023 | Transient-node test | Every 1 minute, a new, transient Codex node is started and bootstrapped against a random node of the test net. A 10 MB file is generated and uploaded to the transient node. The file is then downloaded from a second random test net node and file equality is asserted. After that, the transient node is shut down. 30 seconds later, a new transient Codex node is started and bootstrapped against a third (guaranteed different from first and second) node from the test net. The same file is then downloaded from the new transient node. File equality is asserted. | Not running | Test was not run because the previous more rudamentory test has faulted. |
## Action Points
- Codex logs (from these long-running containers) are often incomplete when downloaded after a test failure. A reliable way of maintaining these logs is needed. The logs can quickly explode in size, even for test-nets that run only for a few hours.
- The distributed testing setup can and has been used to reproduce the failure of the Two-client test in a local environment. Investigation is on-going.
@@ -1,6 +1,4 @@
dotnet run \
--kube-config=/opt/kubeconfig.yaml \
--codex-deployment=codex-deployment.json \
--keep=1 \
--stop=1 \
--dl-logs=1
--stop=1
-11
View File
@@ -1,6 +1,5 @@
using KubernetesWorkflow;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace DistTestCore.Codex
@@ -61,16 +60,6 @@ namespace DistTestCore.Codex
{
public string version { get; set; } = string.Empty;
public string revision { get; set; } = string.Empty;
public bool IsValid()
{
return !string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(revision);
}
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
public class CodexDebugPeerResponse
+13 -12
View File
@@ -5,8 +5,12 @@ namespace DistTestCore.Codex
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-7efa917";
#if Arm64
public const string DockerImage = "codexstorage/nim-codex:sha-7227a4a";
#else
//public const string DockerImage = "thatbenbierens/nim-codex:loopingyeah";
public const string DockerImage = "codexstorage/nim-codex:sha-7227a4a";
#endif
public const string MetricsPortTag = "metrics_port";
public const string DiscoveryPortTag = "discovery-port";
@@ -14,11 +18,15 @@ namespace DistTestCore.Codex
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
public override string Image { get; }
public static string DockerImageOverride = string.Empty;
public CodexContainerRecipe()
protected override string Image
{
Image = GetDockerImage();
get
{
if (!string.IsNullOrEmpty(DockerImageOverride)) return DockerImageOverride;
return DockerImage;
}
}
protected override void Initialize(StartupConfig startupConfig)
@@ -84,12 +92,5 @@ namespace DistTestCore.Codex
if (marketplaceConfig.AccountIndexOverride != null) return marketplaceConfig.AccountIndexOverride.Value;
return Index;
}
private string GetDockerImage()
{
var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE");
if (!string.IsNullOrEmpty(image)) return image;
return DefaultDockerImage;
}
}
}
+9 -14
View File
@@ -14,13 +14,12 @@ namespace DistTestCore
{
private readonly TestLifecycle lifecycle;
public CodexNodeGroup(TestLifecycle lifecycle, CodexSetup setup, RunningContainers[] containers, ICodexNodeFactory codexNodeFactory)
public CodexNodeGroup(TestLifecycle lifecycle, CodexSetup setup, RunningContainers containers, ICodexNodeFactory codexNodeFactory)
{
this.lifecycle = lifecycle;
Setup = setup;
Containers = containers;
Nodes = containers.Containers().Select(c => CreateOnlineCodexNode(c, codexNodeFactory)).ToArray();
Version = new CodexDebugVersionResponse();
Nodes = containers.Containers.Select(c => CreateOnlineCodexNode(c, codexNodeFactory)).ToArray();
}
public IOnlineCodexNode this[int index]
@@ -45,9 +44,8 @@ namespace DistTestCore
}
public CodexSetup Setup { get; private set; }
public RunningContainers[] Containers { get; private set; }
public RunningContainers Containers { get; private set; }
public OnlineCodexNode[] Nodes { get; private set; }
public CodexDebugVersionResponse Version { get; private set; }
public IEnumerator<IOnlineCodexNode> GetEnumerator()
{
@@ -66,17 +64,14 @@ namespace DistTestCore
public void EnsureOnline()
{
foreach (var node in Nodes) node.EnsureOnlineGetVersionResponse();
var versionResponses = Nodes.Select(n => n.Version);
var first = versionResponses.First();
if (!versionResponses.All(v => v.version == first.version && v.revision == first.revision))
foreach (var node in Nodes)
{
throw new Exception("Inconsistent version information received from one or more Codex nodes: " +
string.Join(",", versionResponses.Select(v => v.ToString())));
var debugInfo = node.CodexAccess.GetDebugInfo();
var nodePeerId = debugInfo.id;
var nodeName = node.CodexAccess.Container.Name;
lifecycle.Log.AddStringReplace(nodePeerId, nodeName);
lifecycle.Log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
}
Version = first;
}
private OnlineCodexNode CreateOnlineCodexNode(RunningContainer c, ICodexNodeFactory factory)
+9 -39
View File
@@ -22,7 +22,6 @@ namespace DistTestCore
var gethStartResult = lifecycle.GethStarter.BringOnlineMarketplaceFor(codexSetup);
var startupConfig = CreateStartupConfig(gethStartResult, codexSetup);
var containers = StartCodexContainers(startupConfig, codexSetup.NumberOfNodes, codexSetup.Location);
var metricAccessFactory = CollectMetrics(codexSetup, containers);
@@ -30,16 +29,9 @@ namespace DistTestCore
var codexNodeFactory = new CodexNodeFactory(lifecycle, metricAccessFactory, gethStartResult.MarketplaceAccessFactory);
var group = CreateCodexGroup(codexSetup, containers, codexNodeFactory);
lifecycle.SetCodexVersion(group.Version);
var nl = Environment.NewLine;
var podInfos = string.Join(nl, containers.Containers().Select(c => $"Container: '{c.Name}' runs at '{c.Pod.PodInfo.K8SNodeName}'={c.Pod.PodInfo.Ip}"));
LogEnd($"Started {codexSetup.NumberOfNodes} nodes " +
$"of image '{containers.Containers().First().Recipe.Image}' " +
$"and version '{group.Version}'{nl}" +
podInfos);
var podInfo = group.Containers.RunningPod.PodInfo;
LogEnd($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers.First().Recipe.Image}' at location '{podInfo.K8SNodeName}'={podInfo.Ip}. They are: {group.Describe()}");
LogSeparator();
return group;
}
@@ -47,7 +39,7 @@ namespace DistTestCore
{
LogStart($"Stopping {group.Describe()}...");
var workflow = CreateWorkflow();
foreach (var c in group.Containers) workflow.Stop(c);
workflow.Stop(group.Containers);
RunningGroups.Remove(group);
LogEnd("Stopped.");
}
@@ -66,7 +58,7 @@ namespace DistTestCore
workflow.DownloadContainerLog(container, logHandler);
}
private IMetricsAccessFactory CollectMetrics(CodexSetup codexSetup, RunningContainers[] containers)
private IMetricsAccessFactory CollectMetrics(CodexSetup codexSetup, RunningContainers containers)
{
if (!codexSetup.MetricsEnabled) return new MetricsUnavailableAccessFactory();
@@ -83,42 +75,20 @@ namespace DistTestCore
return startupConfig;
}
private RunningContainers[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, Location location)
private RunningContainers StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, Location location)
{
var result = new List<RunningContainers>();
var recipe = new CodexContainerRecipe();
for (var i = 0; i < numberOfNodes; i++)
{
var workflow = CreateWorkflow();
result.Add(workflow.Start(1, location, recipe, startupConfig));
}
return result.ToArray();
var workflow = CreateWorkflow();
return workflow.Start(numberOfNodes, location, new CodexContainerRecipe(), startupConfig);
}
private CodexNodeGroup CreateCodexGroup(CodexSetup codexSetup, RunningContainers[] runningContainers, CodexNodeFactory codexNodeFactory)
private CodexNodeGroup CreateCodexGroup(CodexSetup codexSetup, RunningContainers runningContainers, CodexNodeFactory codexNodeFactory)
{
var group = new CodexNodeGroup(lifecycle, codexSetup, runningContainers, codexNodeFactory);
RunningGroups.Add(group);
try
{
Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
}
catch
{
CodexNodesNotOnline(runningContainers);
throw;
}
Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
return group;
}
private void CodexNodesNotOnline(RunningContainers[] runningContainers)
{
Log("Codex nodes failed to start");
foreach (var container in runningContainers.Containers()) lifecycle.DownloadLog(container);
}
private StartupWorkflow CreateWorkflow()
{
return workflowCreator.CreateWorkflow();
+5 -24
View File
@@ -16,7 +16,6 @@ namespace DistTestCore
private readonly Configuration configuration = new Configuration();
private readonly Assembly[] testAssemblies;
private readonly FixtureLog fixtureLog;
private readonly StatusLog statusLog;
private readonly object lifecycleLock = new object();
private readonly Dictionary<string, TestLifecycle> lifecycles = new Dictionary<string, TestLifecycle>();
@@ -25,10 +24,7 @@ namespace DistTestCore
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
var logConfig = configuration.GetLogConfig();
var startTime = DateTime.UtcNow;
fixtureLog = new FixtureLog(logConfig, startTime);
statusLog = new StatusLog(logConfig, startTime);
fixtureLog = new FixtureLog(configuration.GetLogConfig());
PeerConnectionTestHelpers = new PeerConnectionTestHelpers(this);
PeerDownloadTestHelpers = new PeerDownloadTestHelpers(this);
@@ -40,12 +36,6 @@ namespace DistTestCore
[OneTimeSetUp]
public void GlobalSetup()
{
fixtureLog.Log($"Codex Distributed Tests are starting...");
fixtureLog.Log($"Codex image: '{new CodexContainerRecipe().Image}'");
fixtureLog.Log($"CodexContracts image: '{new CodexContractsContainerRecipe().Image}'");
fixtureLog.Log($"Prometheus image: '{new PrometheusContainerRecipe().Image}'");
fixtureLog.Log($"Geth image: '{new GethContainerRecipe().Image}'");
// Previous test run may have been interrupted.
// Begin by cleaning everything up.
try
@@ -64,6 +54,9 @@ namespace DistTestCore
}
fixtureLog.Log("Global setup cleanup successful");
fixtureLog.Log($"Codex image: '{CodexContainerRecipe.DockerImage}'");
fixtureLog.Log($"Prometheus image: '{PrometheusContainerRecipe.DockerImage}'");
fixtureLog.Log($"Geth image: '{GethContainerRecipe.DockerImage}'");
}
[SetUp]
@@ -190,7 +183,6 @@ namespace DistTestCore
private void CreateNewTestLifecycle()
{
var testName = GetCurrentTestName();
fixtureLog.WriteLogTag();
Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () =>
{
lock (lifecycleLock)
@@ -203,10 +195,7 @@ namespace DistTestCore
private void DisposeTestLifecycle()
{
var lifecycle = Get();
var testResult = GetTestResult();
var testDuration = lifecycle.GetTestDuration();
fixtureLog.Log($"{GetCurrentTestName()} = {testResult} ({testDuration})");
statusLog.ConcludeTest(testResult, testDuration, GetCodexId(lifecycle));
fixtureLog.Log($"{GetCurrentTestName()} = {GetTestResult()} ({lifecycle.GetTestDuration()})");
Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", () =>
{
lifecycle.Log.EndTest();
@@ -216,14 +205,6 @@ namespace DistTestCore
});
}
private static string GetCodexId(TestLifecycle lifecycle)
{
var v = lifecycle.CodexVersion;
if (v == null) return new CodexContainerRecipe().Image;
if (v.version != "untagged build") return v.version;
return v.revision;
}
private ITimeSet GetTimeSet()
{
if (ShouldUseLongTimeouts()) return new LongTimeSet();
@@ -1,211 +0,0 @@
using DistTestCore.Codex;
using NUnit.Framework;
using Utils;
namespace DistTestCore.Helpers
{
public interface IFullConnectivityImplementation
{
string Description();
string ValidateEntry(FullConnectivityHelper.Entry entry, FullConnectivityHelper.Entry[] allEntries);
FullConnectivityHelper.PeerConnectionState Check(FullConnectivityHelper.Entry from, FullConnectivityHelper.Entry to);
}
public class FullConnectivityHelper
{
private static string Nl = Environment.NewLine;
private readonly DistTest test;
private readonly IFullConnectivityImplementation implementation;
public FullConnectivityHelper(DistTest test, IFullConnectivityImplementation implementation)
{
this.test = test;
this.implementation = implementation;
}
public void AssertFullyConnected(IEnumerable<IOnlineCodexNode> nodes)
{
AssertFullyConnected(nodes.ToArray());
}
private void AssertFullyConnected(IOnlineCodexNode[] nodes)
{
test.Log($"Asserting '{implementation.Description()}' for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
var entries = CreateEntries(nodes);
var pairs = CreatePairs(entries);
RetryWhilePairs(pairs, () =>
{
CheckAndRemoveSuccessful(pairs);
});
if (pairs.Any())
{
var pairDetails = string.Join(Nl, pairs.SelectMany(p => p.GetResultMessages()));
test.Log($"Connections failed:{Nl}{pairDetails}");
Assert.Fail(string.Join(Nl, pairs.SelectMany(p => p.GetResultMessages())));
}
else
{
test.Log($"'{implementation.Description()}' = Success! for nodes: {string.Join(",", nodes.Select(n => n.GetName()))}");
}
}
private static void RetryWhilePairs(List<Pair> pairs, Action action)
{
var timeout = DateTime.UtcNow + TimeSpan.FromMinutes(5);
while (pairs.Any(p => p.Inconclusive) && timeout > DateTime.UtcNow)
{
action();
Time.Sleep(TimeSpan.FromSeconds(2));
}
}
private void CheckAndRemoveSuccessful(List<Pair> pairs)
{
// For large sets, don't try and do all of them at once.
var selectedPair = pairs.Take(20).ToArray();
var pairDetails = new List<string>();
foreach (var pair in selectedPair)
{
test.ScopedTestFiles(pair.Check);
if (pair.Success)
{
pairDetails.AddRange(pair.GetResultMessages());
pairs.Remove(pair);
}
}
test.Log($"Connections successful:{Nl}{string.Join(Nl, pairDetails)}");
}
private Entry[] CreateEntries(IOnlineCodexNode[] nodes)
{
var entries = nodes.Select(n => new Entry(n)).ToArray();
var errors = entries
.Select(e => implementation.ValidateEntry(e, entries))
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
if (errors.Any())
{
Assert.Fail("Some node entries failed to validate: " + string.Join(Nl, errors));
}
return entries;
}
private List<Pair> CreatePairs(Entry[] entries)
{
return CreatePairsIterator(entries).ToList();
}
private IEnumerable<Pair> CreatePairsIterator(Entry[] entries)
{
for (var x = 0; x < entries.Length; x++)
{
for (var y = x + 1; y < entries.Length; y++)
{
yield return new Pair(implementation, entries[x], entries[y]);
}
}
}
public class Entry
{
public Entry(IOnlineCodexNode node)
{
Node = node;
Response = node.GetDebugInfo();
}
public IOnlineCodexNode Node { get; }
public CodexDebugResponse Response { get; }
public override string ToString()
{
if (Response == null || string.IsNullOrEmpty(Response.id)) return "UNKNOWN";
return Response.id;
}
}
public enum PeerConnectionState
{
Unknown,
Connection,
NoConnection,
}
public class Pair
{
private TimeSpan aToBTime = TimeSpan.FromSeconds(0);
private TimeSpan bToATime = TimeSpan.FromSeconds(0);
private readonly IFullConnectivityImplementation implementation;
public Pair(IFullConnectivityImplementation implementation, Entry a, Entry b)
{
this.implementation = implementation;
A = a;
B = b;
}
public Entry A { get; }
public Entry B { get; }
public PeerConnectionState AKnowsB { get; private set; }
public PeerConnectionState BKnowsA { get; private set; }
public bool Success { get { return AKnowsB == PeerConnectionState.Connection && BKnowsA == PeerConnectionState.Connection; } }
public bool Inconclusive { get { return AKnowsB == PeerConnectionState.Unknown || BKnowsA == PeerConnectionState.Unknown; } }
public void Check()
{
aToBTime = Measure(() => AKnowsB = Check(A, B));
bToATime = Measure(() => BKnowsA = Check(B, A));
}
public override string ToString()
{
return $"[{string.Join(",", GetResultMessages())}]";
}
public string[] GetResultMessages()
{
var aName = A.ToString();
var bName = B.ToString();
return new[]
{
$"[{aName} --> {bName}] = {AKnowsB} ({aToBTime.TotalSeconds} seconds)",
$"[{aName} <-- {bName}] = {BKnowsA} ({bToATime.TotalSeconds} seconds)"
};
}
private static TimeSpan Measure(Action action)
{
var start = DateTime.UtcNow;
action();
return DateTime.UtcNow - start;
}
private PeerConnectionState Check(Entry from, Entry to)
{
Thread.Sleep(10);
try
{
return implementation.Check(from, to);
}
catch
{
// Didn't get a conclusive answer. Try again later.
return PeerConnectionState.Unknown;
}
}
}
}
}
+226 -35
View File
@@ -1,66 +1,257 @@
using DistTestCore.Codex;
using static DistTestCore.Helpers.FullConnectivityHelper;
using NUnit.Framework;
using Utils;
namespace DistTestCore.Helpers
{
public class PeerConnectionTestHelpers : IFullConnectivityImplementation
public class PeerConnectionTestHelpers
{
private readonly FullConnectivityHelper helper;
private readonly Random random = new Random();
private readonly DistTest test;
public PeerConnectionTestHelpers(DistTest test)
{
helper = new FullConnectivityHelper(test, this);
this.test = test;
}
public void AssertFullyConnected(IEnumerable<IOnlineCodexNode> nodes)
{
helper.AssertFullyConnected(nodes);
}
var n = nodes.ToArray();
public string Description()
{
return "Peer Discovery";
}
AssertFullyConnected(n);
public string ValidateEntry(Entry entry, Entry[] allEntries)
{
var result = string.Empty;
foreach (var peer in entry.Response.table.nodes)
for (int i = 0; i < 5; i++)
{
var expected = GetExpectedDiscoveryEndpoint(allEntries, peer);
if (expected != peer.address)
Time.Sleep(TimeSpan.FromSeconds(30));
AssertFullyConnected(n);
}
}
private void AssertFullyConnected(IOnlineCodexNode[] nodes)
{
test.Log($"Asserting peers are fully-connected for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
var entries = CreateEntries(nodes);
var pairs = CreatePairs(entries);
RetryWhilePairs(pairs, () =>
{
CheckAndRemoveSuccessful(pairs);
});
if (pairs.Any())
{
test.Log($"Unsuccessful! Peers are not fully-connected: {string.Join(",", nodes.Select(n => n.GetName()))}");
Assert.Fail(string.Join(Environment.NewLine, pairs.Select(p => p.GetMessage())));
test.Log(string.Join(Environment.NewLine, pairs.Select(p => p.GetMessage())));
}
else
{
test.Log($"Success! Peers are fully-connected: {string.Join(",", nodes.Select(n => n.GetName()))}");
}
}
private static void RetryWhilePairs(List<Pair> pairs, Action action)
{
var timeout = DateTime.UtcNow + TimeSpan.FromSeconds(30);
while (pairs.Any() && timeout > DateTime.UtcNow)
{
action();
if (pairs.Any()) Time.Sleep(TimeSpan.FromSeconds(2));
}
}
private void CheckAndRemoveSuccessful(List<Pair> pairs)
{
var checkTasks = pairs.Select(p => Task.Run(() =>
{
ApplyRandomDelay();
p.Check();
})).ToArray();
Task.WaitAll(checkTasks);
foreach (var pair in pairs.ToArray())
{
if (pair.Success)
{
result += $"Node:{entry.Node.GetName()} has incorrect peer table entry. Was: '{peer.address}', expected: '{expected}'. ";
test.Log(pair.GetMessage());
pairs.Remove(pair);
}
}
return result;
}
public PeerConnectionState Check(Entry from, Entry to)
private static Entry[] CreateEntries(IOnlineCodexNode[] nodes)
{
var peerId = to.Response.id;
var entries = nodes.Select(n => new Entry(n)).ToArray();
var incorrectDiscoveryEndpoints = entries.SelectMany(e => e.GetInCorrectDiscoveryEndpoints(entries)).ToArray();
var response = from.Node.GetDebugPeer(peerId);
if (!response.IsPeerFound)
if (incorrectDiscoveryEndpoints.Any())
{
return PeerConnectionState.NoConnection;
Assert.Fail("Some nodes contain peer records with incorrect discovery ip/port information: " +
string.Join(Environment.NewLine, incorrectDiscoveryEndpoints));
}
if (!string.IsNullOrEmpty(response.peerId) && response.addresses.Any())
{
return PeerConnectionState.Connection;
}
return PeerConnectionState.Unknown;
return entries;
}
private static string GetExpectedDiscoveryEndpoint(Entry[] allEntries, CodexDebugTableNodeResponse node)
private static List<Pair> CreatePairs(Entry[] entries)
{
var peer = allEntries.SingleOrDefault(e => e.Response.table.localNode.peerId == node.peerId);
if (peer == null) return $"peerId: {node.peerId} is not known.";
return CreatePairsIterator(entries).ToList();
}
var n = (OnlineCodexNode)peer.Node;
var ip = n.CodexAccess.Container.Pod.PodInfo.Ip;
var discPort = n.CodexAccess.Container.Recipe.GetPortByTag(CodexContainerRecipe.DiscoveryPortTag);
return $"{ip}:{discPort.Number}";
private static IEnumerable<Pair> CreatePairsIterator(Entry[] entries)
{
for (var x = 0; x < entries.Length; x++)
{
for (var y = x + 1; y < entries.Length; y++)
{
yield return new Pair(entries[x], entries[y]);
}
}
}
private void ApplyRandomDelay()
{
// Calling all the nodes all at the same time is not exactly nice.
Time.Sleep(TimeSpan.FromMicroseconds(random.Next(10, 1000)));
}
public class Entry
{
public Entry(IOnlineCodexNode node)
{
Node = node;
Response = node.GetDebugInfo();
}
public IOnlineCodexNode Node { get; }
public CodexDebugResponse Response { get; }
public IEnumerable<string> GetInCorrectDiscoveryEndpoints(Entry[] allEntries)
{
foreach (var peer in Response.table.nodes)
{
var expected = GetExpectedDiscoveryEndpoint(allEntries, peer);
if (expected != peer.address)
{
yield return $"Node:{Node.GetName()} has incorrect peer table entry. Was: '{peer.address}', expected: '{expected}'";
}
}
}
public override string ToString()
{
if (Response == null || string.IsNullOrEmpty(Response.id)) return "UNKNOWN";
return Response.id;
}
private static string GetExpectedDiscoveryEndpoint(Entry[] allEntries, CodexDebugTableNodeResponse node)
{
var peer = allEntries.SingleOrDefault(e => e.Response.table.localNode.peerId == node.peerId);
if (peer == null) return $"peerId: {node.peerId} is not known.";
var n = (OnlineCodexNode)peer.Node;
var ip = n.CodexAccess.Container.Pod.PodInfo.Ip;
var discPort = n.CodexAccess.Container.Recipe.GetPortByTag(CodexContainerRecipe.DiscoveryPortTag);
return $"{ip}:{discPort.Number}";
}
}
public enum PeerConnectionState
{
Unknown,
Connection,
NoConnection,
}
public class Pair
{
private TimeSpan aToBTime = TimeSpan.FromSeconds(0);
private TimeSpan bToATime = TimeSpan.FromSeconds(0);
public Pair(Entry a, Entry b)
{
A = a;
B = b;
}
public Entry A { get; }
public Entry B { get; }
public PeerConnectionState AKnowsB { get; private set; }
public PeerConnectionState BKnowsA { get; private set; }
public bool Success { get { return AKnowsB == PeerConnectionState.Connection && BKnowsA == PeerConnectionState.Connection; } }
public void Check()
{
aToBTime = Measure(() => AKnowsB = Knows(A, B));
bToATime = Measure(() => BKnowsA = Knows(B, A));
}
public string GetMessage()
{
return GetResultMessage() + GetTimePostfix();
}
public override string ToString()
{
return $"[{GetMessage()}]";
}
private string GetResultMessage()
{
var aName = A.ToString();
var bName = B.ToString();
if (Success)
{
return $"{aName} and {bName} know each other.";
}
return $"[{aName}-->{bName}] = {AKnowsB} AND [{aName}<--{bName}] = {BKnowsA}";
}
private string GetTimePostfix()
{
var aName = A.ToString();
var bName = B.ToString();
return $" ({aName}->{bName}: {aToBTime.TotalMinutes} seconds, {bName}->{aName}: {bToATime.TotalSeconds} seconds)";
}
private static TimeSpan Measure(Action action)
{
var start = DateTime.UtcNow;
action();
return DateTime.UtcNow - start;
}
private PeerConnectionState Knows(Entry a, Entry b)
{
lock (a)
{
var peerId = b.Response.id;
try
{
var response = a.Node.GetDebugPeer(peerId);
if (!response.IsPeerFound)
{
return PeerConnectionState.NoConnection;
}
if (!string.IsNullOrEmpty(response.peerId) && response.addresses.Any())
{
return PeerConnectionState.Connection;
}
}
catch
{
}
// Didn't get a conclusive answer. Try again later.
return PeerConnectionState.Unknown;
}
}
}
}
}
+49 -38
View File
@@ -1,63 +1,74 @@
using static DistTestCore.Helpers.FullConnectivityHelper;
using DistTestCore.Codex;
using NUnit.Framework;
namespace DistTestCore.Helpers
{
public class PeerDownloadTestHelpers : IFullConnectivityImplementation
public class PeerDownloadTestHelpers
{
private readonly FullConnectivityHelper helper;
private readonly DistTest test;
private ByteSize testFileSize;
public PeerDownloadTestHelpers(DistTest test)
{
helper = new FullConnectivityHelper(test, this);
testFileSize = 1.MB();
this.test = test;
}
public void AssertFullDownloadInterconnectivity(IEnumerable<IOnlineCodexNode> nodes, ByteSize testFileSize)
{
this.testFileSize = testFileSize;
helper.AssertFullyConnected(nodes);
}
test.Log($"Asserting full download interconnectivity for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
var start = DateTime.UtcNow;
public string Description()
{
return "Download Connectivity";
}
public string ValidateEntry(Entry entry, Entry[] allEntries)
{
return string.Empty;
}
public PeerConnectionState Check(Entry from, Entry to)
{
var expectedFile = GenerateTestFile(from.Node, to.Node);
var contentId = from.Node.UploadFile(expectedFile);
try
foreach (var node in nodes)
{
var downloadedFile = to.Node.DownloadContent(contentId, expectedFile.Label + "_downloaded");
var uploader = node;
var downloaders = nodes.Where(n => n != uploader).ToArray();
test.ScopedTestFiles(() =>
{
PerformTest(uploader, downloaders, testFileSize);
});
}
test.Log($"Success! Full download interconnectivity for nodes: {string.Join(",", nodes.Select(n => n.GetName()))}");
var timeTaken = DateTime.UtcNow - start;
AssertTimePerMB(timeTaken, nodes.Count(), testFileSize);
}
private void AssertTimePerMB(TimeSpan timeTaken, int numberOfNodes, ByteSize size)
{
var numberOfDownloads = numberOfNodes * (numberOfNodes - 1);
var timePerDownload = timeTaken / numberOfDownloads;
float sizeInMB = size.ToMB();
var timePerMB = timePerDownload / sizeInMB;
test.Log($"Performed {numberOfDownloads} downloads of {size} in {timeTaken.TotalSeconds} seconds, for an average of {timePerMB.TotalSeconds} seconds per MB.");
Assert.That(timePerMB, Is.LessThan(CodexContainerRecipe.MaxDownloadTimePerMegabyte), "MaxDownloadTimePerMegabyte performance threshold breached.");
}
private void PerformTest(IOnlineCodexNode uploader, IOnlineCodexNode[] downloaders, ByteSize testFileSize)
{
// Generate 1 test file per downloader.
var files = downloaders.Select(d => GenerateTestFile(uploader, d, testFileSize)).ToArray();
// Upload all the test files to the uploader.
var contentIds = files.Select(uploader.UploadFile).ToArray();
// Each downloader should retrieve its own test file.
for (var i = 0; i < downloaders.Length; i++)
{
var expectedFile = files[i];
var downloadedFile = downloaders[i].DownloadContent(contentIds[i], $"{expectedFile.Label}DOWNLOADED");
expectedFile.AssertIsEqual(downloadedFile);
return PeerConnectionState.Connection;
}
catch
{
// Should an exception occur during the download or file-content assertion,
// We consider that as no-connection for the purpose of this test.
return PeerConnectionState.NoConnection;
}
// Should an exception occur during upload, then this try is inconclusive and we try again next loop.
}
private TestFile GenerateTestFile(IOnlineCodexNode uploader, IOnlineCodexNode downloader)
private TestFile GenerateTestFile(IOnlineCodexNode uploader, IOnlineCodexNode downloader, ByteSize testFileSize)
{
var up = uploader.GetName().Replace("<", "").Replace(">", "");
var down = downloader.GetName().Replace("<", "").Replace(">", "");
var label = $"~from:{up}-to:{down}~";
var label = $"FROM{up}TO{down}";
return test.GenerateTestFile(testFileSize, label);
}
}
@@ -4,15 +4,15 @@ namespace DistTestCore.Marketplace
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
#if Arm64
public const string DockerImage = "emizzle/codex-contracts-deployment:latest";
#else
public const string DockerImage = "thatbenbierens/codex-contracts-deployment:nomint2";
#endif
public const string MarketplaceAddressFilename = "/usr/app/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/usr/app/artifacts/contracts/Marketplace.sol/Marketplace.json";
public override string Image { get; }
public CodexContractsContainerRecipe()
{
Image = "codexstorage/dist-tests-codex-contracts-eth:sha-b4e4897";
}
protected override string Image => DockerImage;
protected override void Initialize(StartupConfig startupConfig)
{
@@ -4,18 +4,19 @@ namespace DistTestCore.Marketplace
{
public class GethContainerRecipe : ContainerRecipeFactory
{
private const string defaultArgs = "--ipcdisable --syncmode full";
#if Arm64
public const string DockerImage = "emizzle/geth-confenv:latest";
#else
public const string DockerImage = "thatbenbierens/geth-confenv:onethousand";
#endif
public const string HttpPortTag = "http_port";
public const string DiscoveryPortTag = "disc_port";
private const string defaultArgs = "--ipcdisable --syncmode full";
public const string AccountsFilename = "accounts.csv";
public override string Image { get; }
public GethContainerRecipe()
{
Image = "codexstorage/dist-tests-geth:sha-b788a2d";
}
protected override string Image => DockerImage;
protected override void Initialize(StartupConfig startupConfig)
{
+6 -96
View File
@@ -1,7 +1,5 @@
using DistTestCore.Codex;
using DistTestCore.Helpers;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using System.Numerics;
@@ -12,7 +10,7 @@ namespace DistTestCore.Marketplace
public interface IMarketplaceAccess
{
string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan maxDuration);
StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
string RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
void AssertThatBalance(IResolveConstraint constraint, string message = "");
TestToken GetBalance();
}
@@ -32,7 +30,7 @@ namespace DistTestCore.Marketplace
this.codexAccess = codexAccess;
}
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
public string RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
{
var request = new CodexSalesRequestStorageRequest
{
@@ -59,9 +57,9 @@ namespace DistTestCore.Marketplace
throw new InvalidOperationException(response);
}
Log($"Storage requested successfully. PurchaseId: '{response}'.");
Log($"Storage requested successfully. PurchaseId: {response}");
return new StoragePurchaseContract(lifecycle.Log, codexAccess, response, duration);
return response;
}
public string MakeStorageAvailable(ByteSize totalSpace, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration)
@@ -123,10 +121,10 @@ namespace DistTestCore.Marketplace
public class MarketplaceUnavailable : IMarketplaceAccess
{
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
public string RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
{
Unavailable();
return null!;
return string.Empty;
}
public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration)
@@ -152,92 +150,4 @@ namespace DistTestCore.Marketplace
throw new InvalidOperationException();
}
}
public class StoragePurchaseContract
{
private readonly BaseLog log;
private readonly CodexAccess codexAccess;
private DateTime? contractStartUtc;
public StoragePurchaseContract(BaseLog log, CodexAccess codexAccess, string purchaseId, TimeSpan contractDuration)
{
this.log = log;
this.codexAccess = codexAccess;
PurchaseId = purchaseId;
ContractDuration = contractDuration;
}
public string PurchaseId { get; }
public TimeSpan ContractDuration { get; }
public void WaitForStorageContractStarted()
{
WaitForStorageContractStarted(TimeSpan.FromSeconds(30));
}
public void WaitForStorageContractFinished()
{
if (!contractStartUtc.HasValue)
{
WaitForStorageContractStarted();
}
var gracePeriod = TimeSpan.FromSeconds(10);
var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value;
var timeout = (ContractDuration - currentContractTime) + gracePeriod;
WaitForStorageContractState(timeout, "finished");
}
/// <summary>
/// Wait for contract to start. Max timeout depends on contract filesize. Allows more time for larger files.
/// </summary>
public void WaitForStorageContractStarted(ByteSize contractFileSize)
{
var filesizeInMb = contractFileSize.SizeInBytes / (1024 * 1024);
var maxWaitTime = TimeSpan.FromSeconds(filesizeInMb * 10.0);
WaitForStorageContractStarted(maxWaitTime);
}
public void WaitForStorageContractStarted(TimeSpan timeout)
{
WaitForStorageContractState(timeout, "started");
contractStartUtc = DateTime.UtcNow;
}
private void WaitForStorageContractState(TimeSpan timeout, string desiredState)
{
var lastState = "";
var waitStart = DateTime.UtcNow;
log.Log($"Waiting for {Time.FormatDuration(timeout)} for contract '{PurchaseId}' to reach state '{desiredState}'.");
while (lastState != desiredState)
{
var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
var statusJson = JsonConvert.SerializeObject(purchaseStatus);
if (purchaseStatus != null && purchaseStatus.state != lastState)
{
lastState = purchaseStatus.state;
log.Debug("Purchase status: " + statusJson);
}
Thread.Sleep(1000);
if (lastState == "errored")
{
Assert.Fail("Contract errored: " + statusJson);
}
if (DateTime.UtcNow - waitStart > timeout)
{
Assert.Fail($"Contract did not reach '{desiredState}' within timeout. {statusJson}");
}
}
log.Log($"Contract '{desiredState}'.");
}
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
}
}
@@ -4,12 +4,9 @@ namespace DistTestCore.Metrics
{
public class PrometheusContainerRecipe : ContainerRecipeFactory
{
public override string Image { get; }
public const string DockerImage = "thatbenbierens/prometheus-envconf:latest";
public PrometheusContainerRecipe()
{
Image = "codexstorage/dist-tests-prometheus:sha-f97d7fd";
}
protected override string Image => DockerImage;
protected override void Initialize(StartupConfig startupConfig)
{
+9 -20
View File
@@ -18,7 +18,6 @@ namespace DistTestCore
IDownloadedLog DownloadLog();
IMetricsAccess Metrics { get; }
IMarketplaceAccess Marketplace { get; }
CodexDebugVersionResponse Version { get; }
ICodexSetup BringOffline();
}
@@ -35,14 +34,12 @@ namespace DistTestCore
Group = group;
Metrics = metricsAccess;
Marketplace = marketplaceAccess;
Version = new CodexDebugVersionResponse();
}
public CodexAccess CodexAccess { get; }
public CodexNodeGroup Group { get; }
public IMetricsAccess Metrics { get; }
public IMarketplaceAccess Marketplace { get; }
public CodexDebugVersionResponse Version { get; private set; }
public string GetName()
{
@@ -75,6 +72,9 @@ namespace DistTestCore
if (string.IsNullOrEmpty(response)) Assert.Fail("Received empty response.");
if (response.StartsWith(UploadFailedMessage)) Assert.Fail("Node failed to store block.");
var logReplacement = $"(CID:{file.Describe()})";
Log($"ContentId '{response}' is {logReplacement}");
lifecycle.Log.AddStringReplace(response, logReplacement);
Log($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
}
@@ -114,30 +114,19 @@ namespace DistTestCore
return Group.BringOffline();
}
public void EnsureOnlineGetVersionResponse()
{
var debugInfo = CodexAccess.GetDebugInfo();
var nodePeerId = debugInfo.id;
var nodeName = CodexAccess.Container.Name;
if (!debugInfo.codex.IsValid())
{
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.codex}");
}
lifecycle.Log.AddStringReplace(nodePeerId, nodeName);
lifecycle.Log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
Version = debugInfo.codex;
}
private string GetPeerMultiAddress(OnlineCodexNode peer, CodexDebugResponse peerInfo)
{
var multiAddress = peerInfo.addrs.First();
// Todo: Is there a case where First address in list is not the way?
if (Group == peer.Group)
{
return multiAddress;
}
// The peer we want to connect is in a different pod.
// We must replace the default IP with the pod IP in the multiAddress.
return multiAddress.Replace("0.0.0.0", peer.CodexAccess.Container.Pod.PodInfo.Ip);
return multiAddress.Replace("0.0.0.0", peer.Group.Containers.RunningPod.PodInfo.Ip);
}
private void DownloadToFile(string contentId, TestFile file)
+2 -2
View File
@@ -12,11 +12,11 @@ namespace DistTestCore
{
}
public RunningContainers CollectMetricsFor(RunningContainers[] containers)
public RunningContainers CollectMetricsFor(RunningContainers containers)
{
LogStart($"Starting metrics server for {containers.Describe()}");
var startupConfig = new StartupConfig();
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(containers.Containers())));
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(containers.Containers)));
var workflow = workflowCreator.CreateWorkflow();
var runningContainers = workflow.Start(1, Location.Unspecified, new PrometheusContainerRecipe(), startupConfig);
+2 -12
View File
@@ -1,5 +1,4 @@
using DistTestCore.Codex;
using DistTestCore.Logs;
using DistTestCore.Logs;
using KubernetesWorkflow;
using Logging;
using Utils;
@@ -8,7 +7,7 @@ namespace DistTestCore
{
public class TestLifecycle
{
private readonly DateTime testStart;
private DateTime testStart = DateTime.MinValue;
public TestLifecycle(BaseLog log, Configuration configuration, ITimeSet timeSet)
: this(log, configuration, timeSet, new WorkflowCreator(log, configuration.GetK8sConfiguration(timeSet)))
@@ -26,9 +25,6 @@ namespace DistTestCore
PrometheusStarter = new PrometheusStarter(this, workflowCreator);
GethStarter = new GethStarter(this, workflowCreator);
testStart = DateTime.UtcNow;
CodexVersion = null;
Log.WriteLogTag();
}
public BaseLog Log { get; }
@@ -38,7 +34,6 @@ namespace DistTestCore
public CodexStarter CodexStarter { get; }
public PrometheusStarter PrometheusStarter { get; }
public GethStarter GethStarter { get; }
public CodexDebugVersionResponse? CodexVersion { get; private set; }
public void DeleteAllResources()
{
@@ -63,10 +58,5 @@ namespace DistTestCore
var testDuration = DateTime.UtcNow - testStart;
return Time.FormatDuration(testDuration);
}
public void SetCodexVersion(CodexDebugVersionResponse version)
{
if (CodexVersion == null) CodexVersion = version;
}
}
}
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": ".."
},
{
"path": "../../../CodexTestLogs"
}
],
"settings": {}
}
+1 -1
View File
@@ -27,7 +27,7 @@
return recipe;
}
public abstract string Image { get; }
protected abstract string Image { get; }
protected int ContainerNumber { get; private set; } = 0;
protected int Index { get; private set; } = 0;
protected abstract void Initialize(StartupConfig config);
-13
View File
@@ -40,17 +40,4 @@ namespace KubernetesWorkflow
public Address ClusterExternalAddress { get; }
public Address ClusterInternalAddress { get; }
}
public static class RunningContainersExtensions
{
public static RunningContainer[] Containers(this RunningContainers[] runningContainers)
{
return runningContainers.SelectMany(c => c.Containers).ToArray();
}
public static string Describe(this RunningContainers[] runningContainers)
{
return string.Join(",", runningContainers.Select(c => c.Describe()));
}
}
}
-8
View File
@@ -73,14 +73,6 @@ namespace Logging
return new LogFile($"{GetFullName()}_{GetSubfileNumber()}", ext);
}
public void WriteLogTag()
{
var runId = NameUtils.GetRunId();
var category = NameUtils.GetCategoryName();
var name = NameUtils.GetTestMethodName();
LogFile.WriteRaw($"{runId} {category} {name}");
}
private string ApplyReplacements(string str)
{
foreach (var replacement in replacements)
+32 -3
View File
@@ -1,14 +1,20 @@
namespace Logging
using NUnit.Framework;
namespace Logging
{
public class FixtureLog : BaseLog
{
private readonly DateTime start;
private readonly string fullName;
private readonly LogConfig config;
public FixtureLog(LogConfig config, DateTime start, string name = "")
public FixtureLog(LogConfig config, string name = "")
: base(config.DebugEnabled)
{
fullName = NameUtils.GetFixtureFullName(config, start, name);
start = DateTime.UtcNow;
var folder = DetermineFolder(config);
var fixtureName = GetFixtureName(name);
fullName = Path.Combine(folder, fixtureName);
this.config = config;
}
@@ -26,5 +32,28 @@
{
return fullName;
}
private string DetermineFolder(LogConfig config)
{
return Path.Join(
config.LogRoot,
$"{start.Year}-{Pad(start.Month)}",
Pad(start.Day));
}
private string GetFixtureName(string name)
{
var test = TestContext.CurrentContext.Test;
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
if (!string.IsNullOrEmpty(name)) className = name;
return $"{Pad(start.Hour)}-{Pad(start.Minute)}-{Pad(start.Second)}Z_{className.Replace('.', '-')}";
}
private static string Pad(int n)
{
return n.ToString().PadLeft(2, '0');
}
}
}
+1 -1
View File
@@ -49,7 +49,7 @@
private static string GetTimestamp()
{
return $"[{DateTime.UtcNow.ToString("o")}]";
return $"[{DateTime.UtcNow.ToString("u")}]";
}
private void EnsurePathExists(string filename)
-83
View File
@@ -1,83 +0,0 @@
using NUnit.Framework;
namespace Logging
{
public static class NameUtils
{
public static string GetTestMethodName(string name = "")
{
if (!string.IsNullOrEmpty(name)) return name;
var test = TestContext.CurrentContext.Test;
var args = FormatArguments(test);
return ReplaceInvalidCharacters($"{test.MethodName}{args}");
}
public static string GetFixtureFullName(LogConfig config, DateTime start, string name)
{
var folder = DetermineFolder(config, start);
var fixtureName = GetFixtureName(name, start);
return Path.Combine(folder, fixtureName);
}
public static string GetRawFixtureName()
{
var test = TestContext.CurrentContext.Test;
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
return className.Replace('.', '-');
}
public static string GetCategoryName()
{
var test = TestContext.CurrentContext.Test;
return test.ClassName!.Substring(0, test.ClassName.LastIndexOf('.'));
}
public static string GetTestId()
{
return GetEnvVar("TESTID");
}
public static string GetRunId()
{
return GetEnvVar("RUNID");
}
private static string GetEnvVar(string name)
{
var v = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(v)) return $"EnvVar'{name}'NotSet";
return v;
}
private static string FormatArguments(TestContext.TestAdapter test)
{
if (test.Arguments == null || !test.Arguments.Any()) return "";
return $"[{string.Join(',', test.Arguments)}]";
}
private static string ReplaceInvalidCharacters(string name)
{
return name.Replace(":", "_");
}
private static string DetermineFolder(LogConfig config, DateTime start)
{
return Path.Join(
config.LogRoot,
$"{start.Year}-{Pad(start.Month)}",
Pad(start.Day));
}
private static string GetFixtureName(string name, DateTime start)
{
var className = GetRawFixtureName();
if (!string.IsNullOrEmpty(name)) className = name;
return $"{Pad(start.Hour)}-{Pad(start.Minute)}-{Pad(start.Second)}Z_{className.Replace('.', '-')}";
}
private static string Pad(int n)
{
return n.ToString().PadLeft(2, '0');
}
}
}
-61
View File
@@ -1,61 +0,0 @@
using Newtonsoft.Json;
namespace Logging
{
public class StatusLog
{
private readonly object fileLock = new object();
private readonly string fullName;
private readonly string fixtureName;
public StatusLog(LogConfig config, DateTime start, string name = "")
{
fullName = NameUtils.GetFixtureFullName(config, start, name) + "_STATUS.log";
fixtureName = NameUtils.GetRawFixtureName();
}
public void ConcludeTest(string resultStatus, string testDuration, string codexId)
{
Write(new StatusLogJson
{
@timestamp = DateTime.UtcNow.ToString("o"),
runid = NameUtils.GetRunId(),
status = resultStatus,
testid = NameUtils.GetTestId(),
codexid = codexId,
category = NameUtils.GetCategoryName(),
fixturename = fixtureName,
testname = NameUtils.GetTestMethodName(),
testduration = testDuration
});
}
private void Write(StatusLogJson json)
{
try
{
lock (fileLock)
{
File.AppendAllLines(fullName, new[] { JsonConvert.SerializeObject(json) });
}
}
catch (Exception ex)
{
Console.WriteLine("Unable to write to status log: " + ex);
}
}
}
public class StatusLogJson
{
public string @timestamp { get; set; } = string.Empty;
public string runid { get; set; } = string.Empty;
public string status { get; set; } = string.Empty;
public string testid { get; set; } = string.Empty;
public string codexid { get; set; } = string.Empty;
public string category { get; set; } = string.Empty;
public string fixturename { get; set; } = string.Empty;
public string testname { get; set; } = string.Empty;
public string testduration { get; set;} = string.Empty;
}
}
+20 -1
View File
@@ -10,7 +10,7 @@ namespace Logging
public TestLog(string folder, bool debug, string name = "")
: base(debug)
{
methodName = NameUtils.GetTestMethodName(name);
methodName = GetMethodName(name);
fullName = Path.Combine(folder, methodName);
Log($"*** Begin: {methodName}");
@@ -37,5 +37,24 @@ namespace Logging
{
return fullName;
}
private string GetMethodName(string name)
{
if (!string.IsNullOrEmpty(name)) return name;
var test = TestContext.CurrentContext.Test;
var args = FormatArguments(test);
return ReplaceInvalidCharacters($"{test.MethodName}{args}");
}
private static string FormatArguments(TestContext.TestAdapter test)
{
if (test.Arguments == null || !test.Arguments.Any()) return "";
return $"[{string.Join(',', test.Arguments)}]";
}
private static string ReplaceInvalidCharacters(string name)
{
return name.Replace(":", "_");
}
}
}
+4 -1
View File
@@ -1,5 +1,7 @@
using DistTestCore;
using NUnit.Framework;
using k8s;
using k8s.Models;
namespace TestsLong.BasicTests
{
@@ -23,7 +25,8 @@ namespace TestsLong.BasicTests
var testFile = GenerateTestFile(filesizeMb.MB());
var contentId = host.UploadFile(testFile);
var list = new List<Task<TestFile?>>();
//sleep for 1 minute
Thread.Sleep(1200000);
foreach (var node in group)
{
list.Add(Task.Run(() => { return node.DownloadContent(contentId); }));
+5 -7
View File
@@ -1,5 +1,6 @@
using DistTestCore;
using NUnit.Framework;
using Utils;
namespace Tests.BasicTests
{
@@ -43,7 +44,6 @@ namespace Tests.BasicTests
{
var sellerInitialBalance = 234.TestTokens();
var buyerInitialBalance = 1000.TestTokens();
var fileSize = 10.MB();
var seller = SetupCodexNode(s => s
.WithStorageQuota(11.GB())
@@ -56,27 +56,25 @@ namespace Tests.BasicTests
maxCollateral: 20.TestTokens(),
maxDuration: TimeSpan.FromMinutes(3));
var testFile = GenerateTestFile(fileSize);
var testFile = GenerateTestFile(10.MB());
var buyer = SetupCodexNode(s => s
.WithBootstrapNode(seller)
.EnableMarketplace(buyerInitialBalance));
buyer.Marketplace.AssertThatBalance(Is.EqualTo(buyerInitialBalance));
var contentId = buyer.UploadFile(testFile);
var purchaseContract = buyer.Marketplace.RequestStorage(contentId,
buyer.Marketplace.RequestStorage(contentId,
pricePerSlotPerSecond: 2.TestTokens(),
requiredCollateral: 10.TestTokens(),
minRequiredNumberOfNodes: 1,
proofProbability: 5,
duration: TimeSpan.FromMinutes(1));
purchaseContract.WaitForStorageContractStarted(fileSize);
Time.Sleep(TimeSpan.FromSeconds(10));
seller.Marketplace.AssertThatBalance(Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
purchaseContract.WaitForStorageContractFinished();
Time.Sleep(TimeSpan.FromMinutes(1));
seller.Marketplace.AssertThatBalance(Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
buyer.Marketplace.AssertThatBalance(Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
+49
View File
@@ -0,0 +1,49 @@
using DistTestCore;
using NUnit.Framework;
namespace Tests.ParallelTests
{
[TestFixture]
public class MixedTests : DistTest
{
[TestCase(1, 10)]
[UseLongTimeouts]
public void ParallelMixed(int numberOfNodes, int filesizeMb)
{
// initialize the nodes
var group = SetupCodexNodes(numberOfNodes);
var host = SetupCodexNode();
foreach (var node in group)
{
host.ConnectToPeer(node);
}
// Upload single file for the download nodes
var testfile = GenerateTestFile(filesizeMb.MB());
var contentId = host.UploadFile(testfile);
var testfiles = new List<TestFile>();
var contentIds = new List<Task<ContentId>>();
// Starts uploads for the upload nodes
for (int i = 0; i < group.Count(); i++)
{
testfiles.Add(GenerateTestFile(filesizeMb.MB()));
var n = i;
contentIds.Add(Task.Run(() => { return host.UploadFile(testfiles[n]); }));
}
// Starts downloads for the download nodes
var downloads = new List<Task<TestFile?>>();
for (int i = 0; i < group.Count(); i++)
{
var n = i;
downloads.Add(Task.Run(() => { return group[n].DownloadContent(contentId); }));
}
Task.WaitAll(downloads.ToArray());
for (int i = 0; i < group.Count(); i++)
{
testfiles[i].AssertIsEqual(downloads[i].Result);
}
}
}
}
+130
View File
@@ -0,0 +1,130 @@
using DistTestCore;
using KubernetesWorkflow;
using NUnit.Framework;
namespace Tests.MembershipChangeTests
{
[TestFixture]
public class MembershipChangeTests : DistTest
{
[Test]
public void SingleDownloadWhileAdding()
{
var filesize = 100.MB();
var group = SetupCodexNodes(1);
var toAdd = SetupCodexNodes(5);
var host = SetupCodexNodes(1)[0];
foreach (var node in group)
{
host.ConnectToPeer(node);
}
var testFile = GenerateTestFile(filesize);
var contentId = host.UploadFile(testFile);
var list = new List<Task>();
foreach (var node in toAdd)
{
list.Add(Task.Run(() => { host.ConnectToPeer(node); }));
}
var resFile = group[0].DownloadContent(contentId);
Task.WaitAll(list.ToArray());
testFile.AssertIsEqual(resFile);
}
[Test]
public void SingleUploadWhileAdding()
{
var filesize = 100.MB();
var group = SetupCodexNodes(1);
var toAdd = SetupCodexNodes(5);
var host = SetupCodexNodes(1)[0];
foreach (var node in group)
{
host.ConnectToPeer(node);
}
var testFile = GenerateTestFile(filesize);
var list = new List<Task>();
foreach (var node in toAdd)
{
list.Add(Task.Run(() => { host.ConnectToPeer(node); }));
}
var contentId = host.UploadFile(testFile);
Task.WaitAll(list.ToArray());
var resFile = group[0].DownloadContent(contentId);
testFile.AssertIsEqual(resFile);
}
[Test]
public void SingleDownloadMixedMembership()
{
var filesize = 100.MB();
var group = SetupCodexNodes(1);
var toAdd = SetupCodexNodes(5);
var toRemove = SetupCodexNodes(5);
var host = SetupCodexNodes(1)[0];
foreach (var node in group)
{
host.ConnectToPeer(node);
}
foreach (var node in toRemove)
{
host.ConnectToPeer(node);
}
var testFile = GenerateTestFile(filesize);
var contentId = host.UploadFile(testFile);
for (var i = 0; i < toAdd.Count(); i++)
{
Task.Run(() => { host.ConnectToPeer(toAdd[i]); });
Task.Run(() => { toRemove[i].BringOffline(); });
}
var resFile = group[0].DownloadContent(contentId);
testFile.AssertIsEqual(resFile);
}
[Test]
public void SingleUploadMixedMembership()
{
var filesize = 100.MB();
var group = SetupCodexNodes(1);
var toAdd = SetupCodexNodes(5);
var toRemove = SetupCodexNodes(5);
var host = SetupCodexNodes(1)[0];
foreach (var node in group)
{
host.ConnectToPeer(node);
}
foreach (var node in toRemove)
{
host.ConnectToPeer(node);
}
var testFile = GenerateTestFile(filesize);
var list = new List<Task>();
for (var i = 0; i < toAdd.Count(); i++)
{
Task.Run(() => { host.ConnectToPeer(toAdd[i]); });
Task.Run(() => { toRemove[i].BringOffline(); });
}
var contentId = host.UploadFile(testFile);
var resFile = group[0].DownloadContent(contentId);
testFile.AssertIsEqual(resFile);
}
}
}
@@ -0,0 +1,67 @@
using DistTestCore;
using KubernetesWorkflow;
using NUnit.Framework;
namespace Tests.MembershipChangeTests
{
[TestFixture]
public class DownloadMembershipChangeTests : DistTest
{
[TestCase(1, 100, 5, 0)]
[TestCase(1, 100, 0, 5)]
[TestCase(1, 100, 5, 5)]
[UseLongTimeouts]
public void DownloadMembershipChange(int numberOfNodes, int filesize, int numberOfNodesToAdd = 0, int numberOfNodesToRemove = 0)
{
// Setup 3 node groups, one which will be added during the procedure, one which will be dropped during the procedure, and the one being tested.
ICodexNodeGroup? toAdd = null;
ICodexNodeGroup? toRemove = null;
var group = SetupCodexNodes(numberOfNodes);
if (numberOfNodesToAdd != 0)
toAdd = SetupCodexNodes(numberOfNodesToAdd);
if (numberOfNodesToRemove != 0)
toRemove = SetupCodexNodes(numberOfNodesToRemove);
var host = SetupCodexNodes(1)[0];
// Connect the main and dropping nodes to the host
foreach (var node in group)
{
host.ConnectToPeer(node);
}
if (toRemove != null)
foreach (var node in toRemove)
host.ConnectToPeer(node);
// Upload a file to the host
var testFile = GenerateTestFile(filesize.MB());
var contentId = host.UploadFile(testFile);
var list = new List<Task<TestFile?>>();
// Start the download for each node
foreach (var node in group)
{
list.Add(Task.Run(() => { return node.DownloadContent(contentId); }));
}
// Start adding and dropping nodes during the download
// TODO: The log is put here for debug, but without it the members do not run async
for (var i = 0; (toAdd != null && i < toAdd.Count()) || (toRemove != null && i < toRemove.Count()); i++)
{
Log($"Iteration {i}");
if (toAdd != null && i < toAdd.Count())
Task.Run(() => { host.ConnectToPeer(toAdd[i]); });
if (toRemove != null && i < toRemove.Count())
Task.Run(() => { toRemove[i].BringOffline(); });
}
// Wait for the download to finish
Task.WaitAll(list.ToArray());
// Assert that the download was successful
foreach (var task in list)
{
testFile.AssertIsEqual(task.Result);
}
}
}
}
+83
View File
@@ -0,0 +1,83 @@
using DistTestCore;
using KubernetesWorkflow;
using NUnit.Framework;
namespace Tests.MembershipChangeTests
{
[TestFixture]
public class MixedMembershipChangeTests : DistTest
{
[TestCase(1, 5, 0, 0)]
[UseLongTimeouts]
public void MixedMembershipChange(int numberOfNodes, int filesize, int numberOfNodesToAdd, int numberOfNodesToRemove)
{
// Creating the node groups
ICodexNodeGroup? toAdd = null;
ICodexNodeGroup? toAddSecondary = null;
ICodexNodeGroup? toRemove = null;
var group = SetupCodexNodes(numberOfNodes);
if (numberOfNodesToAdd != 0) {
toAdd = SetupCodexNodes(numberOfNodesToAdd);
toAddSecondary = SetupCodexNodes(numberOfNodesToAdd);
}
if (numberOfNodesToRemove != 0)
toRemove = SetupCodexNodes(numberOfNodesToRemove);
var host = SetupCodexNodes(1)[0];
// Connect the main and dropping nodes to the host
foreach (var node in group)
{
host.ConnectToPeer(node);
}
if (toRemove != null)
foreach (var node in toRemove)
host.ConnectToPeer(node);
var testFile = GenerateTestFile(filesize.MB());
var contentId = host.UploadFile(testFile);
var testfiles = new List<TestFile>();
var contentIds = new List<Task<ContentId>>();
var downloads = new List<Task<TestFile?>>();
// Start adding and dropping nodes
for (var i = 0; (toAdd != null && i < toAdd.Count()) || (toRemove != null && i < toRemove.Count()); i++)
{
Log($"Iteration {i}");
if (toAdd != null && i < toAdd.Count())
Task.Run(() => { host.ConnectToPeer(toAdd[i]); });
if (toRemove != null && i < toRemove.Count())
Task.Run(() => { toRemove[i].BringOffline(); });
}
// Start the upload for each node in the main group
for (int i = 0; i < group.Count(); i++)
{
testfiles.Add(GenerateTestFile(filesize.MB()));
var n = i;
contentIds.Add(Task.Run(() => { return host.UploadFile(testfiles[n]); }));
}
// Start the download for each node in the main group
for (int i = 0; i < group.Count(); i++)
{
var n = i;
downloads.Add(Task.Run(() => { return group[n].DownloadContent(contentId); }));
}
// Wait for the download to finish
Task.WaitAll(downloads.ToArray());
// Wait for the upload to finish
Task.WaitAll(contentIds.ToArray());
// Assert that the files are intact
for (int i = 0; i < group.Count(); i++)
{
testfiles[i].AssertIsEqual(downloads[i].Result);
}
}
}
}
+78
View File
@@ -0,0 +1,78 @@
using DistTestCore;
using KubernetesWorkflow;
using NUnit.Framework;
namespace Tests.MembershipChangeTests
{
[TestFixture]
public class UploadMembershipChangeTests : DistTest
{
[TestCase(1, 100, 5, 0)]
[TestCase(1, 100, 0, 5)]
[TestCase(1, 100, 5, 5)]
[UseLongTimeouts]
public void UploadMembershipChange(int numberOfNodes, int filesize, int numberOfNodesToAdd = 0, int numberOfNodesToRemove = 0)
{
// Creating the node groups
ICodexNodeGroup? toAdd = null;
ICodexNodeGroup? toRemove = null;
var group = SetupCodexNodes(numberOfNodes);
if (numberOfNodesToAdd != 0)
toAdd = SetupCodexNodes(numberOfNodesToAdd);
if (numberOfNodesToRemove != 0)
toRemove = SetupCodexNodes(numberOfNodesToRemove);
var host = SetupCodexNodes(1)[0];
// Connect the main and dropping nodes to the host
foreach (var node in group)
{
host.ConnectToPeer(node);
}
if (toRemove != null)
foreach (var node in toRemove)
host.ConnectToPeer(node);
var testfiles = new List<TestFile>();
var contentIds = new List<Task<ContentId>>();
// Start adding and dropping nodes
// Start the upload for each node in the main group
for (int i = 0; i < group.Count(); i++)
{
testfiles.Add(GenerateTestFile(filesize.MB()));
var n = i;
contentIds.Add(Task.Run(() => { return host.UploadFile(testfiles[n]); }));
}
for (var i = 0; (toAdd != null && i < toAdd.Count()) || (toRemove != null && i < toRemove.Count()); i++)
{
Log($"Iteration {i}");
if (toAdd != null && i < toAdd.Count())
Task.Run(() => { host.ConnectToPeer(toAdd[i]); });
if (toRemove != null && i < toRemove.Count())
Task.Run(() => { toRemove[i].BringOffline(); });
}
// Wait for the upload to finish
Task.WaitAll(contentIds.ToArray());
// Download the files
var downloads = new List<Task<TestFile?>>();
for (int i = 0; i < group.Count(); i++)
{
var n = i;
downloads.Add(Task.Run(() => { return group[n].DownloadContent(contentIds[n].Result); }));
}
// Wait for the download to finish
Task.WaitAll(downloads.ToArray());
// Assert that the files are intact
for (int i = 0; i < group.Count(); i++)
{
testfiles[i].AssertIsEqual(downloads[i].Result);
}
}
}
}
-11
View File
@@ -1,11 +0,0 @@
services:
dist-test-run:
build:
context: ..
dockerfile: docker/Dockerfile
environment:
- CODEXDOCKERIMAGE=codexstorage/nim-codex:sha-14c5270
- BRANCH="feature/docker-image-testruns"
- KUBECONFIG=/opt/kubeconfig
- LOGPATH=/opt/logs
- RUNNERLOCATION=ExternalToCluster
-4
View File
@@ -28,10 +28,6 @@ spec:
value: ${BRANCH}
- name: SOURCE
value: ${SOURCE}
- name: RUNID
value: ${RUNID}
- name: TESTID
value: ${TESTID}
volumeMounts:
- name: kubeconfig
mountPath: /opt/kubeconfig.yaml