2
0
mirror of synced 2025-02-21 12:38:20 +00:00

Merge branch 'master' into feature/tests

This commit is contained in:
Corbo12 2023-06-07 14:14:26 +02:00
commit 21292456aa
62 changed files with 1336 additions and 500 deletions

73
.github/workflows/dist-tests.yaml vendored Normal file
View File

@ -0,0 +1,73 @@
name: Dist Tests
on:
# push:
# branches:
# - master
# tags:
# - 'v*.*.*'
# paths-ignore:
# - '**/*.md'
# - '.gitignore'
# - 'docker/**'
# - '!docker/job.yaml'
workflow_dispatch:
inputs:
branch:
description: Branch
required: false
type: string
source:
description: Repository with tests
required: false
type: string
nameprefix:
description: Runner job/pod name prefix
required: false
type: string
namespace:
description: Kubernetes namespace for runner
required: false
type: string
env:
BRANCH: ${{ github.ref_name }}
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
NAMEPREFIX: cs-codex-dist-tests
NAMESPACE: cs-codex-dist-tests
JOB_MANIFEST: docker/job.yaml
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.26.1
jobs:
run_tests:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Variables
run: |
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
[[ -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"
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
uses: azure/setup-kubectl@v3
with:
version: ${{ env.KUBE_VERSION }}
- name: Kubectl - Kubeconfig
run: |
mkdir -p "${HOME}"/.kube
echo "${{ env.KUBE_CONFIG }}" | base64 -d > "${HOME}"/.kube/config
- name: Kubectl - Create Job
run: |
export RUNID=$(date +%Y%m%d-%H%M%S)
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -

View File

@ -1,9 +1,9 @@
namespace DistTestCore using NUnit.Framework;
namespace DistTestCore
{ {
public class AutoBootstrapDistTest : DistTest public class AutoBootstrapDistTest : DistTest
{ {
private IOnlineCodexNode? bootstrapNode;
public override IOnlineCodexNode SetupCodexBootstrapNode(Action<ICodexSetup> setup) public override IOnlineCodexNode SetupCodexBootstrapNode(Action<ICodexSetup> setup)
{ {
throw new Exception("AutoBootstrapDistTest creates and attaches a single boostrap node for you. " + throw new Exception("AutoBootstrapDistTest creates and attaches a single boostrap node for you. " +
@ -12,19 +12,18 @@
public override ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup) public override ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
{ {
var codexSetup = new CodexSetup(numberOfNodes); var codexSetup = CreateCodexSetup(numberOfNodes);
setup(codexSetup); setup(codexSetup);
codexSetup.WithBootstrapNode(EnsureBootstapNode()); codexSetup.WithBootstrapNode(BootstrapNode);
return BringOnline(codexSetup); return BringOnline(codexSetup);
} }
private IOnlineCodexNode EnsureBootstapNode() [SetUp]
public void SetUpBootstrapNode()
{ {
if (bootstrapNode == null) BootstrapNode = BringOnline(CreateCodexSetup(1))[0];
{
bootstrapNode = base.SetupCodexBootstrapNode(s => { });
}
return bootstrapNode;
} }
protected IOnlineCodexNode BootstrapNode { get; private set; } = null!;
} }
} }

View File

@ -32,6 +32,11 @@ namespace DistTestCore
lifecycle.Log.Log($"{GetClassName()} {msg}"); lifecycle.Log.Log($"{GetClassName()} {msg}");
} }
protected void Debug(string msg)
{
lifecycle.Log.Debug($"{GetClassName()} {msg}", 1);
}
private string GetClassName() private string GetClassName()
{ {
return $"({GetType().Name})"; return $"({GetType().Name})";

View File

@ -1,17 +1,14 @@
using KubernetesWorkflow; using KubernetesWorkflow;
using Logging;
namespace DistTestCore.Codex namespace DistTestCore.Codex
{ {
public class CodexAccess public class CodexAccess
{ {
private readonly BaseLog log; private readonly TestLifecycle lifecycle;
private readonly ITimeSet timeSet;
public CodexAccess(BaseLog log, ITimeSet timeSet, RunningContainer runningContainer) public CodexAccess(TestLifecycle lifecycle, RunningContainer runningContainer)
{ {
this.log = log; this.lifecycle = lifecycle;
this.timeSet = timeSet;
Container = runningContainer; Container = runningContainer;
} }
@ -19,7 +16,30 @@ namespace DistTestCore.Codex
public CodexDebugResponse GetDebugInfo() public CodexDebugResponse GetDebugInfo()
{ {
return Http().HttpGetJson<CodexDebugResponse>("debug/info"); return Http(TimeSpan.FromSeconds(2)).HttpGetJson<CodexDebugResponse>("debug/info");
}
public CodexDebugPeerResponse GetDebugPeer(string peerId)
{
return GetDebugPeer(peerId, TimeSpan.FromSeconds(2));
}
public CodexDebugPeerResponse GetDebugPeer(string peerId, TimeSpan timeout)
{
var http = Http(timeout);
var str = http.HttpGetString($"debug/peer/{peerId}");
if (str.ToLowerInvariant() == "unable to find peer!")
{
return new CodexDebugPeerResponse
{
IsPeerFound = false
};
}
var result = http.TryJsonDeserialize<CodexDebugPeerResponse>(str);
result.IsPeerFound = true;
return result;
} }
public string UploadFile(FileStream fileStream) public string UploadFile(FileStream fileStream)
@ -42,6 +62,11 @@ namespace DistTestCore.Codex
return Http().HttpPostJson($"storage/request/{contentId}", request); return Http().HttpPostJson($"storage/request/{contentId}", request);
} }
public string ConnectToPeer(string peerId, string peerMultiAddress)
{
return Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
}
public void EnsureOnline() public void EnsureOnline()
{ {
try try
@ -51,25 +76,20 @@ namespace DistTestCore.Codex
var nodePeerId = debugInfo.id; var nodePeerId = debugInfo.id;
var nodeName = Container.Name; var nodeName = Container.Name;
log.AddStringReplace(nodePeerId, $"___{nodeName}___"); lifecycle.Log.AddStringReplace(nodePeerId, nodeName);
lifecycle.Log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
} }
catch (Exception e) catch (Exception e)
{ {
log.Error($"Failed to start codex node: {e}. Test infra failure."); lifecycle.Log.Error($"Failed to start codex node: {e}. Test infra failure.");
throw new InvalidOperationException($"Failed to start codex node. Test infra failure.", e); throw new InvalidOperationException($"Failed to start codex node. Test infra failure.", e);
} }
} }
private Http Http() private Http Http(TimeSpan? timeoutOverride = null)
{ {
var ip = Container.Pod.Cluster.IP; var address = lifecycle.Configuration.GetAddress(Container);
var port = Container.ServicePorts[0].Number; return new Http(lifecycle.Log, lifecycle.TimeSet, address, baseUrl: "/api/codex/v1", timeoutOverride);
return new Http(log, timeSet, ip, port, baseUrl: "/api/codex/v1");
}
public string ConnectToPeer(string peerId, string peerMultiAddress)
{
return Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
} }
} }
@ -82,6 +102,22 @@ namespace DistTestCore.Codex
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>(); public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>(); public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
public CodexDebugVersionResponse codex { get; set; } = new(); public CodexDebugVersionResponse codex { get; set; } = new();
public CodexDebugTableResponse table { get; set; } = new();
}
public class CodexDebugTableResponse
{
public CodexDebugTableNodeResponse localNode { get; set; } = new();
public CodexDebugTableNodeResponse[] nodes { get; set; } = Array.Empty<CodexDebugTableNodeResponse>();
}
public class CodexDebugTableNodeResponse
{
public string nodeId { get; set; } = string.Empty;
public string peerId { get; set; } = string.Empty;
public string record { get; set; } = string.Empty;
public string address { get; set; } = string.Empty;
public bool seen { get; set; }
} }
public class EnginePeerResponse public class EnginePeerResponse
@ -110,6 +146,20 @@ namespace DistTestCore.Codex
public string revision { get; set; } = string.Empty; public string revision { get; set; } = string.Empty;
} }
public class CodexDebugPeerResponse
{
public bool IsPeerFound { get; set; }
public string peerId { get; set; } = string.Empty;
public long seqNo { get; set; }
public CodexDebugPeerAddressResponse[] addresses { get; set; } = Array.Empty<CodexDebugPeerAddressResponse>();
}
public class CodexDebugPeerAddressResponse
{
public string address { get; set; } = string.Empty;
}
public class CodexSalesAvailabilityRequest public class CodexSalesAvailabilityRequest
{ {
public string size { get; set; } = string.Empty; public string size { get; set; } = string.Empty;

View File

@ -1,5 +1,4 @@
using System.Runtime.InteropServices; using DistTestCore.Marketplace;
using DistTestCore.Marketplace;
using KubernetesWorkflow; using KubernetesWorkflow;
namespace DistTestCore.Codex namespace DistTestCore.Codex
@ -7,12 +6,13 @@ namespace DistTestCore.Codex
public class CodexContainerRecipe : ContainerRecipeFactory public class CodexContainerRecipe : ContainerRecipeFactory
{ {
#if Arm64 #if Arm64
public const string DockerImage = "emizzle/nim-codex-arm64:sha-c7af585"; public const string DockerImage = "codexstorage/nim-codex:sha-7b88ea0";
#else #else
//public const string DockerImage = "thatbenbierens/nim-codex:sha-9716635"; //public const string DockerImage = "codexstorage/nim-codex:sha-7b88ea0";
public const string DockerImage = "thatbenbierens/codexlocal:latest"; public const string DockerImage = "codexstorage/nim-codex:sha-7b88ea0";
#endif #endif
public const string MetricsPortTag = "metrics_port"; public const string MetricsPortTag = "metrics_port";
public const string DiscoveryPortTag = "discovery-port";
protected override string Image => DockerImage; protected override string Image => DockerImage;
@ -22,7 +22,8 @@ namespace DistTestCore.Codex
AddExposedPortAndVar("API_PORT"); AddExposedPortAndVar("API_PORT");
AddEnvVar("DATA_DIR", $"datadir{ContainerNumber}"); AddEnvVar("DATA_DIR", $"datadir{ContainerNumber}");
AddInternalPortAndVar("DISC_PORT"); AddInternalPortAndVar("DISC_PORT", DiscoveryPortTag);
AddEnvVar("LOG_LEVEL", config.LogLevel.ToString()!.ToUpperInvariant());
var listenPort = AddInternalPort(); var listenPort = AddInternalPort();
AddEnvVar("LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}"); AddEnvVar("LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}");
@ -31,11 +32,6 @@ namespace DistTestCore.Codex
{ {
AddEnvVar("BOOTSTRAP_SPR", config.BootstrapSpr); AddEnvVar("BOOTSTRAP_SPR", config.BootstrapSpr);
} }
if (config.LogLevel != null)
{
AddEnvVar("LOG_LEVEL", config.LogLevel.ToString()!.ToUpperInvariant());
}
if (config.StorageQuota != null) if (config.StorageQuota != null)
{ {
AddEnvVar("STORAGE_QUOTA", config.StorageQuota.SizeInBytes.ToString()!); AddEnvVar("STORAGE_QUOTA", config.StorageQuota.SizeInBytes.ToString()!);
@ -53,12 +49,13 @@ namespace DistTestCore.Codex
var companionNodeAccount = companionNode.Accounts[Index]; var companionNodeAccount = companionNode.Accounts[Index];
Additional(companionNodeAccount); Additional(companionNodeAccount);
var ip = companionNode.RunningContainer.Pod.Ip; var ip = companionNode.RunningContainer.Pod.PodInfo.Ip;
var port = companionNode.RunningContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag).Number; var port = companionNode.RunningContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag).Number;
AddEnvVar("ETH_PROVIDER", $"ws://{ip}:{port}"); AddEnvVar("ETH_PROVIDER", $"ws://{ip}:{port}");
AddEnvVar("ETH_ACCOUNT", companionNodeAccount.Account); AddEnvVar("ETH_ACCOUNT", companionNodeAccount.Account);
AddEnvVar("ETH_MARKETPLACE_ADDRESS", gethConfig.MarketplaceNetwork.Marketplace.Address); AddEnvVar("ETH_MARKETPLACE_ADDRESS", gethConfig.MarketplaceNetwork.Marketplace.Address);
AddEnvVar("PERSISTENCE", "1");
} }
} }
} }

View File

