Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a6c5e95ee | ||
|
|
771a952f02 | ||
|
|
9fab7757b0 | ||
|
|
4234719e53 | ||
|
|
032506535f | ||
|
|
5675c1d77a | ||
|
|
29137263fb | ||
|
|
8d229f1720 | ||
|
|
9a7946c863 | ||
|
|
10ffd29257 | ||
|
|
933932b091 | ||
|
|
dbbd05ea97 | ||
|
|
dc4749aca6 | ||
|
|
ce7ad1ba70 | ||
|
|
71f45ca5ea | ||
|
|
754e74f6d8 | ||
|
|
344c004299 | ||
|
|
07b85bda5e | ||
|
|
8b3589a489 | ||
|
|
c762e30331 | ||
|
|
0807b16aa4 | ||
|
|
0f9e6b6b58 | ||
|
|
9de7e1d320 | ||
|
|
2dd5775296 | ||
|
|
f2517b2ade | ||
|
|
c744fd12c3 | ||
|
|
bbf66bb0f0 | ||
|
|
bfefba9633 | ||
|
|
d6d32bf8e6 | ||
|
|
07776dc6fb | ||
|
|
87d346ee20 | ||
|
|
fa1057ddfd | ||
|
|
009ce1e9f3 | ||
|
|
f50b8dfc83 | ||
|
|
78434048b7 | ||
|
|
fa7ba9149e | ||
|
|
6147c14c9b |
@@ -15,19 +15,19 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Branch
|
||||
description: Branch (master)
|
||||
required: false
|
||||
type: string
|
||||
source:
|
||||
description: Repository with tests
|
||||
description: Repository with tests (current)
|
||||
required: false
|
||||
type: string
|
||||
nameprefix:
|
||||
description: Runner job/pod name prefix
|
||||
description: Runner prefix (cs-codex-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
namespace:
|
||||
description: Kubernetes namespace for runner
|
||||
description: Runner namespace (cs-codex-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -56,6 +56,8 @@ 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
|
||||
@@ -69,5 +71,4 @@ jobs:
|
||||
|
||||
- name: Kubectl - Create Job
|
||||
run: |
|
||||
export RUNID=$(date +%Y%m%d-%H%M%S)
|
||||
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace CodexNetDeployer
|
||||
if (!setup.MetricsEnabled) return null;
|
||||
|
||||
Log("Starting metrics service...");
|
||||
var runningContainers = new RunningContainers(null!, null!, codexContainers.ToArray());
|
||||
var runningContainers = new[] { new RunningContainers(null!, null!, codexContainers.ToArray()) };
|
||||
return lifecycle.PrometheusStarter.CollectMetricsFor(runningContainers).Containers.Single();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ public class Program
|
||||
}
|
||||
|
||||
Console.WriteLine("Using images:" + nl +
|
||||
$"\tCodex image: '{CodexContainerRecipe.DockerImage}'" + nl +
|
||||
$"\tCodex Contracts image: '{CodexContractsContainerRecipe.DockerImage}'" + nl +
|
||||
$"\tPrometheus image: '{PrometheusContainerRecipe.DockerImage}'" + nl +
|
||||
$"\tGeth image: '{GethContainerRecipe.DockerImage}'" + 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);
|
||||
|
||||
if (!args.Any(a => a == "-y"))
|
||||
{
|
||||
|
||||
@@ -3,10 +3,11 @@ 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
|
||||
|
||||
--block-ttl=120 \
|
||||
-y
|
||||
|
||||
@@ -25,6 +25,9 @@ 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;
|
||||
@@ -57,9 +60,11 @@ namespace ContinuousTests
|
||||
private static void PrintHelp()
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine("CodexNetDownloader lets you download all container logs given a codex-deployment.json file." + nl);
|
||||
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 assumes you are running this tool from *inside* the Kubernetes cluster. " +
|
||||
|
||||
Console.WriteLine("ContinuousTests 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,14 @@ namespace ContinuousTests
|
||||
startupChecker.Check();
|
||||
|
||||
var taskFactory = new TaskFactory();
|
||||
var overviewLog = new FixtureLog(new LogConfig(config.LogPath, false), "Overview");
|
||||
var overviewLog = new FixtureLog(new LogConfig(config.LogPath, false), DateTime.UtcNow, "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)
|
||||
@@ -61,5 +63,18 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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), testName);
|
||||
fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), DateTime.UtcNow, testName);
|
||||
|
||||
nodes = CreateRandomNodes(handle.Test.RequiredNumberOfNodes);
|
||||
dataFolder = config.DataPath + "-" + Guid.NewGuid();
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace ContinuousTests
|
||||
|
||||
public void Check()
|
||||
{
|
||||
var log = new FixtureLog(new LogConfig(config.LogPath, false), "StartupChecks");
|
||||
var log = new FixtureLog(new LogConfig(config.LogPath, false), DateTime.UtcNow, "StartupChecks");
|
||||
log.Log("Starting continuous test run...");
|
||||
log.Log("Checking configuration...");
|
||||
PreflightCheck(config);
|
||||
|
||||
@@ -60,40 +60,5 @@
|
||||
// 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.");
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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,4 +1,6 @@
|
||||
dotnet run \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--codex-deployment=codex-deployment.json \
|
||||
--stop=1
|
||||
--keep=1 \
|
||||
--stop=1 \
|
||||
--dl-logs=1
|
||||
@@ -1,5 +1,6 @@
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore.Codex
|
||||
@@ -60,6 +61,16 @@ 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
|
||||
|
||||
@@ -5,12 +5,8 @@ namespace DistTestCore.Codex
|
||||
{
|
||||
public class CodexContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
#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
|
||||
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-7efa917";
|
||||
|
||||
public const string MetricsPortTag = "metrics_port";
|
||||
public const string DiscoveryPortTag = "discovery-port";
|
||||
|
||||
@@ -18,15 +14,11 @@ namespace DistTestCore.Codex
|
||||
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
|
||||
public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
|
||||
|
||||
public static string DockerImageOverride = string.Empty;
|
||||
public override string Image { get; }
|
||||
|
||||
protected override string Image
|
||||
public CodexContainerRecipe()
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(DockerImageOverride)) return DockerImageOverride;
|
||||
return DockerImage;
|
||||
}
|
||||
Image = GetDockerImage();
|
||||
}
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
@@ -92,5 +84,12 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ 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();
|
||||
Nodes = containers.Containers().Select(c => CreateOnlineCodexNode(c, codexNodeFactory)).ToArray();
|
||||
Version = new CodexDebugVersionResponse();
|
||||
}
|
||||
|
||||
public IOnlineCodexNode this[int index]
|
||||
@@ -44,8 +45,9 @@ 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()
|
||||
{
|
||||
@@ -64,14 +66,17 @@ namespace DistTestCore
|
||||
|
||||
public void EnsureOnline()
|
||||
{
|
||||
foreach (var node in Nodes)
|
||||
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))
|
||||
{
|
||||
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);
|
||||
throw new Exception("Inconsistent version information received from one or more Codex nodes: " +
|
||||
string.Join(",", versionResponses.Select(v => v.ToString())));
|
||||
}
|
||||
|
||||
Version = first;
|
||||
}
|
||||
|
||||
private OnlineCodexNode CreateOnlineCodexNode(RunningContainer c, ICodexNodeFactory factory)
|
||||
|
||||
@@ -22,6 +22,7 @@ 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);
|
||||
@@ -29,9 +30,16 @@ namespace DistTestCore
|
||||
var codexNodeFactory = new CodexNodeFactory(lifecycle, metricAccessFactory, gethStartResult.MarketplaceAccessFactory);
|
||||
|
||||
var group = CreateCodexGroup(codexSetup, containers, codexNodeFactory);
|
||||
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()}");
|
||||
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);
|
||||
LogSeparator();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -39,7 +47,7 @@ namespace DistTestCore
|
||||
{
|
||||
LogStart($"Stopping {group.Describe()}...");
|
||||
var workflow = CreateWorkflow();
|
||||
workflow.Stop(group.Containers);
|
||||
foreach (var c in group.Containers) workflow.Stop(c);
|
||||
RunningGroups.Remove(group);
|
||||
LogEnd("Stopped.");
|
||||
}
|
||||
@@ -58,7 +66,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();
|
||||
|
||||
@@ -75,20 +83,42 @@ namespace DistTestCore
|
||||
return startupConfig;
|
||||
}
|
||||
|
||||
private RunningContainers StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, Location location)
|
||||
private RunningContainers[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, Location location)
|
||||
{
|
||||
var workflow = CreateWorkflow();
|
||||
return workflow.Start(numberOfNodes, location, new CodexContainerRecipe(), startupConfig);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
|
||||
|
||||
try
|
||||
{
|
||||
Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
CodexNodesNotOnline(runningContainers);
|
||||
throw;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -16,6 +16,7 @@ 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>();
|
||||
|
||||
@@ -24,7 +25,10 @@ namespace DistTestCore
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
|
||||
|
||||
fixtureLog = new FixtureLog(configuration.GetLogConfig());
|
||||
var logConfig = configuration.GetLogConfig();
|
||||
var startTime = DateTime.UtcNow;
|
||||
fixtureLog = new FixtureLog(logConfig, startTime);
|
||||
statusLog = new StatusLog(logConfig, startTime);
|
||||
|
||||
PeerConnectionTestHelpers = new PeerConnectionTestHelpers(this);
|
||||
PeerDownloadTestHelpers = new PeerDownloadTestHelpers(this);
|
||||
@@ -36,6 +40,12 @@ 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
|
||||
@@ -54,9 +64,6 @@ 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]
|
||||
@@ -183,6 +190,7 @@ namespace DistTestCore
|
||||
private void CreateNewTestLifecycle()
|
||||
{
|
||||
var testName = GetCurrentTestName();
|
||||
fixtureLog.WriteLogTag();
|
||||
Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () =>
|
||||
{
|
||||
lock (lifecycleLock)
|
||||
@@ -195,7 +203,10 @@ namespace DistTestCore
|
||||
private void DisposeTestLifecycle()
|
||||
{
|
||||
var lifecycle = Get();
|
||||
fixtureLog.Log($"{GetCurrentTestName()} = {GetTestResult()} ({lifecycle.GetTestDuration()})");
|
||||
var testResult = GetTestResult();
|
||||
var testDuration = lifecycle.GetTestDuration();
|
||||
fixtureLog.Log($"{GetCurrentTestName()} = {testResult} ({testDuration})");
|
||||
statusLog.ConcludeTest(testResult, testDuration, GetCodexId(lifecycle));
|
||||
Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", () =>
|
||||
{
|
||||
lifecycle.Log.EndTest();
|
||||
@@ -205,6 +216,14 @@ 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();
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,257 +1,66 @@
|
||||
using DistTestCore.Codex;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
using static DistTestCore.Helpers.FullConnectivityHelper;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public class PeerConnectionTestHelpers
|
||||
public class PeerConnectionTestHelpers : IFullConnectivityImplementation
|
||||
{
|
||||
private readonly Random random = new Random();
|
||||
private readonly DistTest test;
|
||||
private readonly FullConnectivityHelper helper;
|
||||
|
||||
public PeerConnectionTestHelpers(DistTest test)
|
||||
{
|
||||
this.test = test;
|
||||
helper = new FullConnectivityHelper(test, this);
|
||||
}
|
||||
|
||||
public void AssertFullyConnected(IEnumerable<IOnlineCodexNode> nodes)
|
||||
{
|
||||
var n = nodes.ToArray();
|
||||
|
||||
AssertFullyConnected(n);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Time.Sleep(TimeSpan.FromSeconds(30));
|
||||
AssertFullyConnected(n);
|
||||
}
|
||||
helper.AssertFullyConnected(nodes);
|
||||
}
|
||||
|
||||
private void AssertFullyConnected(IOnlineCodexNode[] nodes)
|
||||
public string Description()
|
||||
{
|
||||
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()))}");
|
||||
}
|
||||
return "Peer Discovery";
|
||||
}
|
||||
|
||||
private static void RetryWhilePairs(List<Pair> pairs, Action action)
|
||||
public string ValidateEntry(Entry entry, Entry[] allEntries)
|
||||
{
|
||||
var timeout = DateTime.UtcNow + TimeSpan.FromSeconds(30);
|
||||
while (pairs.Any() && timeout > DateTime.UtcNow)
|
||||
var result = string.Empty;
|
||||
foreach (var peer in entry.Response.table.nodes)
|
||||
{
|
||||
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)
|
||||
var expected = GetExpectedDiscoveryEndpoint(allEntries, peer);
|
||||
if (expected != peer.address)
|
||||
{
|
||||
test.Log(pair.GetMessage());
|
||||
pairs.Remove(pair);
|
||||
result += $"Node:{entry.Node.GetName()} has incorrect peer table entry. Was: '{peer.address}', expected: '{expected}'. ";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Entry[] CreateEntries(IOnlineCodexNode[] nodes)
|
||||
public PeerConnectionState Check(Entry from, Entry to)
|
||||
{
|
||||
var entries = nodes.Select(n => new Entry(n)).ToArray();
|
||||
var incorrectDiscoveryEndpoints = entries.SelectMany(e => e.GetInCorrectDiscoveryEndpoints(entries)).ToArray();
|
||||
var peerId = to.Response.id;
|
||||
|
||||
if (incorrectDiscoveryEndpoints.Any())
|
||||
var response = from.Node.GetDebugPeer(peerId);
|
||||
if (!response.IsPeerFound)
|
||||
{
|
||||
Assert.Fail("Some nodes contain peer records with incorrect discovery ip/port information: " +
|
||||
string.Join(Environment.NewLine, incorrectDiscoveryEndpoints));
|
||||
return PeerConnectionState.NoConnection;
|
||||
}
|
||||
|
||||
return entries;
|
||||
if (!string.IsNullOrEmpty(response.peerId) && response.addresses.Any())
|
||||
{
|
||||
return PeerConnectionState.Connection;
|
||||
}
|
||||
return PeerConnectionState.Unknown;
|
||||
}
|
||||
|
||||
private static List<Pair> CreatePairs(Entry[] entries)
|
||||
private static string GetExpectedDiscoveryEndpoint(Entry[] allEntries, CodexDebugTableNodeResponse node)
|
||||
{
|
||||
return CreatePairsIterator(entries).ToList();
|
||||
}
|
||||
var peer = allEntries.SingleOrDefault(e => e.Response.table.localNode.peerId == node.peerId);
|
||||
if (peer == null) return $"peerId: {node.peerId} is not known.";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,63 @@
|
||||
using DistTestCore.Codex;
|
||||
using NUnit.Framework;
|
||||
using static DistTestCore.Helpers.FullConnectivityHelper;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public class PeerDownloadTestHelpers
|
||||
public class PeerDownloadTestHelpers : IFullConnectivityImplementation
|
||||
{
|
||||
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)
|
||||
{
|
||||
test.Log($"Asserting full download interconnectivity for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
|
||||
var start = DateTime.UtcNow;
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
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);
|
||||
this.testFileSize = testFileSize;
|
||||
helper.AssertFullyConnected(nodes);
|
||||
}
|
||||
|
||||
private void AssertTimePerMB(TimeSpan timeTaken, int numberOfNodes, ByteSize size)
|
||||
public string Description()
|
||||
{
|
||||
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.");
|
||||
return "Download Connectivity";
|
||||
}
|
||||
|
||||
private void PerformTest(IOnlineCodexNode uploader, IOnlineCodexNode[] downloaders, ByteSize testFileSize)
|
||||
public string ValidateEntry(Entry entry, Entry[] allEntries)
|
||||
{
|
||||
// Generate 1 test file per downloader.
|
||||
var files = downloaders.Select(d => GenerateTestFile(uploader, d, testFileSize)).ToArray();
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Upload all the test files to the uploader.
|
||||
var contentIds = files.Select(uploader.UploadFile).ToArray();
|
||||
public PeerConnectionState Check(Entry from, Entry to)
|
||||
{
|
||||
var expectedFile = GenerateTestFile(from.Node, to.Node);
|
||||
|
||||
// Each downloader should retrieve its own test file.
|
||||
for (var i = 0; i < downloaders.Length; i++)
|
||||
var contentId = from.Node.UploadFile(expectedFile);
|
||||
|
||||
try
|
||||
{
|
||||
var expectedFile = files[i];
|
||||
var downloadedFile = downloaders[i].DownloadContent(contentIds[i], $"{expectedFile.Label}DOWNLOADED");
|
||||
|
||||
var downloadedFile = to.Node.DownloadContent(contentId, 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, ByteSize testFileSize)
|
||||
private TestFile GenerateTestFile(IOnlineCodexNode uploader, IOnlineCodexNode downloader)
|
||||
{
|
||||
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
|
||||
{
|
||||
#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 const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
|
||||
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
|
||||
|
||||
protected override string Image => DockerImage;
|
||||
public override string Image { get; }
|
||||
|
||||
public CodexContractsContainerRecipe()
|
||||
{
|
||||
Image = "codexstorage/dist-tests-codex-contracts-eth:sha-b4e4897";
|
||||
}
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
|
||||
@@ -4,19 +4,18 @@ namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class GethContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
#if Arm64
|
||||
public const string DockerImage = "emizzle/geth-confenv:latest";
|
||||
#else
|
||||
public const string DockerImage = "thatbenbierens/geth-confenv:onethousand";
|
||||
#endif
|
||||
private const string defaultArgs = "--ipcdisable --syncmode full";
|
||||
|
||||
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";
|
||||
|
||||
protected override string Image => DockerImage;
|
||||
public override string Image { get; }
|
||||
|
||||
public GethContainerRecipe()
|
||||
{
|
||||
Image = "codexstorage/dist-tests-geth:sha-b788a2d";
|
||||
}
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Helpers;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Constraints;
|
||||
using System.Numerics;
|
||||
@@ -10,7 +12,7 @@ namespace DistTestCore.Marketplace
|
||||
public interface IMarketplaceAccess
|
||||
{
|
||||
string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan maxDuration);
|
||||
string RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
|
||||
StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
|
||||
void AssertThatBalance(IResolveConstraint constraint, string message = "");
|
||||
TestToken GetBalance();
|
||||
}
|
||||
@@ -30,7 +32,7 @@ namespace DistTestCore.Marketplace
|
||||
this.codexAccess = codexAccess;
|
||||
}
|
||||
|
||||
public string RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
{
|
||||
var request = new CodexSalesRequestStorageRequest
|
||||
{
|
||||
@@ -57,9 +59,9 @@ namespace DistTestCore.Marketplace
|
||||
throw new InvalidOperationException(response);
|
||||
}
|
||||
|
||||
Log($"Storage requested successfully. PurchaseId: {response}");
|
||||
Log($"Storage requested successfully. PurchaseId: '{response}'.");
|
||||
|
||||
return response;
|
||||
return new StoragePurchaseContract(lifecycle.Log, codexAccess, response, duration);
|
||||
}
|
||||
|
||||
public string MakeStorageAvailable(ByteSize totalSpace, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration)
|
||||
@@ -121,10 +123,10 @@ namespace DistTestCore.Marketplace
|
||||
|
||||
public class MarketplaceUnavailable : IMarketplaceAccess
|
||||
{
|
||||
public string RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
{
|
||||
Unavailable();
|
||||
return string.Empty;
|
||||
return null!;
|
||||
}
|
||||
|
||||
public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration)
|
||||
@@ -150,4 +152,92 @@ 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,9 +4,12 @@ namespace DistTestCore.Metrics
|
||||
{
|
||||
public class PrometheusContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public const string DockerImage = "thatbenbierens/prometheus-envconf:latest";
|
||||
public override string Image { get; }
|
||||
|
||||
protected override string Image => DockerImage;
|
||||
public PrometheusContainerRecipe()
|
||||
{
|
||||
Image = "codexstorage/dist-tests-prometheus:sha-f97d7fd";
|
||||
}
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace DistTestCore
|
||||
IDownloadedLog DownloadLog();
|
||||
IMetricsAccess Metrics { get; }
|
||||
IMarketplaceAccess Marketplace { get; }
|
||||
CodexDebugVersionResponse Version { get; }
|
||||
ICodexSetup BringOffline();
|
||||
}
|
||||
|
||||
@@ -34,12 +35,14 @@ 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()
|
||||
{
|
||||
@@ -72,9 +75,6 @@ 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,19 +114,30 @@ 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.Group.Containers.RunningPod.PodInfo.Ip);
|
||||
return multiAddress.Replace("0.0.0.0", peer.CodexAccess.Container.Pod.PodInfo.Ip);
|
||||
}
|
||||
|
||||
private void DownloadToFile(string contentId, TestFile 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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DistTestCore.Logs;
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Logs;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
using Utils;
|
||||
@@ -7,7 +8,7 @@ namespace DistTestCore
|
||||
{
|
||||
public class TestLifecycle
|
||||
{
|
||||
private DateTime testStart = DateTime.MinValue;
|
||||
private readonly DateTime testStart;
|
||||
|
||||
public TestLifecycle(BaseLog log, Configuration configuration, ITimeSet timeSet)
|
||||
: this(log, configuration, timeSet, new WorkflowCreator(log, configuration.GetK8sConfiguration(timeSet)))
|
||||
@@ -25,6 +26,9 @@ namespace DistTestCore
|
||||
PrometheusStarter = new PrometheusStarter(this, workflowCreator);
|
||||
GethStarter = new GethStarter(this, workflowCreator);
|
||||
testStart = DateTime.UtcNow;
|
||||
CodexVersion = null;
|
||||
|
||||
Log.WriteLogTag();
|
||||
}
|
||||
|
||||
public BaseLog Log { get; }
|
||||
@@ -34,6 +38,7 @@ namespace DistTestCore
|
||||
public CodexStarter CodexStarter { get; }
|
||||
public PrometheusStarter PrometheusStarter { get; }
|
||||
public GethStarter GethStarter { get; }
|
||||
public CodexDebugVersionResponse? CodexVersion { get; private set; }
|
||||
|
||||
public void DeleteAllResources()
|
||||
{
|
||||
@@ -58,5 +63,10 @@ namespace DistTestCore
|
||||
var testDuration = DateTime.UtcNow - testStart;
|
||||
return Time.FormatDuration(testDuration);
|
||||
}
|
||||
|
||||
public void SetCodexVersion(CodexDebugVersionResponse version)
|
||||
{
|
||||
if (CodexVersion == null) CodexVersion = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../../../CodexTestLogs"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
return recipe;
|
||||
}
|
||||
|
||||
protected abstract string Image { get; }
|
||||
public abstract string Image { get; }
|
||||
protected int ContainerNumber { get; private set; } = 0;
|
||||
protected int Index { get; private set; } = 0;
|
||||
protected abstract void Initialize(StartupConfig config);
|
||||
|
||||
@@ -40,4 +40,17 @@ 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,14 @@ 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)
|
||||
|
||||
+3
-32
@@ -1,20 +1,14 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Logging
|
||||
namespace Logging
|
||||
{
|
||||
public class FixtureLog : BaseLog
|
||||
{
|
||||
private readonly DateTime start;
|
||||
private readonly string fullName;
|
||||
private readonly LogConfig config;
|
||||
|
||||
public FixtureLog(LogConfig config, string name = "")
|
||||
public FixtureLog(LogConfig config, DateTime start, string name = "")
|
||||
: base(config.DebugEnabled)
|
||||
{
|
||||
start = DateTime.UtcNow;
|
||||
var folder = DetermineFolder(config);
|
||||
var fixtureName = GetFixtureName(name);
|
||||
fullName = Path.Combine(folder, fixtureName);
|
||||
fullName = NameUtils.GetFixtureFullName(config, start, name);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@@ -32,28 +26,5 @@ namespace Logging
|
||||
{
|
||||
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
@@ -49,7 +49,7 @@
|
||||
|
||||
private static string GetTimestamp()
|
||||
{
|
||||
return $"[{DateTime.UtcNow.ToString("u")}]";
|
||||
return $"[{DateTime.UtcNow.ToString("o")}]";
|
||||
}
|
||||
|
||||
private void EnsurePathExists(string filename)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+1
-20
@@ -10,7 +10,7 @@ namespace Logging
|
||||
public TestLog(string folder, bool debug, string name = "")
|
||||
: base(debug)
|
||||
{
|
||||
methodName = GetMethodName(name);
|
||||
methodName = NameUtils.GetTestMethodName(name);
|
||||
fullName = Path.Combine(folder, methodName);
|
||||
|
||||
Log($"*** Begin: {methodName}");
|
||||
@@ -37,24 +37,5 @@ 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(":", "_");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using DistTestCore;
|
||||
using NUnit.Framework;
|
||||
using k8s;
|
||||
using k8s.Models;
|
||||
|
||||
namespace TestsLong.BasicTests
|
||||
{
|
||||
@@ -25,8 +23,7 @@ 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); }));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using DistTestCore;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
namespace Tests.BasicTests
|
||||
{
|
||||
@@ -44,6 +43,7 @@ namespace Tests.BasicTests
|
||||
{
|
||||
var sellerInitialBalance = 234.TestTokens();
|
||||
var buyerInitialBalance = 1000.TestTokens();
|
||||
var fileSize = 10.MB();
|
||||
|
||||
var seller = SetupCodexNode(s => s
|
||||
.WithStorageQuota(11.GB())
|
||||
@@ -56,25 +56,27 @@ namespace Tests.BasicTests
|
||||
maxCollateral: 20.TestTokens(),
|
||||
maxDuration: TimeSpan.FromMinutes(3));
|
||||
|
||||
var testFile = GenerateTestFile(10.MB());
|
||||
var testFile = GenerateTestFile(fileSize);
|
||||
|
||||
var buyer = SetupCodexNode(s => s
|
||||
.WithBootstrapNode(seller)
|
||||
.EnableMarketplace(buyerInitialBalance));
|
||||
|
||||
buyer.Marketplace.AssertThatBalance(Is.EqualTo(buyerInitialBalance));
|
||||
|
||||
var contentId = buyer.UploadFile(testFile);
|
||||
buyer.Marketplace.RequestStorage(contentId,
|
||||
var purchaseContract = buyer.Marketplace.RequestStorage(contentId,
|
||||
pricePerSlotPerSecond: 2.TestTokens(),
|
||||
requiredCollateral: 10.TestTokens(),
|
||||
minRequiredNumberOfNodes: 1,
|
||||
proofProbability: 5,
|
||||
duration: TimeSpan.FromMinutes(1));
|
||||
|
||||
Time.Sleep(TimeSpan.FromSeconds(10));
|
||||
purchaseContract.WaitForStorageContractStarted(fileSize);
|
||||
|
||||
seller.Marketplace.AssertThatBalance(Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
|
||||
|
||||
Time.Sleep(TimeSpan.FromMinutes(1));
|
||||
purchaseContract.WaitForStorageContractFinished();
|
||||
|
||||
seller.Marketplace.AssertThatBalance(Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
|
||||
buyer.Marketplace.AssertThatBalance(Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -28,6 +28,10 @@ spec:
|
||||
value: ${BRANCH}
|
||||
- name: SOURCE
|
||||
value: ${SOURCE}
|
||||
- name: RUNID
|
||||
value: ${RUNID}
|
||||
- name: TESTID
|
||||
value: ${TESTID}
|
||||
volumeMounts:
|
||||
- name: kubeconfig
|
||||
mountPath: /opt/kubeconfig.yaml
|
||||
|
||||
Reference in New Issue
Block a user