Initial setup for continuous test runner
This commit is contained in:
parent
62d646836d
commit
d23f5aa29d
|
@ -0,0 +1,78 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace ContinuousTests
|
||||||
|
{
|
||||||
|
public class Configuration
|
||||||
|
{
|
||||||
|
public string LogPath { get; set; } = string.Empty;
|
||||||
|
public string[] CodexUrls { get; set; } = Array.Empty<string>();
|
||||||
|
public int SleepSecondsPerTest { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConfigLoader
|
||||||
|
{
|
||||||
|
private const string filename = "config.json";
|
||||||
|
|
||||||
|
public Configuration Load()
|
||||||
|
{
|
||||||
|
var config = Read();
|
||||||
|
Validate(config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Configuration Read()
|
||||||
|
{
|
||||||
|
if (File.Exists(filename))
|
||||||
|
{
|
||||||
|
var lines = File.ReadAllText(filename);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = JsonConvert.DeserializeObject<Configuration>(lines);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
var logPath = Environment.GetEnvironmentVariable("LOGPATH");
|
||||||
|
var codexUrls = Environment.GetEnvironmentVariable("CODEXURLS");
|
||||||
|
var sleep = Environment.GetEnvironmentVariable("SLEEPSECONDSPERTEST");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(logPath) && !string.IsNullOrEmpty(codexUrls) && !string.IsNullOrEmpty(sleep))
|
||||||
|
{
|
||||||
|
var urls = codexUrls.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var ms = 0;
|
||||||
|
if (int.TryParse(sleep, out ms))
|
||||||
|
{
|
||||||
|
if (urls.Length > 0)
|
||||||
|
{
|
||||||
|
return new Configuration { LogPath = logPath, CodexUrls = urls, SleepSecondsPerTest = ms };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"Unable to load configuration from '{filename}', and " +
|
||||||
|
$"unable to load configuration from environment variables 'LOGPATH' and 'CODEXURLS', and 'SLEEPSECONDSPERTEST'. " +
|
||||||
|
$"(semi-colon-separated URLs) " +
|
||||||
|
$"Create the configuration file or set the environment veriables.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Validate(Configuration configuration)
|
||||||
|
{
|
||||||
|
if (configuration.SleepSecondsPerTest < 10)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Warning: configuration.SleepMsPerTest was less than 10 seconds. Using 10 seconds instead!");
|
||||||
|
configuration.SleepSecondsPerTest = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(configuration.LogPath))
|
||||||
|
{
|
||||||
|
throw new Exception($"Unvalid logpath set: '{configuration.LogPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configuration.CodexUrls.Any())
|
||||||
|
{
|
||||||
|
throw new Exception("No Codex URLs found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
using DistTestCore;
|
||||||
|
using DistTestCore.Codex;
|
||||||
|
using Logging;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
|
namespace ContinuousTests
|
||||||
|
{
|
||||||
|
public class ContinuousTestRunner
|
||||||
|
{
|
||||||
|
private readonly ConfigLoader configLoader = new ConfigLoader();
|
||||||
|
private readonly TestFinder testFinder = new TestFinder();
|
||||||
|
|
||||||
|
public void Run()
|
||||||
|
{
|
||||||
|
var config = configLoader.Load();
|
||||||
|
var log = new TestLog(config.LogPath, true);
|
||||||
|
|
||||||
|
log.Log("Starting continuous test run...");
|
||||||
|
log.Log("Checking configuration...");
|
||||||
|
PreflightCheck(config);
|
||||||
|
log.Log("Contacting Codex nodes...");
|
||||||
|
var nodes = CreateCodexNodes(log, new LongTimeSet(), config);
|
||||||
|
log.Log("OK");
|
||||||
|
log.Log("");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var run = new TestRun(config, log, testFinder, nodes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
run.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"Exception during test run: " + ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(config.SleepSecondsPerTest * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreflightCheck(Configuration config)
|
||||||
|
{
|
||||||
|
var tests = testFinder.GetTests();
|
||||||
|
if (!tests.Any())
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to find any tests.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
foreach (var test in tests)
|
||||||
|
{
|
||||||
|
if (test.RequiredNumberOfNodes > config.CodexUrls.Length)
|
||||||
|
{
|
||||||
|
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Configuration only has {config.CodexUrls.Length}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
throw new Exception("Prerun check failed: " + string.Join(", ", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CodexNode[] CreateCodexNodes(BaseLog log, ITimeSet timeSet, Configuration config)
|
||||||
|
{
|
||||||
|
var nodes = config.CodexUrls.Select(url =>
|
||||||
|
{
|
||||||
|
var address = new Address(url, 1234);
|
||||||
|
return new CodexNode(log, timeSet, address);
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
var pass = true;
|
||||||
|
foreach (var n in nodes)
|
||||||
|
{
|
||||||
|
log.Log($"Checking '{n.Address.Host}'...");
|
||||||
|
|
||||||
|
if (EnsureOnline(n))
|
||||||
|
{
|
||||||
|
log.Log("OK");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Error($"No response from '{n.Address.Host}'.");
|
||||||
|
pass = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!pass)
|
||||||
|
{
|
||||||
|
throw new Exception("Not all codex nodes responded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureOnline(CodexNode n)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = n.GetDebugInfo();
|
||||||
|
if (info == null || string.IsNullOrEmpty(info.id)) return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DistTestCore\DistTestCore.csproj" />
|
||||||
|
<ProjectReference Include="..\Logging\Logging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,33 @@
|
||||||
|
using DistTestCore;
|
||||||
|
using DistTestCore.Codex;
|
||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace ContinuousTests
|
||||||
|
{
|
||||||
|
public interface IContinuousTest
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
int RequiredNumberOfNodes { get; }
|
||||||
|
|
||||||
|
void Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ContinuousTest : IContinuousTest
|
||||||
|
{
|
||||||
|
public CodexNode[] Nodes { get; set; } = null!;
|
||||||
|
public BaseLog Log { get; set; } = null!;
|
||||||
|
public FileManager FileManager { get; set; } = null!;
|
||||||
|
|
||||||
|
public abstract int RequiredNumberOfNodes { get; }
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return GetType().Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Run();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
using ContinuousTests;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var runner = new ContinuousTestRunner();
|
||||||
|
runner.Run();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace ContinuousTests
|
||||||
|
{
|
||||||
|
public class TestFinder
|
||||||
|
{
|
||||||
|
private readonly List<IContinuousTest> testList = new List<IContinuousTest>();
|
||||||
|
|
||||||
|
public IContinuousTest[] GetTests()
|
||||||
|
{
|
||||||
|
if (!testList.Any()) FindTests();
|
||||||
|
return testList.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FindTests()
|
||||||
|
{
|
||||||
|
var types = GetType().Assembly.GetTypes();
|
||||||
|
var testTypes = types.Where(t => typeof(IContinuousTest).IsAssignableFrom(t) && !t.IsAbstract);
|
||||||
|
foreach (var testType in testTypes)
|
||||||
|
{
|
||||||
|
var t = Activator.CreateInstance(testType);
|
||||||
|
testList.Add((IContinuousTest)t!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
using DistTestCore;
|
||||||
|
using DistTestCore.Codex;
|
||||||
|
using Logging;
|
||||||
|
|
||||||
|
namespace ContinuousTests
|
||||||
|
{
|
||||||
|
public class TestRun
|
||||||
|
{
|
||||||
|
private readonly Random random = new Random();
|
||||||
|
private readonly Configuration config;
|
||||||
|
private readonly BaseLog log;
|
||||||
|
private readonly TestFinder testFinder;
|
||||||
|
private readonly CodexNode[] nodes;
|
||||||
|
private readonly FileManager fileManager;
|
||||||
|
|
||||||
|
public TestRun(Configuration config, BaseLog log, TestFinder testFinder, CodexNode[] nodes)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
this.log = log;
|
||||||
|
this.testFinder = testFinder;
|
||||||
|
this.nodes = nodes;
|
||||||
|
fileManager = new FileManager(log, new DistTestCore.Configuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run()
|
||||||
|
{
|
||||||
|
var remainingTests = testFinder.GetTests().ToList();
|
||||||
|
while (remainingTests.Any())
|
||||||
|
{
|
||||||
|
var test = PickOneRandom(remainingTests);
|
||||||
|
var selectedNodes = SelectRandomNodes(test.RequiredNumberOfNodes);
|
||||||
|
AssignEssentials(test, selectedNodes);
|
||||||
|
fileManager.PushFileSet();
|
||||||
|
|
||||||
|
log.Log($"Start '{test.Name}'");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
test.Run();
|
||||||
|
log.Log($"'{test.Name}' = Passed");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
log.Log($"'{test.Name}' = Failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fileManager.PopFileSet();
|
||||||
|
ClearEssentials(test);
|
||||||
|
Thread.Sleep(config.SleepSecondsPerTest * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssignEssentials(IContinuousTest test, CodexNode[] nodes)
|
||||||
|
{
|
||||||
|
var t = (ContinuousTest)test;
|
||||||
|
t.Nodes = nodes;
|
||||||
|
t.Log = log;
|
||||||
|
t.FileManager = fileManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearEssentials(IContinuousTest test)
|
||||||
|
{
|
||||||
|
var t = (ContinuousTest)test;
|
||||||
|
t.Nodes = null!;
|
||||||
|
t.Log = null!;
|
||||||
|
t.FileManager = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CodexNode[] SelectRandomNodes(int number)
|
||||||
|
{
|
||||||
|
var remainingNodes = nodes.ToList();
|
||||||
|
var result = new CodexNode[number];
|
||||||
|
for (var i = 0; i < number; i++)
|
||||||
|
{
|
||||||
|
result[i] = PickOneRandom(remainingNodes);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T PickOneRandom<T>(List<T> remainingItems)
|
||||||
|
{
|
||||||
|
var i = random.Next(0, remainingItems.Count);
|
||||||
|
var result = remainingItems[i];
|
||||||
|
remainingItems.RemoveAt(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,68 +10,19 @@ namespace DistTestCore.Codex
|
||||||
{
|
{
|
||||||
this.lifecycle = lifecycle;
|
this.lifecycle = lifecycle;
|
||||||
Container = runningContainer;
|
Container = runningContainer;
|
||||||
|
|
||||||
|
var address = lifecycle.Configuration.GetAddress(Container);
|
||||||
|
Node = new CodexNode(lifecycle.Log, lifecycle.TimeSet, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RunningContainer Container { get; }
|
public RunningContainer Container { get; }
|
||||||
|
public CodexNode Node { get; }
|
||||||
public CodexDebugResponse GetDebugInfo()
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
return Http().HttpPostStream("upload", fileStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream DownloadFile(string contentId)
|
|
||||||
{
|
|
||||||
return Http().HttpGetStream("download/" + contentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request)
|
|
||||||
{
|
|
||||||
return Http().HttpPostJson<CodexSalesAvailabilityRequest, CodexSalesAvailabilityResponse>("sales/availability", request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId)
|
|
||||||
{
|
|
||||||
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
|
||||||
{
|
{
|
||||||
var debugInfo = GetDebugInfo();
|
var debugInfo = Node.GetDebugInfo();
|
||||||
if (debugInfo == null || string.IsNullOrEmpty(debugInfo.id)) throw new InvalidOperationException("Unable to get debug-info from codex node at startup.");
|
if (debugInfo == null || string.IsNullOrEmpty(debugInfo.id)) throw new InvalidOperationException("Unable to get debug-info from codex node at startup.");
|
||||||
|
|
||||||
var nodePeerId = debugInfo.id;
|
var nodePeerId = debugInfo.id;
|
||||||
|
@ -85,106 +36,5 @@ namespace DistTestCore.Codex
|
||||||
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(TimeSpan? timeoutOverride = null)
|
|
||||||
{
|
|
||||||
var address = lifecycle.Configuration.GetAddress(Container);
|
|
||||||
return new Http(lifecycle.Log, lifecycle.TimeSet, address, baseUrl: "/api/codex/v1", timeoutOverride);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CodexDebugResponse
|
|
||||||
{
|
|
||||||
public string id { get; set; } = string.Empty;
|
|
||||||
public string[] addrs { get; set; } = new string[0];
|
|
||||||
public string repo { get; set; } = string.Empty;
|
|
||||||
public string spr { get; set; } = string.Empty;
|
|
||||||
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
|
|
||||||
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
|
|
||||||
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 string peerId { get; set; } = string.Empty;
|
|
||||||
public EnginePeerContextResponse context { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EnginePeerContextResponse
|
|
||||||
{
|
|
||||||
public int blocks { get; set; } = 0;
|
|
||||||
public int peerWants { get; set; } = 0;
|
|
||||||
public int exchanged { get; set; } = 0;
|
|
||||||
public string lastExchange { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SwitchPeerResponse
|
|
||||||
{
|
|
||||||
public string peerId { get; set; } = string.Empty;
|
|
||||||
public string key { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CodexDebugVersionResponse
|
|
||||||
{
|
|
||||||
public string version { 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 string size { get; set; } = string.Empty;
|
|
||||||
public string duration { get; set; } = string.Empty;
|
|
||||||
public string minPrice { get; set; } = string.Empty;
|
|
||||||
public string maxCollateral { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CodexSalesAvailabilityResponse
|
|
||||||
{
|
|
||||||
public string id { get; set; } = string.Empty;
|
|
||||||
public string size { get; set; } = string.Empty;
|
|
||||||
public string duration { get; set; } = string.Empty;
|
|
||||||
public string minPrice { get; set; } = string.Empty;
|
|
||||||
public string maxCollateral { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CodexSalesRequestStorageRequest
|
|
||||||
{
|
|
||||||
public string duration { get; set; } = string.Empty;
|
|
||||||
public string proofProbability { get; set; } = string.Empty;
|
|
||||||
public string reward { get; set; } = string.Empty;
|
|
||||||
public string collateral { get; set; } = string.Empty;
|
|
||||||
public string? expiry { get; set; }
|
|
||||||
public uint? nodes { get; set; }
|
|
||||||
public uint? tolerance { get; set;}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
using Logging;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
|
namespace DistTestCore.Codex
|
||||||
|
{
|
||||||
|
public class CodexNode
|
||||||
|
{
|
||||||
|
private readonly BaseLog log;
|
||||||
|
private readonly ITimeSet timeSet;
|
||||||
|
|
||||||
|
public CodexNode(BaseLog log, ITimeSet timeSet, Address address)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.timeSet = timeSet;
|
||||||
|
Address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Address Address { get; }
|
||||||
|
|
||||||
|
public CodexDebugResponse GetDebugInfo()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return Http().HttpPostStream("upload", fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream DownloadFile(string contentId)
|
||||||
|
{
|
||||||
|
return Http().HttpGetStream("download/" + contentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request)
|
||||||
|
{
|
||||||
|
return Http().HttpPostJson<CodexSalesAvailabilityRequest, CodexSalesAvailabilityResponse>("sales/availability", request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId)
|
||||||
|
{
|
||||||
|
return Http().HttpPostJson($"storage/request/{contentId}", request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConnectToPeer(string peerId, string peerMultiAddress)
|
||||||
|
{
|
||||||
|
return Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Http Http(TimeSpan? timeoutOverride = null)
|
||||||
|
{
|
||||||
|
return new Http(log, timeSet, Address, baseUrl: "/api/codex/v1", timeoutOverride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CodexDebugResponse
|
||||||
|
{
|
||||||
|
public string id { get; set; } = string.Empty;
|
||||||
|
public string[] addrs { get; set; } = new string[0];
|
||||||
|
public string repo { get; set; } = string.Empty;
|
||||||
|
public string spr { get; set; } = string.Empty;
|
||||||
|
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
|
||||||
|
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
|
||||||
|
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 string peerId { get; set; } = string.Empty;
|
||||||
|
public EnginePeerContextResponse context { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnginePeerContextResponse
|
||||||
|
{
|
||||||
|
public int blocks { get; set; } = 0;
|
||||||
|
public int peerWants { get; set; } = 0;
|
||||||
|
public int exchanged { get; set; } = 0;
|
||||||
|
public string lastExchange { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SwitchPeerResponse
|
||||||
|
{
|
||||||
|
public string peerId { get; set; } = string.Empty;
|
||||||
|
public string key { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CodexDebugVersionResponse
|
||||||
|
{
|
||||||
|
public string version { 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 string size { get; set; } = string.Empty;
|
||||||
|
public string duration { get; set; } = string.Empty;
|
||||||
|
public string minPrice { get; set; } = string.Empty;
|
||||||
|
public string maxCollateral { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CodexSalesAvailabilityResponse
|
||||||
|
{
|
||||||
|
public string id { get; set; } = string.Empty;
|
||||||
|
public string size { get; set; } = string.Empty;
|
||||||
|
public string duration { get; set; } = string.Empty;
|
||||||
|
public string minPrice { get; set; } = string.Empty;
|
||||||
|
public string maxCollateral { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CodexSalesRequestStorageRequest
|
||||||
|
{
|
||||||
|
public string duration { get; set; } = string.Empty;
|
||||||
|
public string proofProbability { get; set; } = string.Empty;
|
||||||
|
public string reward { get; set; } = string.Empty;
|
||||||
|
public string collateral { get; set; } = string.Empty;
|
||||||
|
public string? expiry { get; set; }
|
||||||
|
public uint? nodes { get; set; }
|
||||||
|
public uint? tolerance { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using DistTestCore.Codex;
|
using DistTestCore.Codex;
|
||||||
using KubernetesWorkflow;
|
using KubernetesWorkflow;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
namespace DistTestCore
|
namespace DistTestCore
|
||||||
{
|
{
|
||||||
|
@ -52,7 +53,7 @@ namespace DistTestCore
|
||||||
return runnerLocation;
|
return runnerLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RunningContainerAddress GetAddress(RunningContainer container)
|
public Address GetAddress(RunningContainer container)
|
||||||
{
|
{
|
||||||
if (GetTestRunnerLocation() == TestRunnerLocation.InternalToCluster)
|
if (GetTestRunnerLocation() == TestRunnerLocation.InternalToCluster)
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,11 +19,11 @@ namespace DistTestCore
|
||||||
public const int ChunkSize = 1024 * 1024 * 100;
|
public const int ChunkSize = 1024 * 1024 * 100;
|
||||||
private static NumberSource folderNumberSource = new NumberSource(0);
|
private static NumberSource folderNumberSource = new NumberSource(0);
|
||||||
private readonly Random random = new Random();
|
private readonly Random random = new Random();
|
||||||
private readonly TestLog log;
|
private readonly BaseLog log;
|
||||||
private readonly string folder;
|
private readonly string folder;
|
||||||
private readonly List<List<TestFile>> fileSetStack = new List<List<TestFile>>();
|
private readonly List<List<TestFile>> fileSetStack = new List<List<TestFile>>();
|
||||||
|
|
||||||
public FileManager(TestLog log, Configuration configuration)
|
public FileManager(BaseLog log, Configuration configuration)
|
||||||
{
|
{
|
||||||
folder = Path.Combine(configuration.GetFileManagerFolder(), folderNumberSource.GetNextNumber().ToString("D5"));
|
folder = Path.Combine(configuration.GetFileManagerFolder(), folderNumberSource.GetNextNumber().ToString("D5"));
|
||||||
|
|
||||||
|
@ -142,9 +142,9 @@ namespace DistTestCore
|
||||||
|
|
||||||
public class TestFile
|
public class TestFile
|
||||||
{
|
{
|
||||||
private readonly TestLog log;
|
private readonly BaseLog log;
|
||||||
|
|
||||||
public TestFile(TestLog log, string filename, string label)
|
public TestFile(BaseLog log, string filename, string label)
|
||||||
{
|
{
|
||||||
this.log = log;
|
this.log = log;
|
||||||
Filename = filename;
|
Filename = filename;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using KubernetesWorkflow;
|
using Logging;
|
||||||
using Logging;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
@ -11,11 +10,11 @@ namespace DistTestCore
|
||||||
{
|
{
|
||||||
private readonly BaseLog log;
|
private readonly BaseLog log;
|
||||||
private readonly ITimeSet timeSet;
|
private readonly ITimeSet timeSet;
|
||||||
private readonly RunningContainerAddress address;
|
private readonly Address address;
|
||||||
private readonly string baseUrl;
|
private readonly string baseUrl;
|
||||||
private readonly TimeSpan? timeoutOverride;
|
private readonly TimeSpan? timeoutOverride;
|
||||||
|
|
||||||
public Http(BaseLog log, ITimeSet timeSet, RunningContainerAddress address, string baseUrl, TimeSpan? timeoutOverride = null)
|
public Http(BaseLog log, ITimeSet timeSet, Address address, string baseUrl, TimeSpan? timeoutOverride = null)
|
||||||
{
|
{
|
||||||
this.log = log;
|
this.log = log;
|
||||||
this.timeSet = timeSet;
|
this.timeSet = timeSet;
|
||||||
|
|
|
@ -50,7 +50,7 @@ namespace DistTestCore.Marketplace
|
||||||
$"proofProbability: {proofProbability}, " +
|
$"proofProbability: {proofProbability}, " +
|
||||||
$"duration: {Time.FormatDuration(duration)})");
|
$"duration: {Time.FormatDuration(duration)})");
|
||||||
|
|
||||||
var response = codexAccess.RequestStorage(request, contentId.Id);
|
var response = codexAccess.Node.RequestStorage(request, contentId.Id);
|
||||||
|
|
||||||
if (response == "Purchasing not available")
|
if (response == "Purchasing not available")
|
||||||
{
|
{
|
||||||
|
@ -78,7 +78,7 @@ namespace DistTestCore.Marketplace
|
||||||
$"maxCollateral: {maxCollateral}, " +
|
$"maxCollateral: {maxCollateral}, " +
|
||||||
$"maxDuration: {Time.FormatDuration(maxDuration)})");
|
$"maxDuration: {Time.FormatDuration(maxDuration)})");
|
||||||
|
|
||||||
var response = codexAccess.SalesAvailability(request);
|
var response = codexAccess.Node.SalesAvailability(request);
|
||||||
|
|
||||||
Log($"Storage successfully made available. Id: {response.id}");
|
Log($"Storage successfully made available. Id: {response.id}");
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ namespace DistTestCore
|
||||||
|
|
||||||
public CodexDebugResponse GetDebugInfo()
|
public CodexDebugResponse GetDebugInfo()
|
||||||
{
|
{
|
||||||
var debugInfo = CodexAccess.GetDebugInfo();
|
var debugInfo = CodexAccess.Node.GetDebugInfo();
|
||||||
var known = string.Join(",", debugInfo.table.nodes.Select(n => n.peerId));
|
var known = string.Join(",", debugInfo.table.nodes.Select(n => n.peerId));
|
||||||
Log($"Got DebugInfo with id: '{debugInfo.id}'. This node knows: {known}");
|
Log($"Got DebugInfo with id: '{debugInfo.id}'. This node knows: {known}");
|
||||||
return debugInfo;
|
return debugInfo;
|
||||||
|
@ -57,12 +57,12 @@ namespace DistTestCore
|
||||||
|
|
||||||
public CodexDebugPeerResponse GetDebugPeer(string peerId)
|
public CodexDebugPeerResponse GetDebugPeer(string peerId)
|
||||||
{
|
{
|
||||||
return CodexAccess.GetDebugPeer(peerId);
|
return CodexAccess.Node.GetDebugPeer(peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CodexDebugPeerResponse GetDebugPeer(string peerId, TimeSpan timeout)
|
public CodexDebugPeerResponse GetDebugPeer(string peerId, TimeSpan timeout)
|
||||||
{
|
{
|
||||||
return CodexAccess.GetDebugPeer(peerId, timeout);
|
return CodexAccess.Node.GetDebugPeer(peerId, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentId UploadFile(TestFile file)
|
public ContentId UploadFile(TestFile file)
|
||||||
|
@ -72,7 +72,7 @@ namespace DistTestCore
|
||||||
var logMessage = $"Uploading file {file.Describe()}...";
|
var logMessage = $"Uploading file {file.Describe()}...";
|
||||||
var response = Stopwatch.Measure(lifecycle.Log, logMessage, () =>
|
var response = Stopwatch.Measure(lifecycle.Log, logMessage, () =>
|
||||||
{
|
{
|
||||||
return CodexAccess.UploadFile(fileStream);
|
return CodexAccess.Node.UploadFile(fileStream);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.StartsWith(UploadFailedMessage))
|
if (response.StartsWith(UploadFailedMessage))
|
||||||
|
@ -101,7 +101,7 @@ namespace DistTestCore
|
||||||
|
|
||||||
Log($"Connecting to peer {peer.GetName()}...");
|
Log($"Connecting to peer {peer.GetName()}...");
|
||||||
var peerInfo = node.GetDebugInfo();
|
var peerInfo = node.GetDebugInfo();
|
||||||
var response = CodexAccess.ConnectToPeer(peerInfo.id, GetPeerMultiAddress(peer, peerInfo));
|
var response = CodexAccess.Node.ConnectToPeer(peerInfo.id, GetPeerMultiAddress(peer, peerInfo));
|
||||||
|
|
||||||
Assert.That(response, Is.EqualTo(SuccessfullyConnectedMessage), "Unable to connect codex nodes.");
|
Assert.That(response, Is.EqualTo(SuccessfullyConnectedMessage), "Unable to connect codex nodes.");
|
||||||
Log($"Successfully connected to peer {peer.GetName()}.");
|
Log($"Successfully connected to peer {peer.GetName()}.");
|
||||||
|
@ -141,7 +141,7 @@ namespace DistTestCore
|
||||||
using var fileStream = File.OpenWrite(file.Filename);
|
using var fileStream = File.OpenWrite(file.Filename);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var downloadStream = CodexAccess.DownloadFile(contentId);
|
using var downloadStream = CodexAccess.Node.DownloadFile(contentId);
|
||||||
downloadStream.CopyTo(fileStream);
|
downloadStream.CopyTo(fileStream);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
namespace KubernetesWorkflow
|
using Utils;
|
||||||
|
|
||||||
|
namespace KubernetesWorkflow
|
||||||
{
|
{
|
||||||
public class RunningContainers
|
public class RunningContainers
|
||||||
{
|
{
|
||||||
|
@ -21,7 +23,7 @@
|
||||||
|
|
||||||
public class RunningContainer
|
public class RunningContainer
|
||||||
{
|
{
|
||||||
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, StartupConfig startupConfig, RunningContainerAddress clusterExternalAddress, RunningContainerAddress clusterInternalAddress)
|
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, StartupConfig startupConfig, Address clusterExternalAddress, Address clusterInternalAddress)
|
||||||
{
|
{
|
||||||
Pod = pod;
|
Pod = pod;
|
||||||
Recipe = recipe;
|
Recipe = recipe;
|
||||||
|
@ -35,8 +37,8 @@
|
||||||
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 Address ClusterExternalAddress { get; }
|
||||||
public RunningContainerAddress ClusterInternalAddress { get; }
|
public Address ClusterInternalAddress { get; }
|
||||||
|
|
||||||
private string GetContainerName(ContainerRecipe recipe, StartupConfig startupConfig)
|
private string GetContainerName(ContainerRecipe recipe, StartupConfig startupConfig)
|
||||||
{
|
{
|
||||||
|
@ -50,16 +52,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RunningContainerAddress
|
|
||||||
{
|
|
||||||
public RunningContainerAddress(string host, int port)
|
|
||||||
{
|
|
||||||
Host = host;
|
|
||||||
Port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Host { get; }
|
|
||||||
public int Port { get; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Logging;
|
using Logging;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
namespace KubernetesWorkflow
|
namespace KubernetesWorkflow
|
||||||
{
|
{
|
||||||
|
@ -87,20 +88,20 @@ namespace KubernetesWorkflow
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RunningContainerAddress GetContainerExternalAddress(RunningPod pod, Port[] servicePorts)
|
private Address GetContainerExternalAddress(RunningPod pod, Port[] servicePorts)
|
||||||
{
|
{
|
||||||
return new RunningContainerAddress(
|
return new Address(
|
||||||
pod.Cluster.HostAddress,
|
pod.Cluster.HostAddress,
|
||||||
GetServicePort(servicePorts));
|
GetServicePort(servicePorts));
|
||||||
}
|
}
|
||||||
|
|
||||||
private RunningContainerAddress GetContainerInternalAddress(ContainerRecipe recipe)
|
private Address GetContainerInternalAddress(ContainerRecipe recipe)
|
||||||
{
|
{
|
||||||
var serviceName = "service-" + numberSource.WorkflowNumber;
|
var serviceName = "service-" + numberSource.WorkflowNumber;
|
||||||
var namespaceName = cluster.Configuration.K8sNamespacePrefix + testNamespace;
|
var namespaceName = cluster.Configuration.K8sNamespacePrefix + testNamespace;
|
||||||
var port = GetInternalPort(recipe);
|
var port = GetInternalPort(recipe);
|
||||||
|
|
||||||
return new RunningContainerAddress(
|
return new Address(
|
||||||
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
|
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
|
||||||
port);
|
port);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Utils
|
||||||
|
{
|
||||||
|
public class Address
|
||||||
|
{
|
||||||
|
public Address(string host, int port)
|
||||||
|
{
|
||||||
|
Host = host;
|
||||||
|
Port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Host { get; }
|
||||||
|
public int Port { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logging", "Logging\Logging.
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NethereumWorkflow", "Nethereum\NethereumWorkflow.csproj", "{D6C3555E-D52D-4993-A87B-71AB650398FD}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NethereumWorkflow", "Nethereum\NethereumWorkflow.csproj", "{D6C3555E-D52D-4993-A87B-71AB650398FD}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ContinuousTests", "ContinuousTests\ContinuousTests.csproj", "{025B7074-0A09-4FCC-9BB9-03AE2A961EA1}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -51,6 +53,10 @@ Global
|
||||||
{D6C3555E-D52D-4993-A87B-71AB650398FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D6C3555E-D52D-4993-A87B-71AB650398FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D6C3555E-D52D-4993-A87B-71AB650398FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D6C3555E-D52D-4993-A87B-71AB650398FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D6C3555E-D52D-4993-A87B-71AB650398FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D6C3555E-D52D-4993-A87B-71AB650398FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{025B7074-0A09-4FCC-9BB9-03AE2A961EA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{025B7074-0A09-4FCC-9BB9-03AE2A961EA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{025B7074-0A09-4FCC-9BB9-03AE2A961EA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{025B7074-0A09-4FCC-9BB9-03AE2A961EA1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
Loading…
Reference in New Issue