@ -5,9 +5,14 @@ namespace DistTestCore.Codex
{ {
public class CodexStartupConfig public class CodexStartupConfig
{ {
public CodexStartupConfig(CodexLogLevel logLevel)
{
LogLevel = logLevel;
}
public string? NameOverride { get; set; } public string? NameOverride { get; set; }
public Location Location { get; set; } public Location Location { get; set; }
public CodexLogLevel? LogLevel { get; set; } public CodexLogLevel LogLevel { get; }
public ByteSize? StorageQuota { get; set; } public ByteSize? StorageQuota { get; set; }
public bool MetricsEnabled { get; set; } public bool MetricsEnabled { get; set; }
public MarketplaceInitialConfig? MarketplaceConfig { get; set; } public MarketplaceInitialConfig? MarketplaceConfig { get; set; }

View File

@ -69,7 +69,7 @@ namespace DistTestCore
private OnlineCodexNode CreateOnlineCodexNode(RunningContainer c, ICodexNodeFactory factory) private OnlineCodexNode CreateOnlineCodexNode(RunningContainer c, ICodexNodeFactory factory)
{ {
var access = new CodexAccess(lifecycle.Log, lifecycle.TimeSet, c); var access = new CodexAccess(lifecycle, c);
return factory.CreateOnlineCodexNode(access, this); return factory.CreateOnlineCodexNode(access, this);
} }
} }

View File

@ -8,7 +8,6 @@ namespace DistTestCore
{ {
ICodexSetup WithName(string name); ICodexSetup WithName(string name);
ICodexSetup At(Location location); ICodexSetup At(Location location);
ICodexSetup WithLogLevel(CodexLogLevel level);
ICodexSetup WithBootstrapNode(IOnlineCodexNode node); ICodexSetup WithBootstrapNode(IOnlineCodexNode node);
ICodexSetup WithStorageQuota(ByteSize storageQuota); ICodexSetup WithStorageQuota(ByteSize storageQuota);
ICodexSetup EnableMetrics(); ICodexSetup EnableMetrics();
@ -20,7 +19,8 @@ namespace DistTestCore
{ {
public int NumberOfNodes { get; } public int NumberOfNodes { get; }
public CodexSetup(int numberOfNodes) public CodexSetup(int numberOfNodes, CodexLogLevel logLevel)
: base(logLevel)
{ {
NumberOfNodes = numberOfNodes; NumberOfNodes = numberOfNodes;
} }
@ -43,12 +43,6 @@ namespace DistTestCore
return this; return this;
} }
public ICodexSetup WithLogLevel(CodexLogLevel level)
{
LogLevel = level;
return this;
}
public ICodexSetup WithStorageQuota(ByteSize storageQuota) public ICodexSetup WithStorageQuota(ByteSize storageQuota)
{ {
StorageQuota = storageQuota; StorageQuota = storageQuota;
@ -80,7 +74,7 @@ namespace DistTestCore
private IEnumerable<string> DescribeArgs() private IEnumerable<string> DescribeArgs()
{ {
if (LogLevel != null) yield return $"LogLevel={LogLevel}"; yield return $"LogLevel={LogLevel}";
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}"; if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
if (StorageQuota != null) yield return $"StorageQuote={StorageQuota}"; if (StorageQuota != null) yield return $"StorageQuote={StorageQuota}";
} }

View File

@ -1,6 +1,7 @@
using DistTestCore.Codex; using DistTestCore.Codex;
using DistTestCore.Marketplace; using DistTestCore.Marketplace;
using KubernetesWorkflow; using KubernetesWorkflow;
using Logging;
namespace DistTestCore namespace DistTestCore
{ {
@ -27,7 +28,8 @@ namespace DistTestCore
var codexNodeFactory = new CodexNodeFactory(lifecycle, metricAccessFactory, gethStartResult.MarketplaceAccessFactory); var codexNodeFactory = new CodexNodeFactory(lifecycle, metricAccessFactory, gethStartResult.MarketplaceAccessFactory);
var group = CreateCodexGroup(codexSetup, containers, codexNodeFactory); var group = CreateCodexGroup(codexSetup, containers, codexNodeFactory);
LogEnd($"Started {codexSetup.NumberOfNodes} nodes at '{group.Containers.RunningPod.Ip}'. They are: {group.Describe()}"); var podInfo = group.Containers.RunningPod.PodInfo;
LogEnd($"Started {codexSetup.NumberOfNodes} nodes at location '{podInfo.K8SNodeName}'={podInfo.Ip}. They are: {group.Describe()}");
LogSeparator(); LogSeparator();
return group; return group;
} }
@ -74,7 +76,7 @@ namespace DistTestCore
{ {
var group = new CodexNodeGroup(lifecycle, codexSetup, runningContainers, codexNodeFactory); var group = new CodexNodeGroup(lifecycle, codexSetup, runningContainers, codexNodeFactory);
RunningGroups.Add(group); RunningGroups.Add(group);
group.EnsureOnline(); Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
return group; return group;
} }

View File

@ -1,32 +1,89 @@
using KubernetesWorkflow; using DistTestCore.Codex;
using KubernetesWorkflow;
namespace DistTestCore namespace DistTestCore
{ {
public class Configuration public class Configuration
{ {
private readonly string? kubeConfigFile;
private readonly string logPath;
private readonly bool logDebug;
private readonly string dataFilesPath;
private readonly CodexLogLevel codexLogLevel;
private readonly TestRunnerLocation runnerLocation;
public Configuration()
{
kubeConfigFile = GetNullableEnvVarOrDefault("KUBECONFIG", null);
logPath = GetEnvVarOrDefault("LOGPATH", "CodexTestLogs");
logDebug = GetEnvVarOrDefault("LOGDEBUG", "false").ToLowerInvariant() == "true";
dataFilesPath = GetEnvVarOrDefault("DATAFILEPATH", "TestDataFiles");
codexLogLevel = ParseEnum<CodexLogLevel>(GetEnvVarOrDefault("LOGLEVEL", nameof(CodexLogLevel.Trace)));
runnerLocation = ParseEnum<TestRunnerLocation>(GetEnvVarOrDefault("RUNNERLOCATION", nameof(TestRunnerLocation.ExternalToCluster)));
}
public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet) public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet)
{ {
return new KubernetesWorkflow.Configuration( return new KubernetesWorkflow.Configuration(
k8sNamespacePrefix: "ct-", k8sNamespacePrefix: "ct-",
kubeConfigFile: null, kubeConfigFile: kubeConfigFile,
operationTimeout: timeSet.K8sOperationTimeout(), operationTimeout: timeSet.K8sOperationTimeout(),
retryDelay: timeSet.WaitForK8sServiceDelay(), retryDelay: timeSet.WaitForK8sServiceDelay()
locationMap: new[]
{
new ConfigurationLocationEntry(Location.BensOldGamingMachine, "worker01"),
new ConfigurationLocationEntry(Location.BensLaptop, "worker02"),
}
); );
} }
public Logging.LogConfig GetLogConfig() public Logging.LogConfig GetLogConfig()
{ {
return new Logging.LogConfig("CodexTestLogs", debugEnabled: false); return new Logging.LogConfig(logPath, debugEnabled: logDebug);
} }
public string GetFileManagerFolder() public string GetFileManagerFolder()
{ {
return "TestDataFiles"; return dataFilesPath;
}
public CodexLogLevel GetCodexLogLevel()
{
return codexLogLevel;
}
public TestRunnerLocation GetTestRunnerLocation()
{
return runnerLocation;
}
public RunningContainerAddress GetAddress(RunningContainer container)
{
if (GetTestRunnerLocation() == TestRunnerLocation.InternalToCluster)
{
return container.ClusterInternalAddress;
}
return container.ClusterExternalAddress;
}
private static string GetEnvVarOrDefault(string varName, string defaultValue)
{
var v = Environment.GetEnvironmentVariable(varName);
if (v == null) return defaultValue;
return v;
}
private static string? GetNullableEnvVarOrDefault(string varName, string? defaultValue)
{
var v = Environment.GetEnvironmentVariable(varName);
if (v == null) return defaultValue;
return v;
}
private static T ParseEnum<T>(string value)
{
return (T)Enum.Parse(typeof(T), value, true);
} }
} }
public enum TestRunnerLocation
{
ExternalToCluster,
InternalToCluster,
}
} }

View File

@ -1,4 +1,5 @@
using DistTestCore.Codex; using DistTestCore.Codex;
using DistTestCore.Helpers;
using DistTestCore.Logs; using DistTestCore.Logs;
using DistTestCore.Marketplace; using DistTestCore.Marketplace;
using DistTestCore.Metrics; using DistTestCore.Metrics;
@ -25,8 +26,14 @@ namespace DistTestCore
testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray(); testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
fixtureLog = new FixtureLog(configuration.GetLogConfig()); fixtureLog = new FixtureLog(configuration.GetLogConfig());
PeerConnectionTestHelpers = new PeerConnectionTestHelpers(this);
PeerDownloadTestHelpers = new PeerDownloadTestHelpers(this);
} }
public PeerConnectionTestHelpers PeerConnectionTestHelpers { get; }
public PeerDownloadTestHelpers PeerDownloadTestHelpers { get; }
[OneTimeSetUp] [OneTimeSetUp]
public void GlobalSetup() public void GlobalSetup()
{ {
@ -85,6 +92,17 @@ namespace DistTestCore
return Get().FileManager.GenerateTestFile(size); return Get().FileManager.GenerateTestFile(size);
} }
/// <summary>
/// Any test files generated in 'action' will be deleted after it returns.
/// This helps prevent large tests from filling up discs.
/// </summary>
public void ScopedTestFiles(Action action)
{
Get().FileManager.PushFileSet();
action();
Get().FileManager.PopFileSet();
}
public IOnlineCodexNode SetupCodexBootstrapNode() public IOnlineCodexNode SetupCodexBootstrapNode()
{ {
return SetupCodexBootstrapNode(s => { }); return SetupCodexBootstrapNode(s => { });
@ -116,7 +134,7 @@ namespace DistTestCore
public virtual ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup) public virtual ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
{ {
var codexSetup = new CodexSetup(numberOfNodes); var codexSetup = CreateCodexSetup(numberOfNodes);
setup(codexSetup); setup(codexSetup);
@ -128,16 +146,31 @@ namespace DistTestCore
return Get().CodexStarter.BringOnline((CodexSetup)codexSetup); return Get().CodexStarter.BringOnline((CodexSetup)codexSetup);
} }
protected void Log(string msg) public IEnumerable<IOnlineCodexNode> GetAllOnlineCodexNodes()
{ {
TestContext.Progress.WriteLine(msg); return Get().CodexStarter.RunningGroups.SelectMany(g => g.Nodes);
Get().Log.Log(msg);
} }
protected void Debug(string msg) public BaseLog GetTestLog()
{
return Get().Log;
}
public void Log(string msg)
{ {
TestContext.Progress.WriteLine(msg); TestContext.Progress.WriteLine(msg);
Get().Log.Debug(msg); GetTestLog().Log(msg);
}
public void Debug(string msg)
{
TestContext.Progress.WriteLine(msg);
GetTestLog().Debug(msg);
}
protected CodexSetup CreateCodexSetup(int numberOfNodes)
{
return new CodexSetup(numberOfNodes, configuration.GetCodexLogLevel());
} }
private TestLifecycle Get() private TestLifecycle Get()

View File

@ -9,6 +9,8 @@ namespace DistTestCore
TestFile CreateEmptyTestFile(); TestFile CreateEmptyTestFile();
TestFile GenerateTestFile(ByteSize size); TestFile GenerateTestFile(ByteSize size);
void DeleteAllTestFiles(); void DeleteAllTestFiles();
void PushFileSet();
void PopFileSet();
} }
public class FileManager : IFileManager public class FileManager : IFileManager
@ -18,6 +20,7 @@ namespace DistTestCore
private readonly Random random = new Random(); private readonly Random random = new Random();
private readonly TestLog log; private readonly TestLog log;
private readonly string folder; private readonly string folder;
private readonly List<List<TestFile>> fileSetStack = new List<List<TestFile>>();
public FileManager(TestLog log, Configuration configuration) public FileManager(TestLog log, Configuration configuration)
{ {
@ -31,6 +34,7 @@ namespace DistTestCore
{ {
var result = new TestFile(Path.Combine(folder, Guid.NewGuid().ToString() + "_test.bin")); var result = new TestFile(Path.Combine(folder, Guid.NewGuid().ToString() + "_test.bin"));
File.Create(result.Filename).Close(); File.Create(result.Filename).Close();
if (fileSetStack.Any()) fileSetStack.Last().Add(result);
return result; return result;
} }
@ -47,6 +51,27 @@ namespace DistTestCore
DeleteDirectory(); DeleteDirectory();
} }
public void PushFileSet()
{
fileSetStack.Add(new List<TestFile>());
}
public void PopFileSet()
{
if (!fileSetStack.Any()) return;
var pop = fileSetStack.Last();
fileSetStack.Remove(pop);
foreach (var file in pop)
{
try
{
File.Delete(file.Filename);
}
catch { }
}
}
private void GenerateFileBytes(TestFile result, ByteSize size) private void GenerateFileBytes(TestFile result, ByteSize size)
{ {
long bytesLeft = size.SizeInBytes; long bytesLeft = size.SizeInBytes;

View File

@ -33,7 +33,7 @@ namespace DistTestCore
private void TransferInitialBalance(MarketplaceNetwork marketplaceNetwork, MarketplaceInitialConfig marketplaceConfig, GethCompanionNodeInfo companionNode) private void TransferInitialBalance(MarketplaceNetwork marketplaceNetwork, MarketplaceInitialConfig marketplaceConfig, GethCompanionNodeInfo companionNode)
{ {
var interaction = marketplaceNetwork.StartInteraction(lifecycle.Log); var interaction = marketplaceNetwork.StartInteraction(lifecycle);
var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress; var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress;
var accounts = companionNode.Accounts.Select(a => a.Account).ToArray(); var accounts = companionNode.Accounts.Select(a => a.Account).ToArray();
@ -52,7 +52,7 @@ namespace DistTestCore
private IMarketplaceAccessFactory CreateMarketplaceAccessFactory(MarketplaceNetwork marketplaceNetwork) private IMarketplaceAccessFactory CreateMarketplaceAccessFactory(MarketplaceNetwork marketplaceNetwork)
{ {
return new GethMarketplaceAccessFactory(lifecycle.Log, marketplaceNetwork); return new GethMarketplaceAccessFactory(lifecycle, marketplaceNetwork);
} }
private GethCompanionNodeInfo StartCompanionNode(CodexSetup codexSetup, MarketplaceNetwork marketplaceNetwork) private GethCompanionNodeInfo StartCompanionNode(CodexSetup codexSetup, MarketplaceNetwork marketplaceNetwork)

View File

@ -0,0 +1,22 @@
using NUnit.Framework.Constraints;
using NUnit.Framework;
using Utils;
namespace DistTestCore.Helpers
{
public static class AssertHelpers
{
public static void RetryAssert<T>(IResolveConstraint constraint, Func<T> actual, string message)
{
try
{
var c = constraint.Resolve();
Time.WaitUntil(() => c.ApplyTo(actual()).IsSuccess);
}
catch (TimeoutException)
{
Assert.That(actual(), constraint, message);
}
}
}
}

View File

@ -0,0 +1,239 @@
using DistTestCore.Codex;
using NUnit.Framework;
using Utils;
namespace DistTestCore.Helpers
{
public class PeerConnectionTestHelpers
{
private readonly Random random = new Random();
private readonly DistTest test;
public PeerConnectionTestHelpers(DistTest test)
{
this.test = test;
}
public void AssertFullyConnected(IEnumerable<IOnlineCodexNode> nodes)
{
AssertFullyConnected(nodes.ToArray());
}
public void AssertFullyConnected(params 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.FromMinutes(10);
while (pairs.Any() && timeout > DateTime.UtcNow)
{
action();
if (pairs.Any()) Time.Sleep(TimeSpan.FromSeconds(5));
}
}
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)
{
test.Log(pair.GetMessage());
pairs.Remove(pair);
}
}
}
private static Entry[] CreateEntries(IOnlineCodexNode[] nodes)
{
var entries = nodes.Select(n => new Entry(n)).ToArray();
var incorrectDiscoveryEndpoints = entries.SelectMany(e => e.GetInCorrectDiscoveryEndpoints(entries)).ToArray();
if (incorrectDiscoveryEndpoints.Any())
{
Assert.Fail("Some nodes contain peer records with incorrect discovery ip/port information: " +
string.Join(Environment.NewLine, incorrectDiscoveryEndpoints));
}
return entries;
}
private static List<Pair> CreatePairs(Entry[] entries)
{
return CreatePairsIterator(entries).ToList();
}
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}'";
}
}
}
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 readonly TimeSpan timeout = TimeSpan.FromSeconds(60);
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();
}
private string GetResultMessage()
{
var aName = A.Response.id;
var bName = B.Response.id;
if (Success)
{
return $"{aName} and {bName} know each other.";
}
return $"[{aName}-->{bName}] = {AKnowsB} AND [{aName}<--{bName}] = {BKnowsA}";
}
private string GetTimePostfix()
{
var aName = A.Response.id;
var bName = B.Response.id;
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, timeout);
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;
}
}
}
}
}

View File

@ -0,0 +1,52 @@
namespace DistTestCore.Helpers
{
public class PeerDownloadTestHelpers
{
private readonly DistTest test;
public PeerDownloadTestHelpers(DistTest test)
{
this.test = test;
}
public void AssertFullDownloadInterconnectivity(IEnumerable<IOnlineCodexNode> nodes)
{
AssertFullDownloadInterconnectivity(nodes, 1.MB());
}
public void AssertFullDownloadInterconnectivity(IEnumerable<IOnlineCodexNode> nodes, ByteSize testFileSize)
{
test.Log($"Asserting full download interconnectivity for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
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()))}");
}
private void PerformTest(IOnlineCodexNode uploader, IOnlineCodexNode[] downloaders, ByteSize testFileSize)
{
// 1 test file per downloader.
var files = downloaders.Select(d => test.GenerateTestFile(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.AssertIsEqual(downloadedFile);
}
}
}
}

View File

@ -1,6 +1,6 @@
using Logging; using KubernetesWorkflow;
using Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using NUnit.Framework;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using Utils; using Utils;
@ -11,18 +11,17 @@ namespace DistTestCore
{ {
private readonly BaseLog log; private readonly BaseLog log;
private readonly ITimeSet timeSet; private readonly ITimeSet timeSet;
private readonly string ip; private readonly RunningContainerAddress address;
private readonly int port;
private readonly string baseUrl; private readonly string baseUrl;
private readonly TimeSpan? timeoutOverride;
public Http(BaseLog log, ITimeSet timeSet, string ip, int port, string baseUrl) public Http(BaseLog log, ITimeSet timeSet, RunningContainerAddress address, string baseUrl, TimeSpan? timeoutOverride = null)
{ {
this.log = log; this.log = log;
this.timeSet = timeSet; this.timeSet = timeSet;
this.ip = ip; this.address = address;
this.port = port;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.timeoutOverride = timeoutOverride;
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl; if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/"; if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
} }
@ -38,7 +37,7 @@ namespace DistTestCore
var str = Time.Wait(result.Content.ReadAsStringAsync()); var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str); Log(url, str);
return str; ; return str; ;
}); }, $"HTTP-GET:{route}");
} }
public T HttpGetJson<T>(string route) public T HttpGetJson<T>(string route)
@ -62,10 +61,10 @@ namespace DistTestCore
using var content = JsonContent.Create(body); using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body)); Log(url, JsonConvert.SerializeObject(body));
var result = Time.Wait(client.PostAsync(url, content)); var result = Time.Wait(client.PostAsync(url, content));
var str= Time.Wait(result.Content.ReadAsStringAsync()); var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str); Log(url, str);
return str; return str;
}); }, $"HTTP-POST-JSON: {route}");
} }
public string HttpPostStream(string route, Stream stream) public string HttpPostStream(string route, Stream stream)
@ -81,7 +80,7 @@ namespace DistTestCore
var str =Time.Wait(response.Content.ReadAsStringAsync()); var str =Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str); Log(url, str);
return str; return str;
}); }, $"HTTP-POST-STREAM: {route}");
} }
public Stream HttpGetStream(string route) public Stream HttpGetStream(string route)
@ -92,43 +91,10 @@ namespace DistTestCore
var url = GetUrl() + route; var url = GetUrl() + route;
Log(url, "~ STREAM ~"); Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url)); return Time.Wait(client.GetStreamAsync(url));
}); }, $"HTTP-GET-STREAM: {route}");
} }
private string GetUrl() public T TryJsonDeserialize<T>(string json)
{
return $"http://{ip}:{port}{baseUrl}";
}
private void Log(string url, string message)
{
log.Debug($"({url}) = '{message}'", 3);
}
private T Retry<T>(Func<T> operation)
{
var retryCounter = 0;
while (true)
{
try
{
return operation();
}
catch (Exception exception)
{
timeSet.HttpCallRetryDelay();
retryCounter++;
if (retryCounter > timeSet.HttpCallRetryCount())
{
Assert.Fail(exception.ToString());
throw;
}
}
}
}
private static T TryJsonDeserialize<T>(string json)
{ {
try try
{ {
@ -137,15 +103,38 @@ namespace DistTestCore
catch (Exception exception) catch (Exception exception)
{ {
var msg = $"Failed to deserialize JSON: '{json}' with exception: {exception}"; var msg = $"Failed to deserialize JSON: '{json}' with exception: {exception}";
Assert.Fail(msg);
throw new InvalidOperationException(msg, exception); throw new InvalidOperationException(msg, exception);
} }
} }
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
log.Debug($"({url}) = '{message}'", 3);
}
private T Retry<T>(Func<T> operation, string description)
{
return Time.Retry(operation, timeSet.HttpCallRetryTimeout(), timeSet.HttpCallRetryDelay(), description);
}
private HttpClient GetClient() private HttpClient GetClient()
{
if (timeoutOverride.HasValue)
{
return GetClient(timeoutOverride.Value);
}
return GetClient(timeSet.HttpCallTimeout());
}
private HttpClient GetClient(TimeSpan timeout)
{ {
var client = new HttpClient(); var client = new HttpClient();
client.Timeout = timeSet.HttpCallTimeout(); client.Timeout = timeout;
return client; return client;
} }
} }

View File

@ -7,7 +7,7 @@ namespace DistTestCore.Marketplace
#if Arm64 #if Arm64
public const string DockerImage = "emizzle/codex-contracts-deployment:latest"; public const string DockerImage = "emizzle/codex-contracts-deployment:latest";
#else #else
public const string DockerImage = "thatbenbierens/codex-contracts-deployment:nomint"; public const string DockerImage = "thatbenbierens/codex-contracts-deployment:nomint2";
#endif #endif
public const string MarketplaceAddressFilename = "/usr/app/deployments/codexdisttestnetwork/Marketplace.json"; 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 MarketplaceArtifactFilename = "/usr/app/artifacts/contracts/Marketplace.sol/Marketplace.json";

View File

@ -5,7 +5,6 @@ namespace DistTestCore.Marketplace
{ {
public class CodexContractsStarter : BaseStarter public class CodexContractsStarter : BaseStarter
{ {
private const string readyString = "Done! Sleeping indefinitely...";
public CodexContractsStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator) public CodexContractsStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator) : base(lifecycle, workflowCreator)
@ -14,7 +13,7 @@ namespace DistTestCore.Marketplace
public MarketplaceInfo Start(GethBootstrapNodeInfo bootstrapNode) public MarketplaceInfo Start(GethBootstrapNodeInfo bootstrapNode)
{ {
LogStart("Deploying Codex contracts..."); LogStart("Deploying Codex Marketplace...");
var workflow = workflowCreator.CreateWorkflow(); var workflow = workflowCreator.CreateWorkflow();
var startupConfig = CreateStartupConfig(bootstrapNode.RunningContainers.Containers[0]); var startupConfig = CreateStartupConfig(bootstrapNode.RunningContainers.Containers[0]);
@ -25,32 +24,33 @@ namespace DistTestCore.Marketplace
WaitUntil(() => WaitUntil(() =>
{ {
var logHandler = new ContractsReadyLogHandler(readyString); var logHandler = new ContractsReadyLogHandler(Debug);
workflow.DownloadContainerLog(container, logHandler); workflow.DownloadContainerLog(container, logHandler);
return logHandler.Found; return logHandler.Found;
}); });
Log("Contracts deployed. Extracting addresses...");
var extractor = new ContainerInfoExtractor(lifecycle.Log, workflow, container); var extractor = new ContainerInfoExtractor(lifecycle.Log, workflow, container);
var marketplaceAddress = extractor.ExtractMarketplaceAddress(); var marketplaceAddress = extractor.ExtractMarketplaceAddress();
var abi = extractor.ExtractMarketplaceAbi(); var abi = extractor.ExtractMarketplaceAbi();
var interaction = bootstrapNode.StartInteraction(lifecycle.Log); var interaction = bootstrapNode.StartInteraction(lifecycle);
var tokenAddress = interaction.GetTokenAddress(marketplaceAddress); var tokenAddress = interaction.GetTokenAddress(marketplaceAddress);
LogEnd("Contracts deployed."); LogEnd("Extract completed. Marketplace deployed.");
return new MarketplaceInfo(marketplaceAddress, abi, tokenAddress); return new MarketplaceInfo(marketplaceAddress, abi, tokenAddress);
} }
private void WaitUntil(Func<bool> predicate) private void WaitUntil(Func<bool> predicate)
{ {
Time.WaitUntil(predicate, TimeSpan.FromMinutes(2), TimeSpan.FromSeconds(1)); Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2));
} }
private StartupConfig CreateStartupConfig(RunningContainer bootstrapContainer) private StartupConfig CreateStartupConfig(RunningContainer bootstrapContainer)
{ {
var startupConfig = new StartupConfig(); var startupConfig = new StartupConfig();
var contractsConfig = new CodexContractsContainerConfig(bootstrapContainer.Pod.Ip, bootstrapContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag)); var contractsConfig = new CodexContractsContainerConfig(bootstrapContainer.Pod.PodInfo.Ip, bootstrapContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag));
startupConfig.Add(contractsConfig); startupConfig.Add(contractsConfig);
return startupConfig; return startupConfig;
} }
@ -72,18 +72,27 @@ namespace DistTestCore.Marketplace
public class ContractsReadyLogHandler : LogHandler public class ContractsReadyLogHandler : LogHandler
{ {
private readonly string targetString; // Log should contain 'Compiled 15 Solidity files successfully' at some point.
private const string RequiredCompiledString = "Solidity files successfully";
// When script is done, it prints the ready-string.
private const string ReadyString = "Done! Sleeping indefinitely...";
private readonly Action<string> debug;
public ContractsReadyLogHandler(string targetString) public ContractsReadyLogHandler(Action<string> debug)
{ {
this.targetString = targetString; this.debug = debug;
debug($"Looking for '{RequiredCompiledString}' and '{ReadyString}' in container logs...");
} }
public bool SeenCompileString { get; private set; }
public bool Found { get; private set; } public bool Found { get; private set; }
protected override void ProcessLine(string line) protected override void ProcessLine(string line)
{ {
if (line.Contains(targetString)) Found = true; debug(line);
if (line.Contains(RequiredCompiledString)) SeenCompileString = true;
if (SeenCompileString && line.Contains(ReadyString)) Found = true;
} }
} }
} }

View File

@ -56,30 +56,6 @@ namespace DistTestCore.Marketplace
return marketplaceAbi; return marketplaceAbi;
} }
private string Retry(Func<string> fetch)
{
var result = string.Empty;
Time.WaitUntil(() =>
{
result = Catch(fetch);
return !string.IsNullOrEmpty(result);
}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(3));
return result;
}
private string Catch(Func<string> fetch)
{
try
{
return fetch();
}
catch
{
return string.Empty;
}
}
private string FetchAccountsCsv() private string FetchAccountsCsv()
{ {
return workflow.ExecuteCommand(container, "cat", GethContainerRecipe.AccountsFilename); return workflow.ExecuteCommand(container, "cat", GethContainerRecipe.AccountsFilename);
@ -103,7 +79,7 @@ namespace DistTestCore.Marketplace
private string FetchPubKey() private string FetchPubKey()
{ {
var enodeFinder = new PubKeyFinder(); var enodeFinder = new PubKeyFinder(s => log.Debug(s));
workflow.DownloadContainerLog(container, enodeFinder); workflow.DownloadContainerLog(container, enodeFinder);
return enodeFinder.GetPubKey(); return enodeFinder.GetPubKey();
} }
@ -116,21 +92,35 @@ namespace DistTestCore.Marketplace
var privateKey = tokens[1]; var privateKey = tokens[1];
return new GethAccount(account, privateKey); return new GethAccount(account, privateKey);
} }
private static string Retry(Func<string> fetch)
{
return Time.Retry(fetch, nameof(ContainerInfoExtractor));
}
} }
public class PubKeyFinder : LogHandler, ILogHandler public class PubKeyFinder : LogHandler, ILogHandler
{ {
private const string openTag = "self=enode://"; private const string openTag = "self=enode://";
private const string openTagQuote = "self=\"enode://"; private const string openTagQuote = "self=\"enode://";
private readonly Action<string> debug;
private string pubKey = string.Empty; private string pubKey = string.Empty;
public PubKeyFinder(Action<string> debug)
{
this.debug = debug;
debug($"Looking for '{openTag}' in container logs...");
}
public string GetPubKey() public string GetPubKey()
{ {
if (string.IsNullOrEmpty(pubKey)) throw new Exception("Not found yet exception.");
return pubKey; return pubKey;
} }
protected override void ProcessLine(string line) protected override void ProcessLine(string line)
{ {
debug(line);
if (line.Contains(openTag)) if (line.Contains(openTag))
{ {
ExtractPubKey(openTag, line); ExtractPubKey(openTag, line);

View File

@ -1,5 +1,4 @@
using KubernetesWorkflow; using KubernetesWorkflow;
using Logging;
using NethereumWorkflow; using NethereumWorkflow;
namespace DistTestCore.Marketplace namespace DistTestCore.Marketplace
@ -21,13 +20,12 @@ namespace DistTestCore.Marketplace
public string PubKey { get; } public string PubKey { get; }
public Port DiscoveryPort { get; } public Port DiscoveryPort { get; }
public NethereumInteraction StartInteraction(BaseLog log) public NethereumInteraction StartInteraction(TestLifecycle lifecycle)
{ {
var ip = RunningContainers.RunningPod.Cluster.IP; var address = lifecycle.Configuration.GetAddress(RunningContainers.Containers[0]);
var port = RunningContainers.Containers[0].ServicePorts[0].Number;
var account = Account; var account = Account;
var creator = new NethereumInteractionCreator(log, ip, port, account.PrivateKey); var creator = new NethereumInteractionCreator(lifecycle.Log, address.Host, address.Port, account.PrivateKey);
return creator.CreateWorkflow(); return creator.CreateWorkflow();
} }
} }

View File

@ -1,5 +1,4 @@
using KubernetesWorkflow; using KubernetesWorkflow;
using Logging;
using NethereumWorkflow; using NethereumWorkflow;
namespace DistTestCore.Marketplace namespace DistTestCore.Marketplace
@ -15,13 +14,12 @@ namespace DistTestCore.Marketplace
public RunningContainer RunningContainer { get; } public RunningContainer RunningContainer { get; }
public GethAccount[] Accounts { get; } public GethAccount[] Accounts { get; }
public NethereumInteraction StartInteraction(BaseLog log, GethAccount account) public NethereumInteraction StartInteraction(TestLifecycle lifecycle, GethAccount account)
{ {
var ip = RunningContainer.Pod.Cluster.IP; var address = lifecycle.Configuration.GetAddress(RunningContainer);
var port = RunningContainer.ServicePorts[0].Number;
var privateKey = account.PrivateKey; var privateKey = account.PrivateKey;
var creator = new NethereumInteractionCreator(log, ip, port, privateKey); var creator = new NethereumInteractionCreator(lifecycle.Log, address.Host, address.Port, privateKey);
return creator.CreateWorkflow(); return creator.CreateWorkflow();
} }
} }

View File

@ -50,7 +50,7 @@ namespace DistTestCore.Marketplace
{ {
Time.WaitUntil(() => Time.WaitUntil(() =>
{ {
var interaction = node.StartInteraction(lifecycle.Log, node.Accounts.First()); var interaction = node.StartInteraction(lifecycle, node.Accounts.First());
return interaction.IsSynced(marketplace.Marketplace.Address, marketplace.Marketplace.Abi); return interaction.IsSynced(marketplace.Marketplace.Address, marketplace.Marketplace.Abi);
}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(3)); }, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(3));
} }

View File

@ -58,7 +58,7 @@ namespace DistTestCore.Marketplace
var httpPort = AddExposedPort(tag: HttpPortTag); var httpPort = AddExposedPort(tag: HttpPortTag);
var bootPubKey = config.BootstrapNode.PubKey; var bootPubKey = config.BootstrapNode.PubKey;
var bootIp = config.BootstrapNode.RunningContainers.Containers[0].Pod.Ip; var bootIp = config.BootstrapNode.RunningContainers.Containers[0].Pod.PodInfo.Ip;
var bootPort = config.BootstrapNode.DiscoveryPort.Number; var bootPort = config.BootstrapNode.DiscoveryPort.Number;
var bootstrapArg = $"--bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}"; var bootstrapArg = $"--bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}";

View File

@ -1,5 +1,5 @@
using DistTestCore.Codex; using DistTestCore.Codex;
using Logging; using DistTestCore.Helpers;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Constraints; using NUnit.Framework.Constraints;
using System.Numerics; using System.Numerics;
@ -17,14 +17,14 @@ namespace DistTestCore.Marketplace
public class MarketplaceAccess : IMarketplaceAccess public class MarketplaceAccess : IMarketplaceAccess
{ {
private readonly TestLog log; private readonly TestLifecycle lifecycle;
private readonly MarketplaceNetwork marketplaceNetwork; private readonly MarketplaceNetwork marketplaceNetwork;
private readonly GethAccount account; private readonly GethAccount account;
private readonly CodexAccess codexAccess; private readonly CodexAccess codexAccess;
public MarketplaceAccess(TestLog log, MarketplaceNetwork marketplaceNetwork, GethAccount account, CodexAccess codexAccess) public MarketplaceAccess(TestLifecycle lifecycle, MarketplaceNetwork marketplaceNetwork, GethAccount account, CodexAccess codexAccess)
{ {
this.log = log; this.lifecycle = lifecycle;
this.marketplaceNetwork = marketplaceNetwork; this.marketplaceNetwork = marketplaceNetwork;
this.account = account; this.account = account;
this.codexAccess = codexAccess; this.codexAccess = codexAccess;
@ -98,12 +98,12 @@ namespace DistTestCore.Marketplace
public void AssertThatBalance(IResolveConstraint constraint, string message = "") public void AssertThatBalance(IResolveConstraint constraint, string message = "")
{ {
Assert.That(GetBalance(), constraint, message); AssertHelpers.RetryAssert(constraint, GetBalance, message);
} }
public TestToken GetBalance() public TestToken GetBalance()
{ {
var interaction = marketplaceNetwork.StartInteraction(log); var interaction = marketplaceNetwork.StartInteraction(lifecycle);
var amount = interaction.GetBalance(marketplaceNetwork.Marketplace.TokenAddress, account.Account); var amount = interaction.GetBalance(marketplaceNetwork.Marketplace.TokenAddress, account.Account);
var balance = new TestToken(amount); var balance = new TestToken(amount);
@ -114,7 +114,7 @@ namespace DistTestCore.Marketplace
private void Log(string msg) private void Log(string msg)
{ {
log.Log($"{codexAccess.Container.Name} {msg}"); lifecycle.Log.Log($"{codexAccess.Container.Name} {msg}");
} }
} }

View File

@ -18,19 +18,19 @@ namespace DistTestCore.Marketplace
public class GethMarketplaceAccessFactory : IMarketplaceAccessFactory public class GethMarketplaceAccessFactory : IMarketplaceAccessFactory
{ {
private readonly TestLog log; private readonly TestLifecycle lifecycle;
private readonly MarketplaceNetwork marketplaceNetwork; private readonly MarketplaceNetwork marketplaceNetwork;
public GethMarketplaceAccessFactory(TestLog log, MarketplaceNetwork marketplaceNetwork) public GethMarketplaceAccessFactory(TestLifecycle lifecycle, MarketplaceNetwork marketplaceNetwork)
{ {
this.log = log; this.lifecycle = lifecycle;
this.marketplaceNetwork = marketplaceNetwork; this.marketplaceNetwork = marketplaceNetwork;
} }
public IMarketplaceAccess CreateMarketplaceAccess(CodexAccess access) public IMarketplaceAccess CreateMarketplaceAccess(CodexAccess access)
{ {
var companionNode = GetGethCompanionNode(access); var companionNode = GetGethCompanionNode(access);
return new MarketplaceAccess(log, marketplaceNetwork, companionNode, access); return new MarketplaceAccess(lifecycle, marketplaceNetwork, companionNode, access);
} }
private GethAccount GetGethCompanionNode(CodexAccess access) private GethAccount GetGethCompanionNode(CodexAccess access)

View File

@ -1,5 +1,4 @@
using Logging; using NethereumWorkflow;
using NethereumWorkflow;
namespace DistTestCore.Marketplace namespace DistTestCore.Marketplace
{ {
@ -14,9 +13,9 @@ namespace DistTestCore.Marketplace
public GethBootstrapNodeInfo Bootstrap { get; } public GethBootstrapNodeInfo Bootstrap { get; }
public MarketplaceInfo Marketplace { get; } public MarketplaceInfo Marketplace { get; }
public NethereumInteraction StartInteraction(BaseLog log) public NethereumInteraction StartInteraction(TestLifecycle lifecycle)
{ {
return Bootstrap.StartInteraction(log); return Bootstrap.StartInteraction(lifecycle);
} }
} }
} }

View File

@ -1,4 +1,5 @@
using KubernetesWorkflow; using DistTestCore.Helpers;
using KubernetesWorkflow;
using Logging; using Logging;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Constraints; using NUnit.Framework.Constraints;
@ -28,12 +29,14 @@ namespace DistTestCore.Metrics
public void AssertThat(string metricName, IResolveConstraint constraint, string message = "") public void AssertThat(string metricName, IResolveConstraint constraint, string message = "")
{ {
var metricSet = GetMetricWithTimeout(metricName); AssertHelpers.RetryAssert(constraint, () =>
var metricValue = metricSet.Values[0].Value; {
var metricSet = GetMetricWithTimeout(metricName);
var metricValue = metricSet.Values[0].Value;
log.Log($"{node.Name} metric '{metricName}' = {metricValue}"); log.Log($"{node.Name} metric '{metricName}' = {metricValue}");
return metricValue;
Assert.That(metricValue, constraint, message); }, message);
} }
public Metrics? GetAllMetrics() public Metrics? GetAllMetrics()

View File

@ -28,7 +28,7 @@ namespace DistTestCore.Metrics
public IMetricsAccess CreateMetricsAccess(RunningContainer codexContainer) public IMetricsAccess CreateMetricsAccess(RunningContainer codexContainer)
{ {
var query = new MetricsQuery(lifecycle.Log, lifecycle.TimeSet, prometheusContainer); var query = new MetricsQuery(lifecycle, prometheusContainer);
return new MetricsAccess(lifecycle.Log, lifecycle.TimeSet, query, codexContainer); return new MetricsAccess(lifecycle.Log, lifecycle.TimeSet, query, codexContainer);
} }
} }

View File

@ -1,6 +1,5 @@
using DistTestCore.Codex; using DistTestCore.Codex;
using KubernetesWorkflow; using KubernetesWorkflow;
using Logging;
using System.Globalization; using System.Globalization;
namespace DistTestCore.Metrics namespace DistTestCore.Metrics
@ -9,15 +8,16 @@ namespace DistTestCore.Metrics
{ {
private readonly Http http; private readonly Http http;
public MetricsQuery(BaseLog log, ITimeSet timeSet, RunningContainers runningContainers) public MetricsQuery(TestLifecycle lifecycle, RunningContainers runningContainers)
{ {
RunningContainers = runningContainers; RunningContainers = runningContainers;
var address = lifecycle.Configuration.GetAddress(runningContainers.Containers[0]);
http = new Http( http = new Http(
log, lifecycle.Log,
timeSet, lifecycle.TimeSet,
runningContainers.RunningPod.Cluster.IP, address,
runningContainers.Containers[0].ServicePorts[0].Number,
"api/v1"); "api/v1");
} }
@ -119,7 +119,7 @@ namespace DistTestCore.Metrics
private string GetInstanceNameForNode(RunningContainer node) private string GetInstanceNameForNode(RunningContainer node)
{ {
var ip = node.Pod.Ip; var ip = node.Pod.PodInfo.Ip;
var port = node.Recipe.GetPortByTag(CodexContainerRecipe.MetricsPortTag).Number; var port = node.Recipe.GetPortByTag(CodexContainerRecipe.MetricsPortTag).Number;
return $"{ip}:{port}"; return $"{ip}:{port}";
} }

View File

@ -10,6 +10,8 @@ namespace DistTestCore
{ {
string GetName(); string GetName();
CodexDebugResponse GetDebugInfo(); CodexDebugResponse GetDebugInfo();
CodexDebugPeerResponse GetDebugPeer(string peerId);
CodexDebugPeerResponse GetDebugPeer(string peerId, TimeSpan timeout);
ContentId UploadFile(TestFile file); ContentId UploadFile(TestFile file);
TestFile? DownloadContent(ContentId contentId); TestFile? DownloadContent(ContentId contentId);
void ConnectToPeer(IOnlineCodexNode node); void ConnectToPeer(IOnlineCodexNode node);
@ -47,10 +49,21 @@ namespace DistTestCore
public CodexDebugResponse GetDebugInfo() public CodexDebugResponse GetDebugInfo()
{ {
var debugInfo = CodexAccess.GetDebugInfo(); var debugInfo = CodexAccess.GetDebugInfo();
Log($"Got DebugInfo with id: '{debugInfo.id}'."); var known = string.Join(",", debugInfo.table.nodes.Select(n => n.peerId));
Log($"Got DebugInfo with id: '{debugInfo.id}'. This node knows: {known}");
return debugInfo; return debugInfo;
} }
public CodexDebugPeerResponse GetDebugPeer(string peerId)
{
return CodexAccess.GetDebugPeer(peerId);
}
public CodexDebugPeerResponse GetDebugPeer(string peerId, TimeSpan timeout)
{
return CodexAccess.GetDebugPeer(peerId, timeout);
}
public ContentId UploadFile(TestFile file) public ContentId UploadFile(TestFile file)
{ {
Log($"Uploading file of size {file.GetFileSize()}..."); Log($"Uploading file of size {file.GetFileSize()}...");
@ -111,7 +124,7 @@ namespace DistTestCore
// The peer we want to connect is in a different pod. // The peer we want to connect is in a different pod.
// We must replace the default IP with the pod IP in the multiAddress. // We must replace the default IP with the pod IP in the multiAddress.
return multiAddress.Replace("0.0.0.0", peer.Group.Containers.RunningPod.Ip); return multiAddress.Replace("0.0.0.0", peer.Group.Containers.RunningPod.PodInfo.Ip);
} }
private void DownloadToFile(string contentId, TestFile file) private void DownloadToFile(string contentId, TestFile file)

View File

@ -44,7 +44,7 @@ namespace DistTestCore
foreach (var node in nodes) foreach (var node in nodes)
{ {
var ip = node.Pod.Ip; var ip = node.Pod.PodInfo.Ip;
var port = node.Recipe.GetPortByTag(CodexContainerRecipe.MetricsPortTag).Number; var port = node.Recipe.GetPortByTag(CodexContainerRecipe.MetricsPortTag).Number;
config += $" - '{ip}:{port}'\n"; config += $" - '{ip}:{port}'\n";
} }

View File

@ -13,6 +13,7 @@ namespace DistTestCore
public TestLifecycle(TestLog log, Configuration configuration, ITimeSet timeSet) public TestLifecycle(TestLog log, Configuration configuration, ITimeSet timeSet)
{ {
Log = log; Log = log;
Configuration = configuration;
TimeSet = timeSet; TimeSet = timeSet;
workflowCreator = new WorkflowCreator(log, configuration.GetK8sConfiguration(timeSet)); workflowCreator = new WorkflowCreator(log, configuration.GetK8sConfiguration(timeSet));
@ -24,6 +25,7 @@ namespace DistTestCore
} }
public TestLog Log { get; } public TestLog Log { get; }
public Configuration Configuration { get; }
public ITimeSet TimeSet { get; } public ITimeSet TimeSet { get; }
public FileManager FileManager { get; } public FileManager FileManager { get; }
public CodexStarter CodexStarter { get; } public CodexStarter CodexStarter { get; }

View File

@ -1,5 +1,4 @@
using NUnit.Framework; using NUnit.Framework;
using Utils;
namespace DistTestCore namespace DistTestCore
{ {
@ -11,8 +10,8 @@ namespace DistTestCore
public interface ITimeSet public interface ITimeSet
{ {
TimeSpan HttpCallTimeout(); TimeSpan HttpCallTimeout();
int HttpCallRetryCount(); TimeSpan HttpCallRetryTimeout();
void HttpCallRetryDelay(); TimeSpan HttpCallRetryDelay();
TimeSpan WaitForK8sServiceDelay(); TimeSpan WaitForK8sServiceDelay();
TimeSpan K8sOperationTimeout(); TimeSpan K8sOperationTimeout();
TimeSpan WaitForMetricTimeout(); TimeSpan WaitForMetricTimeout();
@ -25,14 +24,14 @@ namespace DistTestCore
return TimeSpan.FromSeconds(10); return TimeSpan.FromSeconds(10);
} }
public int HttpCallRetryCount() public TimeSpan HttpCallRetryTimeout()
{ {
return 5; return TimeSpan.FromMinutes(1);
} }
public void HttpCallRetryDelay() public TimeSpan HttpCallRetryDelay()
{ {
Time.Sleep(TimeSpan.FromSeconds(3)); return TimeSpan.FromSeconds(3);
} }
public TimeSpan WaitForK8sServiceDelay() public TimeSpan WaitForK8sServiceDelay()
@ -58,14 +57,14 @@ namespace DistTestCore
return TimeSpan.FromHours(2); return TimeSpan.FromHours(2);
} }
public int HttpCallRetryCount() public TimeSpan HttpCallRetryTimeout()
{ {
return 2; return TimeSpan.FromHours(5);
} }
public void HttpCallRetryDelay() public TimeSpan HttpCallRetryDelay()
{ {
Time.Sleep(TimeSpan.FromMinutes(5)); return TimeSpan.FromMinutes(5);
} }
public TimeSpan WaitForK8sServiceDelay() public TimeSpan WaitForK8sServiceDelay()

View File

@ -1,40 +0,0 @@
using Utils;
namespace KubernetesWorkflow
{
public class ApplicationLifecycle
{
private static object instanceLock = new object();
private static ApplicationLifecycle? instance;
private readonly NumberSource servicePortNumberSource = new NumberSource(30001);
private readonly NumberSource namespaceNumberSource = new NumberSource(0);
private ApplicationLifecycle()
{
}
public static ApplicationLifecycle Instance
{
// I know singletons are quite evil. But we need to be sure this object is created only once
// and persists for the entire application lifecycle.
get
{
lock (instanceLock)
{
if (instance == null) instance = new ApplicationLifecycle();
return instance;
}
}
}
public NumberSource GetServiceNumberSource()
{
return servicePortNumberSource;
}
public string GetTestNamespace()
{
return namespaceNumberSource.GetNextNumber().ToString("D5");
}
}
}

View File

@ -28,7 +28,7 @@ namespace KubernetesWorkflow
var input = new[] { command }.Concat(arguments).ToArray(); var input = new[] { command }.Concat(arguments).ToArray();
Time.Wait(client.Run(c => c.NamespacedPodExecAsync( Time.Wait(client.Run(c => c.NamespacedPodExecAsync(
pod.Name, k8sNamespace, containerName, input, false, Callback, new CancellationToken()))); pod.PodInfo.Name, k8sNamespace, containerName, input, false, Callback, new CancellationToken())));
} }
public string GetStdOut() public string GetStdOut()

View File

@ -2,31 +2,17 @@
{ {
public class Configuration public class Configuration
{ {
public Configuration(string k8sNamespacePrefix, string? kubeConfigFile, TimeSpan operationTimeout, TimeSpan retryDelay, ConfigurationLocationEntry[] locationMap) public Configuration(string k8sNamespacePrefix, string? kubeConfigFile, TimeSpan operationTimeout, TimeSpan retryDelay)
{ {
K8sNamespacePrefix = k8sNamespacePrefix; K8sNamespacePrefix = k8sNamespacePrefix;
KubeConfigFile = kubeConfigFile; KubeConfigFile = kubeConfigFile;
OperationTimeout = operationTimeout; OperationTimeout = operationTimeout;
RetryDelay = retryDelay; RetryDelay = retryDelay;
LocationMap = locationMap;
} }
public string K8sNamespacePrefix { get; } public string K8sNamespacePrefix { get; }
public string? KubeConfigFile { get; } public string? KubeConfigFile { get; }
public TimeSpan OperationTimeout { get; } public TimeSpan OperationTimeout { get; }
public TimeSpan RetryDelay { get; } public TimeSpan RetryDelay { get; }
public ConfigurationLocationEntry[] LocationMap { get; }
}
public class ConfigurationLocationEntry
{
public ConfigurationLocationEntry(Location location, string workerName)
{
Location = location;
WorkerName = workerName;
}
public Location Location { get; }
public string WorkerName { get; }
} }
} }

View File

@ -10,19 +10,28 @@ namespace KubernetesWorkflow
} }
public Configuration Configuration { get; } public Configuration Configuration { get; }
public string IP { get; private set; } = string.Empty; public string HostAddress { get; private set; } = string.Empty;
public K8sNodeLabel[] AvailableK8sNodes { get; set; } = new K8sNodeLabel[0];
public KubernetesClientConfiguration GetK8sClientConfig() public KubernetesClientConfiguration GetK8sClientConfig()
{ {
var config = GetConfig(); var config = GetConfig();
UpdateIp(config); UpdateHostAddress(config);
return config; return config;
} }
public string GetNodeLabelForLocation(Location location) public K8sNodeLabel? GetNodeLabelForLocation(Location location)
{ {
if (location == Location.Unspecified) return string.Empty; switch (location)
return Configuration.LocationMap.Single(l => l.Location == location).WorkerName; {
case Location.One:
return K8sNodeIfAvailable(0);
case Location.Two:
return K8sNodeIfAvailable(1);
case Location.Three:
return K8sNodeIfAvailable(2);
}
return null;
} }
public TimeSpan K8sOperationTimeout() public TimeSpan K8sOperationTimeout()
@ -47,10 +56,35 @@ namespace KubernetesWorkflow
} }
} }
private void UpdateIp(KubernetesClientConfiguration config) private void UpdateHostAddress(KubernetesClientConfiguration config)
{ {
var host = config.Host.Replace("https://", ""); var host = config.Host.Replace("https://", "");
IP = host.Substring(0, host.IndexOf(':')); if (host.Contains(":"))
{
HostAddress = "http://" + host.Substring(0, host.IndexOf(':'));
}
else
{
HostAddress = config.Host;
}
}
private K8sNodeLabel? K8sNodeIfAvailable(int index)
{
if (AvailableK8sNodes.Length <= index) return null;
return AvailableK8sNodes[index];
} }
} }
public class K8sNodeLabel
{
public K8sNodeLabel(string key, string value)
{
Key = key;
Value = value;
}
public string Key { get; }
public string Value { get; }
}
} }

View File

@ -22,7 +22,6 @@ namespace KubernetesWorkflow
client = new K8sClient(cluster.GetK8sClientConfig()); client = new K8sClient(cluster.GetK8sClientConfig());
K8sTestNamespace = cluster.Configuration.K8sNamespacePrefix + testNamespace; K8sTestNamespace = cluster.Configuration.K8sNamespacePrefix + testNamespace;
log.Debug($"Test namespace: '{K8sTestNamespace}'");
} }
public void Dispose() public void Dispose()
@ -33,13 +32,14 @@ namespace KubernetesWorkflow
public RunningPod BringOnline(ContainerRecipe[] containerRecipes, Location location) public RunningPod BringOnline(ContainerRecipe[] containerRecipes, Location location)
{ {
log.Debug(); log.Debug();
DiscoverK8sNodes();
EnsureTestNamespace(); EnsureTestNamespace();
var deploymentName = CreateDeployment(containerRecipes, location); var deploymentName = CreateDeployment(containerRecipes, location);
var (serviceName, servicePortsMap) = CreateService(containerRecipes); var (serviceName, servicePortsMap) = CreateService(containerRecipes);
var (podName, podIp) = FetchNewPod(); var podInfo = FetchNewPod();
return new RunningPod(cluster, podName, podIp, deploymentName, serviceName, servicePortsMap); return new RunningPod(cluster, podInfo, deploymentName, serviceName, servicePortsMap);
} }
public void Stop(RunningPod pod) public void Stop(RunningPod pod)
@ -48,22 +48,27 @@ namespace KubernetesWorkflow
if (!string.IsNullOrEmpty(pod.ServiceName)) DeleteService(pod.ServiceName); if (!string.IsNullOrEmpty(pod.ServiceName)) DeleteService(pod.ServiceName);
DeleteDeployment(pod.DeploymentName); DeleteDeployment(pod.DeploymentName);
WaitUntilDeploymentOffline(pod.DeploymentName); WaitUntilDeploymentOffline(pod.DeploymentName);
WaitUntilPodOffline(pod.Name); WaitUntilPodOffline(pod.PodInfo.Name);
} }
public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler) public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler)
{ {
log.Debug(); log.Debug();
using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.Name, K8sTestNamespace, recipe.Name)); using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.PodInfo.Name, K8sTestNamespace, recipe.Name));
logHandler.Log(stream); logHandler.Log(stream);
} }
public string ExecuteCommand(RunningPod pod, string containerName, string command, params string[] args) public string ExecuteCommand(RunningPod pod, string containerName, string command, params string[] args)
{ {
log.Debug($"{containerName}: {command} ({string.Join(",", args)})"); var cmdAndArgs = $"{containerName}: {command} ({string.Join(",", args)})";
log.Debug(cmdAndArgs);
var runner = new CommandRunner(client, K8sTestNamespace, pod, containerName, command, args); var runner = new CommandRunner(client, K8sTestNamespace, pod, containerName, command, args);
runner.Run(); runner.Run();
return runner.GetStdOut(); var result = runner.GetStdOut();
log.Debug($"{cmdAndArgs} = '{result}'");
return result;
} }
public void DeleteAllResources() public void DeleteAllResources()
@ -102,6 +107,42 @@ namespace KubernetesWorkflow
} }
} }
#region Discover K8s Nodes
private void DiscoverK8sNodes()
{
if (cluster.AvailableK8sNodes == null || !cluster.AvailableK8sNodes.Any())
{
cluster.AvailableK8sNodes = GetAvailableK8sNodes();
if (cluster.AvailableK8sNodes.Length < 3)
{
log.Debug($"Warning: For full location support, at least 3 Kubernetes Nodes are required in the cluster. Nodes found: '{string.Join(",", cluster.AvailableK8sNodes.Select(p => $"{p.Key}={p.Value}"))}'.");
}
}
}
private K8sNodeLabel[] GetAvailableK8sNodes()
{
var nodes = client.Run(c => c.ListNode());
var optionals = nodes.Items.Select(i => CreateNodeLabel(i));
return optionals.Where(n => n != null).Select(n => n!).ToArray();
}
private K8sNodeLabel? CreateNodeLabel(V1Node i)
{
var keys = i.Metadata.Labels.Keys;
var hostnameKey = keys.SingleOrDefault(k => k.ToLowerInvariant().Contains("hostname"));
if (hostnameKey != null)
{
var hostnameValue = i.Metadata.Labels[hostnameKey];
return new K8sNodeLabel(hostnameKey, hostnameValue);
}
return null;
}
#endregion
#region Namespace management #region Namespace management
private string K8sTestNamespace { get; } private string K8sTestNamespace { get; }
@ -148,10 +189,7 @@ namespace KubernetesWorkflow
}, },
Spec = new V1NetworkPolicySpec Spec = new V1NetworkPolicySpec
{ {
PodSelector = new V1LabelSelector PodSelector = new V1LabelSelector {},
{
MatchLabels = GetSelector()
},
PolicyTypes = new[] PolicyTypes = new[]
{ {
"Ingress", "Ingress",
@ -159,6 +197,16 @@ namespace KubernetesWorkflow
}, },
Ingress = new List<V1NetworkPolicyIngressRule> Ingress = new List<V1NetworkPolicyIngressRule>
{ {
new V1NetworkPolicyIngressRule
{
FromProperty = new List<V1NetworkPolicyPeer>
{
new V1NetworkPolicyPeer
{
PodSelector = new V1LabelSelector {}
}
}
},
new V1NetworkPolicyIngressRule new V1NetworkPolicyIngressRule
{ {
FromProperty = new List<V1NetworkPolicyPeer> FromProperty = new List<V1NetworkPolicyPeer>
@ -167,7 +215,7 @@ namespace KubernetesWorkflow
{ {
NamespaceSelector = new V1LabelSelector NamespaceSelector = new V1LabelSelector
{ {
MatchLabels = GetMyNamespaceSelector() MatchLabels = GetRunnerNamespaceSelector()
} }
} }
} }
@ -175,6 +223,16 @@ namespace KubernetesWorkflow
}, },
Egress = new List<V1NetworkPolicyEgressRule> Egress = new List<V1NetworkPolicyEgressRule>
{ {
new V1NetworkPolicyEgressRule
{
To = new List<V1NetworkPolicyPeer>
{
new V1NetworkPolicyPeer
{
PodSelector = new V1LabelSelector {}
}
}
},
new V1NetworkPolicyEgressRule new V1NetworkPolicyEgressRule
{ {
To = new List<V1NetworkPolicyPeer> To = new List<V1NetworkPolicyPeer>
@ -183,11 +241,62 @@ namespace KubernetesWorkflow
{ {
NamespaceSelector = new V1LabelSelector NamespaceSelector = new V1LabelSelector
{ {
MatchLabels = GetMyNamespaceSelector() MatchLabels = new Dictionary<string, string> { { "kubernetes.io/metadata.name", "kube-system" } }
} }
},
new V1NetworkPolicyPeer
{
PodSelector = new V1LabelSelector
{
MatchLabels = new Dictionary<string, string> { { "k8s-app", "kube-dns" } }
}
}
},
Ports = new List<V1NetworkPolicyPort>
{
new V1NetworkPolicyPort
{
Port = new IntstrIntOrString
{
Value = "53"
},
Protocol = "UDP"
}
}
},
new V1NetworkPolicyEgressRule
{
To = new List<V1NetworkPolicyPeer>
{
new V1NetworkPolicyPeer
{
IpBlock = new V1IPBlock
{
Cidr = "0.0.0.0/0"
}
}
},
Ports = new List<V1NetworkPolicyPort>
{
new V1NetworkPolicyPort
{
Port = new IntstrIntOrString
{
Value = "80"
},
Protocol = "TCP"
},
new V1NetworkPolicyPort
{
Port = new IntstrIntOrString
{
Value = "443"
},
Protocol = "TCP"
} }
} }
} }
} }
} }
}; };
@ -242,11 +351,12 @@ namespace KubernetesWorkflow
private IDictionary<string, string> CreateNodeSelector(Location location) private IDictionary<string, string> CreateNodeSelector(Location location)
{ {
if (location == Location.Unspecified) return new Dictionary<string, string>(); var nodeLabel = cluster.GetNodeLabelForLocation(location);
if (nodeLabel == null) return new Dictionary<string, string>();
return new Dictionary<string, string> return new Dictionary<string, string>
{ {
{ "codex-test-location", cluster.GetNodeLabelForLocation(location) } { nodeLabel.Key, nodeLabel.Value }
}; };
} }
@ -255,9 +365,9 @@ namespace KubernetesWorkflow
return new Dictionary<string, string> { { "codex-test-node", "dist-test-" + workflowNumberSource.WorkflowNumber } }; return new Dictionary<string, string> { { "codex-test-node", "dist-test-" + workflowNumberSource.WorkflowNumber } };
} }
private IDictionary<string, string> GetMyNamespaceSelector() private IDictionary<string, string> GetRunnerNamespaceSelector()
{ {
return new Dictionary<string, string> { { "name", "thatisincorrect" } }; return new Dictionary<string, string> { { "kubernetes.io/metadata.name", "default" } };
} }
private V1ObjectMeta CreateDeploymentMetadata() private V1ObjectMeta CreateDeploymentMetadata()
@ -329,11 +439,11 @@ namespace KubernetesWorkflow
{ {
var result = new Dictionary<ContainerRecipe, Port[]>(); var result = new Dictionary<ContainerRecipe, Port[]>();
var ports = CreateServicePorts(result, containerRecipes); var ports = CreateServicePorts(containerRecipes);
if (!ports.Any()) if (!ports.Any())
{ {
// None of these container-recipes wish to expose anything via a serice port. // None of these container-recipes wish to expose anything via a service port.
// So, we don't have to create a service. // So, we don't have to create a service.
return (string.Empty, result); return (string.Empty, result);
} }
@ -352,9 +462,40 @@ namespace KubernetesWorkflow
client.Run(c => c.CreateNamespacedService(serviceSpec, K8sTestNamespace)); client.Run(c => c.CreateNamespacedService(serviceSpec, K8sTestNamespace));
ReadBackServiceAndMapPorts(serviceSpec, containerRecipes, result);
return (serviceSpec.Metadata.Name, result); return (serviceSpec.Metadata.Name, result);
} }
private void ReadBackServiceAndMapPorts(V1Service serviceSpec, ContainerRecipe[] containerRecipes, Dictionary<ContainerRecipe, Port[]> result)
{
// For each container-recipe, we need to figure out which service-ports it was assigned by K8s.
var readback = client.Run(c => c.ReadNamespacedService(serviceSpec.Metadata.Name, K8sTestNamespace));
foreach (var r in containerRecipes)
{
if (r.ExposedPorts.Any())
{
var firstExposedPort = r.ExposedPorts.First();
var portName = GetNameForPort(r, firstExposedPort);
var matchingServicePorts = readback.Spec.Ports.Where(p => p.Name == portName);
if (matchingServicePorts.Any())
{
// These service ports belongs to this recipe.
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, portName));
var ports = optionals.Where(p => p != null).Select(p => p!).ToArray();
result.Add(r, ports);
}
}
}
}
private Port? MapNodePortIfAble(V1ServicePort p, string tag)
{
if (p.NodePort == null) return null;
return new Port(p.NodePort.Value, tag);
}
private void DeleteService(string serviceName) private void DeleteService(string serviceName)
{ {
client.Run(c => c.DeleteNamespacedService(serviceName, K8sTestNamespace)); client.Run(c => c.DeleteNamespacedService(serviceName, K8sTestNamespace));
@ -369,36 +510,30 @@ namespace KubernetesWorkflow
}; };
} }
private List<V1ServicePort> CreateServicePorts(Dictionary<ContainerRecipe, Port[]> servicePorts, ContainerRecipe[] recipes) private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes)
{ {
var result = new List<V1ServicePort>(); var result = new List<V1ServicePort>();
foreach (var recipe in recipes) foreach (var recipe in recipes)
{ {
result.AddRange(CreateServicePorts(servicePorts, recipe)); result.AddRange(CreateServicePorts(recipe));
} }
return result; return result;
} }
private List<V1ServicePort> CreateServicePorts(Dictionary<ContainerRecipe, Port[]> servicePorts, ContainerRecipe recipe) private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe)
{ {
var result = new List<V1ServicePort>(); var result = new List<V1ServicePort>();
var usedPorts = new List<Port>();
foreach (var port in recipe.ExposedPorts) foreach (var port in recipe.ExposedPorts)
{ {
var servicePort = workflowNumberSource.GetServicePort();
usedPorts.Add(new Port(servicePort, ""));
result.Add(new V1ServicePort result.Add(new V1ServicePort
{ {
Name = GetNameForPort(recipe, port), Name = GetNameForPort(recipe, port),
Protocol = "TCP", Protocol = "TCP",
Port = port.Number, Port = port.Number,
TargetPort = GetNameForPort(recipe, port), TargetPort = GetNameForPort(recipe, port),
NodePort = servicePort
}); });
} }
servicePorts.Add(recipe, usedPorts.ToArray());
return result; return result;
} }
@ -465,7 +600,7 @@ namespace KubernetesWorkflow
#endregion #endregion
private (string, string) FetchNewPod() private PodInfo FetchNewPod()
{ {
var pods = client.Run(c => c.ListNamespacedPod(K8sTestNamespace)).Items; var pods = client.Run(c => c.ListNamespacedPod(K8sTestNamespace)).Items;
@ -475,12 +610,13 @@ namespace KubernetesWorkflow
var newPod = newPods.Single(); var newPod = newPods.Single();
var name = newPod.Name(); var name = newPod.Name();
var ip = newPod.Status.PodIP; var ip = newPod.Status.PodIP;
var k8sNodeName = newPod.Spec.NodeName;
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure."); if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure.");
if (string.IsNullOrEmpty(ip)) throw new InvalidOperationException("Invalid pod IP received. Test infra failure."); if (string.IsNullOrEmpty(ip)) throw new InvalidOperationException("Invalid pod IP received. Test infra failure.");
knownPods.Add(name); knownPods.Add(name);
return (name, ip); return new PodInfo(name, ip, k8sNodeName);
} }
} }
} }

View File

@ -3,7 +3,8 @@
public enum Location public enum Location
{ {
Unspecified, Unspecified,
BensLaptop, One,
BensOldGamingMachine Two,
Three,
} }
} }

View File

@ -21,18 +21,22 @@
public class RunningContainer public class RunningContainer
{ {
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, StartupConfig startupConfig) public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, StartupConfig startupConfig, RunningContainerAddress clusterExternalAddress, RunningContainerAddress clusterInternalAddress)
{ {
Pod = pod; Pod = pod;
Recipe = recipe; Recipe = recipe;
ServicePorts = servicePorts; ServicePorts = servicePorts;
Name = GetContainerName(recipe, startupConfig); Name = GetContainerName(recipe, startupConfig);
ClusterExternalAddress = clusterExternalAddress;
ClusterInternalAddress = clusterInternalAddress;
} }
public string Name { get; } public string Name { get; }
public RunningPod Pod { get; } public RunningPod Pod { get; }
public ContainerRecipe Recipe { get; } public ContainerRecipe Recipe { get; }
public Port[] ServicePorts { get; } public Port[] ServicePorts { get; }
public RunningContainerAddress ClusterExternalAddress { get; }
public RunningContainerAddress ClusterInternalAddress { get; }
private string GetContainerName(ContainerRecipe recipe, StartupConfig startupConfig) private string GetContainerName(ContainerRecipe recipe, StartupConfig startupConfig)
{ {
@ -46,4 +50,16 @@
} }
} }
} }
public class RunningContainerAddress
{
public RunningContainerAddress(string host, int port)
{
Host = host;
Port = port;
}
public string Host { get; }
public int Port { get; }
}
} }

View File

@ -4,25 +4,38 @@
{ {
private readonly Dictionary<ContainerRecipe, Port[]> servicePortMap; private readonly Dictionary<ContainerRecipe, Port[]> servicePortMap;
public RunningPod(K8sCluster cluster, string name, string ip, string deploymentName, string serviceName, Dictionary<ContainerRecipe, Port[]> servicePortMap) public RunningPod(K8sCluster cluster, PodInfo podInfo, string deploymentName, string serviceName, Dictionary<ContainerRecipe, Port[]> servicePortMap)
{ {
Cluster = cluster; Cluster = cluster;
Name = name; PodInfo = podInfo;
Ip = ip;
DeploymentName = deploymentName; DeploymentName = deploymentName;
ServiceName = serviceName; ServiceName = serviceName;
this.servicePortMap = servicePortMap; this.servicePortMap = servicePortMap;
} }
public K8sCluster Cluster { get; } public K8sCluster Cluster { get; }
public string Name { get; } public PodInfo PodInfo { get; }
public string Ip { get; }
internal string DeploymentName { get; } internal string DeploymentName { get; }
internal string ServiceName { get; } internal string ServiceName { get; }
public Port[] GetServicePortsForContainerRecipe(ContainerRecipe containerRecipe) public Port[] GetServicePortsForContainerRecipe(ContainerRecipe containerRecipe)
{ {
if (!servicePortMap.ContainsKey(containerRecipe)) return Array.Empty<Port>();
return servicePortMap[containerRecipe]; return servicePortMap[containerRecipe];
} }
} }
public class PodInfo
{
public PodInfo(string podName, string podIp, string k8sNodeName)
{
Name = podName;
Ip = podIp;
K8SNodeName = k8sNodeName;
}
public string Name { get; }
public string Ip { get; }
public string K8SNodeName { get; }
}
} }

View File

@ -80,10 +80,43 @@ namespace KubernetesWorkflow
var servicePorts = runningPod.GetServicePortsForContainerRecipe(r); var servicePorts = runningPod.GetServicePortsForContainerRecipe(r);
log.Debug($"{r} -> service ports: {string.Join(",", servicePorts.Select(p => p.Number))}"); log.Debug($"{r} -> service ports: {string.Join(",", servicePorts.Select(p => p.Number))}");
return new RunningContainer(runningPod, r, servicePorts, startupConfig); return new RunningContainer(runningPod, r, servicePorts, startupConfig,
GetContainerExternalAddress(runningPod, servicePorts),
GetContainerInternalAddress(r));
}).ToArray(); }).ToArray();
} }
private RunningContainerAddress GetContainerExternalAddress(RunningPod pod, Port[] servicePorts)
{
return new RunningContainerAddress(
pod.Cluster.HostAddress,
GetServicePort(servicePorts));
}
private RunningContainerAddress GetContainerInternalAddress(ContainerRecipe recipe)
{
var serviceName = "service-" + numberSource.WorkflowNumber;
var namespaceName = cluster.Configuration.K8sNamespacePrefix + testNamespace;
var port = GetInternalPort(recipe);
return new RunningContainerAddress(
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
port);
}
private static int GetServicePort(Port[] servicePorts)
{
if (servicePorts.Any()) return servicePorts.First().Number;
return 0;
}
private static int GetInternalPort(ContainerRecipe recipe)
{
if (recipe.ExposedPorts.Any()) return recipe.ExposedPorts.First().Number;
return 0;
}
private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig) private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
{ {
log.Debug(); log.Debug();

View File

@ -16,13 +16,12 @@ namespace KubernetesWorkflow
{ {
cluster = new K8sCluster(configuration); cluster = new K8sCluster(configuration);
this.log = log; this.log = log;
testNamespace = ApplicationLifecycle.Instance.GetTestNamespace(); testNamespace = Guid.NewGuid().ToString().ToLowerInvariant();
} }
public StartupWorkflow CreateWorkflow() public StartupWorkflow CreateWorkflow()
{ {
var workflowNumberSource = new WorkflowNumberSource(numberSource.GetNextNumber(), var workflowNumberSource = new WorkflowNumberSource(numberSource.GetNextNumber(),
ApplicationLifecycle.Instance.GetServiceNumberSource(),
containerNumberSource); containerNumberSource);
return new StartupWorkflow(log, workflowNumberSource, cluster, knownPods, testNamespace); return new StartupWorkflow(log, workflowNumberSource, cluster, knownPods, testNamespace);

View File

@ -4,13 +4,11 @@ namespace KubernetesWorkflow
{ {
public class WorkflowNumberSource public class WorkflowNumberSource
{ {
private readonly NumberSource servicePortNumberSource;
private readonly NumberSource containerNumberSource; private readonly NumberSource containerNumberSource;
public WorkflowNumberSource(int workflowNumber, NumberSource servicePortNumberSource, NumberSource containerNumberSource) public WorkflowNumberSource(int workflowNumber, NumberSource containerNumberSource)
{ {
WorkflowNumber = workflowNumber; WorkflowNumber = workflowNumber;
this.servicePortNumberSource = servicePortNumberSource;
this.containerNumberSource = containerNumberSource; this.containerNumberSource = containerNumberSource;
} }
@ -20,10 +18,5 @@ namespace KubernetesWorkflow
{ {
return containerNumberSource.GetNextNumber(); return containerNumberSource.GetNextNumber();
} }
public int GetServicePort()
{
return servicePortNumberSource.GetNextNumber();
}
} }
} }

View File

@ -54,6 +54,7 @@ namespace Logging
public void AddStringReplace(string from, string to) public void AddStringReplace(string from, string to)
{ {
if (string.IsNullOrWhiteSpace(from)) return;
replacements.Add(new BaseLogStringReplacement(from, to)); replacements.Add(new BaseLogStringReplacement(from, to));
} }

View File

@ -1,7 +1,7 @@
using DistTestCore; using DistTestCore;
using NUnit.Framework; using NUnit.Framework;
namespace Tests.ParallelTests namespace TestsLong.BasicTests
{ {
[TestFixture] [TestFixture]
public class DownloadTests : DistTest public class DownloadTests : DistTest
@ -23,7 +23,7 @@ namespace Tests.ParallelTests
var testFile = GenerateTestFile(filesizeMb.MB()); var testFile = GenerateTestFile(filesizeMb.MB());
var contentId = host.UploadFile(testFile); var contentId = host.UploadFile(testFile);
var list = new List<Task<TestFile?>>(); var list = new List<Task<TestFile?>>();
foreach (var node in group) foreach (var node in group)
{ {
list.Add(Task.Run(() => { return node.DownloadContent(contentId); })); list.Add(Task.Run(() => { return node.DownloadContent(contentId); }));

View File

@ -1,5 +1,4 @@
using DistTestCore; using DistTestCore;
using DistTestCore.Codex;
using NUnit.Framework; using NUnit.Framework;
namespace TestsLong.BasicTests namespace TestsLong.BasicTests
@ -11,7 +10,6 @@ namespace TestsLong.BasicTests
public void OneClientLargeFileTest() public void OneClientLargeFileTest()
{ {
var primary = SetupCodexNode(s => s var primary = SetupCodexNode(s => s
.WithLogLevel(CodexLogLevel.Warn)
.WithStorageQuota(20.GB())); .WithStorageQuota(20.GB()));
var testFile = GenerateTestFile(10.GB()); var testFile = GenerateTestFile(10.GB());

View File

@ -1,5 +1,4 @@
using DistTestCore; using DistTestCore;
using DistTestCore.Codex;
using NUnit.Framework; using NUnit.Framework;
namespace TestsLong.BasicTests namespace TestsLong.BasicTests
@ -32,7 +31,6 @@ namespace TestsLong.BasicTests
public void DownloadConsistencyTest() public void DownloadConsistencyTest()
{ {
var primary = SetupCodexNode(s => s var primary = SetupCodexNode(s => s
.WithLogLevel(CodexLogLevel.Trace)
.WithStorageQuota(2.MB())); .WithStorageQuota(2.MB()));
var testFile = GenerateTestFile(1.MB()); var testFile = GenerateTestFile(1.MB());

View File

@ -1,7 +1,7 @@
using DistTestCore; using DistTestCore;
using NUnit.Framework; using NUnit.Framework;
namespace Tests.ParallelTests namespace TestsLong.BasicTests
{ {
[TestFixture] [TestFixture]
public class UploadTests : DistTest public class UploadTests : DistTest

View File

@ -131,7 +131,7 @@ namespace NethereumWorkflow
public class MintTokensFunction : FunctionMessage public class MintTokensFunction : FunctionMessage
{ {
[Parameter("address", "holder", 1)] [Parameter("address", "holder", 1)]
public string Holder { get; set; } public string Holder { get; set; } = string.Empty;
[Parameter("uint256", "amount", 2)] [Parameter("uint256", "amount", 2)]
public BigInteger Amount { get; set; } public BigInteger Amount { get; set; }
@ -141,6 +141,6 @@ namespace NethereumWorkflow
public class GetTokenBalanceFunction : FunctionMessage public class GetTokenBalanceFunction : FunctionMessage
{ {
[Parameter("address", "owner", 1)] [Parameter("address", "owner", 1)]
public string Owner { get; set; } public string Owner { get; set; } = string.Empty;
} }
} }

View File

@ -26,7 +26,7 @@ namespace NethereumWorkflow
private Web3 CreateWeb3() private Web3 CreateWeb3()
{ {
var account = new Nethereum.Web3.Accounts.Account(privateKey); var account = new Nethereum.Web3.Accounts.Account(privateKey);
return new Web3(account, $"http://{ip}:{port}"); return new Web3(account, $"{ip}:{port}");
} }
} }
} }

View File

@ -1,11 +1,13 @@
# Distributed System Tests for Nim-Codex # Distributed System Tests for Nim-Codex
Using a common dotnet unit-test framework and a few other libraries, this project allows you to write tests that use multiple Codex node instances in various configurations to test the distributed system in a controlled, reproducable environment.
Nim-Codex: https://github.com/status-im/nim-codex Using a common dotnet unit-test framework and a few other libraries, this project allows you to write tests that use multiple Codex node instances in various configurations to test the distributed system in a controlled, reproducible environment.
Dotnet: v6.0
Kubernetes: v1.25.4
Dotnet-kubernetes SDK: v10.1.4 https://github.com/kubernetes-client/csharp Nim-Codex: https://github.com/codex-storage/nim-codex
Dotnet: v6.0
Kubernetes: v1.25.4
Dotnet-kubernetes SDK: v10.1.4 https://github.com/kubernetes-client/csharp
Nethereum: v4.14.0 Nethereum: v4.14.0
## Tests ## Tests
@ -15,6 +17,17 @@ Tests are devided into two assemblies: `/Tests` and `/LongTests`.
TODO: All tests will eventually be running as part of a dedicated CI pipeline and kubernetes cluster. Currently, we're developing these tests and the infra-code to support it by running the whole thing locally. TODO: All tests will eventually be running as part of a dedicated CI pipeline and kubernetes cluster. Currently, we're developing these tests and the infra-code to support it by running the whole thing locally.
## Configuration
Test executing can be configured using the following environment variables.
| Variable | Description | Default |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------|
| KUBECONFIG | Optional path (abs or rel) to kubeconfig YAML file. When null, uses system default (docker-desktop) kubeconfig if available. | (null) |
| LOGPATH | Path (abs or rel) where log files will be saved. | "CodexTestLogs" |
| LOGDEBUG | When "true", enables additional test-runner debug log output. | "false" |
| DATAFILEPATH | Path (abs or rel) where temporary test data files will be saved. | "TestDataFiles" |
| LOGLEVEL | Codex log-level. (case-insensitive) | "Trace" |
| RUNNERLOCATION | Use "ExternalToCluster" when test app is running outside of the k8s cluster. Use "InternalToCluster" when tests are run from inside a pod/container. | "ExternalToCluster" |
## Test logs ## Test logs
Because tests potentially take a long time to run, logging is in place to help you investigate failures afterwards. Should a test fail, all Codex terminal output (as well as metrics if they have been enabled) will be downloaded and stored along with a detailed, step-by-step log of the test. If something's gone wrong and you're here to discover the details, head for the logs. Because tests potentially take a long time to run, logging is in place to help you investigate failures afterwards. Should a test fail, all Codex terminal output (as well as metrics if they have been enabled) will be downloaded and stored along with a detailed, step-by-step log of the test. If something's gone wrong and you're here to discover the details, head for the logs.

View File

@ -1,5 +1,4 @@
using DistTestCore; using DistTestCore;
using DistTestCore.Codex;
using NUnit.Framework; using NUnit.Framework;
using Utils; using Utils;
@ -11,7 +10,7 @@ namespace Tests.BasicTests
[Test] [Test]
public void CodexLogExample() public void CodexLogExample()
{ {
var primary = SetupCodexNode(s => s.WithLogLevel(CodexLogLevel.Trace)); var primary = SetupCodexNode();
primary.UploadFile(GenerateTestFile(5.MB())); primary.UploadFile(GenerateTestFile(5.MB()));
@ -47,7 +46,6 @@ namespace Tests.BasicTests
var buyerInitialBalance = 1000.TestTokens(); var buyerInitialBalance = 1000.TestTokens();
var seller = SetupCodexNode(s => s var seller = SetupCodexNode(s => s
.WithLogLevel(CodexLogLevel.Trace)
.WithStorageQuota(11.GB()) .WithStorageQuota(11.GB())
.EnableMarketplace(sellerInitialBalance)); .EnableMarketplace(sellerInitialBalance));
@ -61,7 +59,6 @@ namespace Tests.BasicTests
var testFile = GenerateTestFile(10.MB()); var testFile = GenerateTestFile(10.MB());
var buyer = SetupCodexNode(s => s var buyer = SetupCodexNode(s => s
.WithLogLevel(CodexLogLevel.Trace)
.WithBootstrapNode(seller) .WithBootstrapNode(seller)
.EnableMarketplace(buyerInitialBalance)); .EnableMarketplace(buyerInitialBalance));

View File

@ -1,85 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using NUnit.Framework;
namespace Tests.BasicTests
{
[TestFixture]
public class PeerTests : DistTest
{
[Test]
public void TwoNodes()
{
var primary = SetupCodexBootstrapNode();
var secondary = SetupCodexNode(s => s.WithBootstrapNode(primary));
primary.ConnectToPeer(secondary); // TODO REMOVE THIS: This is required for the switchPeers to show up.
// This is required for the enginePeers to show up.
//var file = GenerateTestFile(10.MB());
//var contentId = primary.UploadFile(file);
//var file2 = secondary.DownloadContent(contentId);
//file.AssertIsEqual(file2);
AssertKnowEachother(primary, secondary);
}
[TestCase(2)]
[TestCase(3)]
[TestCase(10)]
public void VariableNodes(int number)
{
var bootstrap = SetupCodexBootstrapNode();
var nodes = SetupCodexNodes(number, s => s.WithBootstrapNode(bootstrap));
var file = GenerateTestFile(10.MB());
var contentId = nodes.First().UploadFile(file);
var file2 = nodes.Last().DownloadContent(contentId);
file.AssertIsEqual(file2);
// <TODO REMOVE THIS>
foreach (var node in nodes) bootstrap.ConnectToPeer(node);
for (var x = 0; x < number; x++)
{
for (var y = x + 1; y < number; y++)
{
nodes[x].ConnectToPeer(nodes[y]);
}
}
// </TODO REMOVE THIS>
foreach (var node in nodes) AssertKnowEachother(node, bootstrap);
for (var x = 0; x < number; x++)
{
for (var y = x + 1; y < number; y++)
{
AssertKnowEachother(nodes[x], nodes[y]);
}
}
}
private void AssertKnowEachother(IOnlineCodexNode a, IOnlineCodexNode b)
{
AssertKnowEachother(a.GetDebugInfo(), b.GetDebugInfo());
}
private void AssertKnowEachother(CodexDebugResponse a, CodexDebugResponse b)
{
AssertKnows(a, b);
AssertKnows(b, a);
}
private void AssertKnows(CodexDebugResponse a, CodexDebugResponse b)
{
var enginePeers = string.Join(",", a.enginePeers.Select(p => p.peerId));
var switchPeers = string.Join(",", a.switchPeers.Select(p => p.peerId));
Debug($"{a.id} is looking for {b.id} in engine-peers [{enginePeers}]");
Debug($"{a.id} is looking for {b.id} in switch-peers [{switchPeers}]");
Assert.That(a.enginePeers.Any(p => p.peerId == b.id), $"{a.id} was looking for '{b.id}' in engine-peers [{enginePeers}] but it was not found.");
Assert.That(a.switchPeers.Any(p => p.peerId == b.id), $"{a.id} was looking for '{b.id}' in switch-peers [{switchPeers}] but it was not found.");
}
}
}

View File

@ -28,11 +28,10 @@ namespace Tests.BasicTests
} }
[Test] [Test]
[Ignore("Requires Location map to be configured for k8s cluster.")]
public void TwoClientsTwoLocationsTest() public void TwoClientsTwoLocationsTest()
{ {
var primary = SetupCodexNode(s => s.At(Location.BensLaptop)); var primary = SetupCodexNode(s => s.At(Location.One));
var secondary = SetupCodexNode(s => s.At(Location.BensOldGamingMachine)); var secondary = SetupCodexNode(s => s.At(Location.Two));
PerformTwoClientTest(primary, secondary); PerformTwoClientTest(primary, secondary);
} }

View File

@ -1,66 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using NUnit.Framework;
using Utils;
namespace Tests.DurabilityTests
{
[TestFixture]
public class DurabilityTests : DistTest
{
[Test]
public void BootstrapNodeDisappearsTest()
{
var bootstrapNode = SetupCodexBootstrapNode();
var group = SetupCodexNodes(2, s => s.WithBootstrapNode(bootstrapNode));
var primary = group[0];
var secondary = group[1];
// There is 1 minute of time f or the nodes to connect to each other.
// (Should be easy, they're in the same pod.)
Time.Sleep(TimeSpan.FromMinutes(6));
bootstrapNode.BringOffline();
var file = GenerateTestFile(10.MB());
var contentId = primary.UploadFile(file);
var downloadedFile = secondary.DownloadContent(contentId);
file.AssertIsEqual(downloadedFile);
}
[Test]
public void DataRetentionTest()
{
var bootstrapNode = SetupCodexBootstrapNode(s => s.WithLogLevel(CodexLogLevel.Trace));
var startGroup = SetupCodexNodes(2, s => s.WithLogLevel(CodexLogLevel.Trace).WithBootstrapNode(bootstrapNode));
var finishGroup = SetupCodexNodes(10, s => s.WithLogLevel(CodexLogLevel.Trace).WithBootstrapNode(bootstrapNode));
var file = GenerateTestFile(10.MB());
// Both nodes in the start group have the file.
var content = startGroup[0].UploadFile(file);
DownloadAndAssert(content, file, startGroup[1]);
// Three nodes of the finish group have the file.
DownloadAndAssert(content, file, finishGroup[0]);
DownloadAndAssert(content, file, finishGroup[1]);
DownloadAndAssert(content, file, finishGroup[2]);
// The start group goes away.
startGroup.BringOffline();
// All nodes in the finish group can access the file.
foreach (var node in finishGroup)
{
DownloadAndAssert(content, file, node);
}
}
private void DownloadAndAssert(ContentId content, TestFile file, IOnlineCodexNode onlineCodexNode)
{
var downloaded = onlineCodexNode.DownloadContent(content);
file.AssertIsEqual(downloaded);
}
}
}

View File

@ -0,0 +1,62 @@
using DistTestCore;
using DistTestCore.Helpers;
using NUnit.Framework;
using Utils;
namespace Tests.PeerDiscoveryTests
{
[TestFixture]
public class LayeredDiscoveryTests : DistTest
{
[Test]
public void TwoLayersTest()
{
var root = SetupCodexNode();
var l1Source = SetupCodexNode(s => s.WithBootstrapNode(root));
var l1Node = SetupCodexNode(s => s.WithBootstrapNode(root));
var l2Target = SetupCodexNode(s => s.WithBootstrapNode(l1Node));
AssertAllNodesConnected();
}
[Test]
public void ThreeLayersTest()
{
var root = SetupCodexNode();
var l1Source = SetupCodexNode(s => s.WithBootstrapNode(root));
var l1Node = SetupCodexNode(s => s.WithBootstrapNode(root));
var l2Node = SetupCodexNode(s => s.WithBootstrapNode(l1Node));
var l3Target = SetupCodexNode(s => s.WithBootstrapNode(l2Node));
AssertAllNodesConnected();
}
[TestCase(3)]
[TestCase(5)]
[TestCase(10)]
[TestCase(20)]
[TestCase(50)]
public void NodeChainTest(int chainLength)
{
var node = SetupCodexNode();
for (var i = 1; i < chainLength; i++)
{
node = SetupCodexNode(s => s.WithBootstrapNode(node));
}
AssertAllNodesConnected();
for (int i = 0; i < 5; i++)
{
Time.Sleep(TimeSpan.FromSeconds(30));
AssertAllNodesConnected();
}
}
private void AssertAllNodesConnected()
{
PeerConnectionTestHelpers.AssertFullyConnected(GetAllOnlineCodexNodes());
//PeerDownloadTestHelpers.AssertFullDownloadInterconnectivity(GetAllOnlineCodexNodes());
}
}
}

View File

@ -0,0 +1,73 @@
using DistTestCore;
using DistTestCore.Helpers;
using NUnit.Framework;
using Utils;
namespace Tests.PeerDiscoveryTests
{
[TestFixture]
public class PeerDiscoveryTests : AutoBootstrapDistTest
{
[Test]
public void CanReportUnknownPeerId()
{
var unknownId = "16Uiu2HAkv2CHWpff3dj5iuVNERAp8AGKGNgpGjPexJZHSqUstfsK";
var node = SetupCodexNode();
var result = node.GetDebugPeer(unknownId);
Assert.That(result.IsPeerFound, Is.False);
}
[TestCase(2)]
[TestCase(3)]
[TestCase(10)]
public void VariableNodes(int number)
{
SetupCodexNodes(number);
AssertAllNodesConnected();
}
[TestCase(2)]
[TestCase(3)]
[TestCase(10)]
[TestCase(20)]
public void VariableNodesInPods(int number)
{
for (var i = 0; i < number; i++)
{
SetupCodexNode();
}
AssertAllNodesConnected();
}
[TestCase(3, 3)]
[TestCase(3, 5)]
[TestCase(3, 10)]
[TestCase(5, 10)]
[TestCase(3, 20)]
[TestCase(5, 20)]
public void StagedVariableNodes(int numberOfNodes, int numberOfStages)
{
for (var i = 0; i < numberOfStages; i++)
{
SetupCodexNodes(numberOfNodes);
AssertAllNodesConnected();
}
for (int i = 0; i < 5; i++)
{
Time.Sleep(TimeSpan.FromSeconds(30));
AssertAllNodesConnected();
}
}
private void AssertAllNodesConnected()
{
PeerConnectionTestHelpers.AssertFullyConnected(GetAllOnlineCodexNodes());
//PeerDownloadTestHelpers.AssertFullDownloadInterconnectivity(GetAllOnlineCodexNodes());
}
}
}

View File

@ -22,6 +22,11 @@
result += $"{d.Seconds} secs"; result += $"{d.Seconds} secs";
return result; return result;
} }
public static void WaitUntil(Func<bool> predicate)
{
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1));
}
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryTime) public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryTime)
{ {
@ -38,5 +43,74 @@
state = predicate(); state = predicate();
} }
} }
public static void Retry(Action action, string description)
{
Retry(action, TimeSpan.FromMinutes(1), description);
}
public static T Retry<T>(Func<T> action, string description)
{
return Retry(action, TimeSpan.FromMinutes(1), description);
}
public static void Retry(Action action, TimeSpan timeout, string description)
{
Retry(action, timeout, TimeSpan.FromSeconds(1), description);
}
public static T Retry<T>(Func<T> action, TimeSpan timeout, string description)
{
return Retry(action, timeout, TimeSpan.FromSeconds(1), description);
}
public static void Retry(Action action, TimeSpan timeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var exceptions = new List<Exception>();
while (true)
{
if (DateTime.UtcNow - start > timeout)
{
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
}
try
{
action();
return;
}
catch (Exception ex)
{
exceptions.Add(ex);
}
Sleep(retryTime);
}
}
public static T Retry<T>(Func<T> action, TimeSpan timeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var exceptions = new List<Exception>();
while (true)
{
if (DateTime.UtcNow - start > timeout)
{
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
}
try
{
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
Sleep(retryTime);
}
}
} }
} }

View File

@ -24,7 +24,7 @@ spec:
spec: spec:
containers: containers:
- name: codex-node1 - name: codex-node1
image: thatbenbierens/nim-codex:sha-b204837 image: codexstorage/nim-codex:sha-7b88ea0
ports: ports:
- containerPort: 8080 - containerPort: 8080
name: api-1 name: api-1
@ -38,7 +38,7 @@ spec:
- name: LISTEN_ADDRS - name: LISTEN_ADDRS
value: "/ip4/0.0.0.0/tcp/8082" value: "/ip4/0.0.0.0/tcp/8082"
- name: codex-node2 - name: codex-node2
image: thatbenbierens/nim-codex:sha-b204837 image: codexstorage/nim-codex:sha-7b88ea0
ports: ports:
- containerPort: 8083 - containerPort: 8083
name: api-2 name: api-2

48
docker/job.yaml Normal file
View File

@ -0,0 +1,48 @@
apiVersion: batch/v1
kind: Job
metadata:
name: ${NAMEPREFIX}-${RUNID}
namespace: ${NAMESPACE}
labels:
name: ${NAMEPREFIX}-${RUNID}
run-id: ${RUNID}
spec:
backoffLimit: 0
template:
metadata:
name: ${NAMEPREFIX}
spec:
containers:
- name: ${NAMEPREFIX}-runner
image: codexstorage/cs-codex-dist-tests:sha-300b91e
env:
- name: RUNNERLOCATION
value: InternalToCluster
- name: KUBECONFIG
value: /opt/kubeconfig.yaml
- name: LOGPATH
value: /var/log/cs-codex-dist-tests
- name: NAMESPACE
value: ${NAMESPACE}
- name: BRANCH
value: ${BRANCH}
- name: SOURCE
value: ${SOURCE}
volumeMounts:
- name: kubeconfig
mountPath: /opt/kubeconfig.yaml
subPath: kubeconfig.yaml
- name: logs
mountPath: /var/log/cs-codex-dist-tests
# command:
# - "dotnet"
# - "test"
# - "Tests"
restartPolicy: Never
volumes:
- name: kubeconfig
secret:
secretName: cs-codex-dist-tests-app-kubeconfig
- name: logs
hostPath:
path: /var/log/cs-codex-dist-tests