Moving everything around
This commit is contained in:
parent
bdd977d8a9
commit
7b91c83f5b
|
@ -99,15 +99,5 @@ namespace CodexDistTestCore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PodInfo
|
|
||||||
{
|
|
||||||
public PodInfo(string name, string ip)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
Ip = ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
public string Ip { get; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,15 +13,6 @@ namespace CodexDistTestCore
|
||||||
ICodexNodeGroup BringOnline();
|
ICodexNodeGroup BringOnline();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CodexLogLevel
|
|
||||||
{
|
|
||||||
Trace,
|
|
||||||
Debug,
|
|
||||||
Info,
|
|
||||||
Warn,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Location
|
public enum Location
|
||||||
{
|
{
|
||||||
Unspecified,
|
Unspecified,
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
using CodexDistTestCore.Config;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CodexDistTestCore
|
|
||||||
{
|
|
||||||
public class TestLog
|
|
||||||
{
|
|
||||||
private readonly NumberSource subfileNumberSource = new NumberSource(0);
|
|
||||||
private readonly LogFile file;
|
|
||||||
private readonly DateTime now;
|
|
||||||
|
|
||||||
public TestLog()
|
|
||||||
{
|
|
||||||
now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
var name = GetTestName();
|
|
||||||
file = new LogFile(now, name);
|
|
||||||
|
|
||||||
Log($"Begin: {name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Log(string message)
|
|
||||||
{
|
|
||||||
file.Write(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Error(string message)
|
|
||||||
{
|
|
||||||
Log($"[ERROR] {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndTest()
|
|
||||||
{
|
|
||||||
var result = TestContext.CurrentContext.Result;
|
|
||||||
|
|
||||||
Log($"Finished: {GetTestName()} = {result.Outcome.Status}");
|
|
||||||
if (!string.IsNullOrEmpty(result.Message))
|
|
||||||
{
|
|
||||||
Log(result.Message);
|
|
||||||
Log($"{result.StackTrace}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
|
|
||||||
{
|
|
||||||
RenameLogFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RenameLogFile()
|
|
||||||
{
|
|
||||||
file.ConcatToFilename("_FAILED");
|
|
||||||
}
|
|
||||||
|
|
||||||
public LogFile CreateSubfile(string ext = "log")
|
|
||||||
{
|
|
||||||
return new LogFile(now, $"{GetTestName()}_{subfileNumberSource.GetNextNumber().ToString().PadLeft(6, '0')}", ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTestName()
|
|
||||||
{
|
|
||||||
var test = TestContext.CurrentContext.Test;
|
|
||||||
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
|
|
||||||
var args = FormatArguments(test);
|
|
||||||
return $"{className}.{test.MethodName}{args}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatArguments(TestContext.TestAdapter test)
|
|
||||||
{
|
|
||||||
if (test.Arguments == null || !test.Arguments.Any()) return "";
|
|
||||||
return $"[{string.Join(',', test.Arguments)}]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LogFile
|
|
||||||
{
|
|
||||||
private readonly DateTime now;
|
|
||||||
private string name;
|
|
||||||
private readonly string ext;
|
|
||||||
private readonly string filepath;
|
|
||||||
|
|
||||||
public LogFile(DateTime now, string name, string ext = "log")
|
|
||||||
{
|
|
||||||
this.now = now;
|
|
||||||
this.name = name;
|
|
||||||
this.ext = ext;
|
|
||||||
|
|
||||||
filepath = Path.Join(
|
|
||||||
LogConfig.LogRoot,
|
|
||||||
$"{now.Year}-{Pad(now.Month)}",
|
|
||||||
Pad(now.Day));
|
|
||||||
|
|
||||||
Directory.CreateDirectory(filepath);
|
|
||||||
|
|
||||||
GenerateFilename();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FullFilename { get; private set; } = string.Empty;
|
|
||||||
public string FilenameWithoutPath { get; private set; } = string.Empty;
|
|
||||||
|
|
||||||
public void Write(string message)
|
|
||||||
{
|
|
||||||
WriteRaw($"{GetTimestamp()} {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteRaw(string message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.AppendAllLines(FullFilename, new[] { message });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Writing to log has failed: " + ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConcatToFilename(string toAdd)
|
|
||||||
{
|
|
||||||
var oldFullName = FullFilename;
|
|
||||||
|
|
||||||
name += toAdd;
|
|
||||||
|
|
||||||
GenerateFilename();
|
|
||||||
|
|
||||||
File.Move(oldFullName, FullFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Pad(int n)
|
|
||||||
{
|
|
||||||
return n.ToString().PadLeft(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTimestamp()
|
|
||||||
{
|
|
||||||
return $"[{DateTime.UtcNow.ToString("u")}]";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GenerateFilename()
|
|
||||||
{
|
|
||||||
FilenameWithoutPath = $"{Pad(now.Hour)}-{Pad(now.Minute)}-{Pad(now.Second)}Z_{name.Replace('.', '-')}.{ext}";
|
|
||||||
FullFilename = Path.Combine(filepath, FilenameWithoutPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,15 +2,6 @@
|
||||||
{
|
{
|
||||||
public static class Utils
|
public static class Utils
|
||||||
{
|
{
|
||||||
public static void Sleep(TimeSpan span)
|
|
||||||
{
|
|
||||||
Thread.Sleep(span);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T Wait<T>(Task<T> task)
|
|
||||||
{
|
|
||||||
task.Wait();
|
|
||||||
return task.Result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
namespace DistTestCore
|
||||||
|
{
|
||||||
|
public class ByteSize
|
||||||
|
{
|
||||||
|
public ByteSize(long sizeInBytes)
|
||||||
|
{
|
||||||
|
SizeInBytes = sizeInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long SizeInBytes { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IntExtensions
|
||||||
|
{
|
||||||
|
private const long Kilo = 1024;
|
||||||
|
|
||||||
|
public static ByteSize KB(this long i)
|
||||||
|
{
|
||||||
|
return new ByteSize(i * Kilo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize MB(this long i)
|
||||||
|
{
|
||||||
|
return (i * Kilo).KB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize GB(this long i)
|
||||||
|
{
|
||||||
|
return (i * Kilo).MB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize TB(this long i)
|
||||||
|
{
|
||||||
|
return (i * Kilo).GB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize KB(this int i)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(i).KB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize MB(this int i)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(i).MB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize GB(this int i)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(i).GB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSize TB(this int i)
|
||||||
|
{
|
||||||
|
return Convert.ToInt64(i).TB();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
using KubernetesWorkflow;
|
||||||
|
|
||||||
|
namespace DistTestCore.Codex
|
||||||
|
{
|
||||||
|
public class CodexAccess
|
||||||
|
{
|
||||||
|
private readonly RunningContainer runningContainer;
|
||||||
|
|
||||||
|
public CodexAccess(RunningContainer runningContainer)
|
||||||
|
{
|
||||||
|
this.runningContainer = runningContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodexDebugResponse GetDebugInfo()
|
||||||
|
{
|
||||||
|
var response = Http().HttpGetJson<CodexDebugResponse>("debug/info");
|
||||||
|
//Log($"Got DebugInfo with id: '{response.id}'.");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Http Http()
|
||||||
|
{
|
||||||
|
var ip = runningContainer.Pod.Cluster.GetIp();
|
||||||
|
var port = runningContainer.ServicePorts[0].Number;
|
||||||
|
return new Http(ip, port, baseUrl: "/api/codex/v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 CodexDebugVersionResponse codex { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CodexDebugVersionResponse
|
||||||
|
{
|
||||||
|
public string version { get; set; } = string.Empty;
|
||||||
|
public string revision { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
using KubernetesWorkflow;
|
||||||
|
|
||||||
|
namespace DistTestCore.Codex
|
||||||
|
{
|
||||||
|
public class CodexContainerRecipe : ContainerRecipeFactory
|
||||||
|
{
|
||||||
|
protected override string Image => "thatbenbierens/nim-codex:sha-b204837";
|
||||||
|
|
||||||
|
protected override void Initialize(StartupConfig startupConfig)
|
||||||
|
{
|
||||||
|
var config = startupConfig.Get<CodexStartupConfig>();
|
||||||
|
|
||||||
|
AddExposedPortAndVar("API_PORT");
|
||||||
|
AddEnvVar("DATA_DIR", $"datadir{ContainerNumber}");
|
||||||
|
AddInternalPortAndVar("DISC_PORT");
|
||||||
|
|
||||||
|
var listenPort = AddInternalPort();
|
||||||
|
AddEnvVar("LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}");
|
||||||
|
|
||||||
|
if (config.LogLevel != null)
|
||||||
|
{
|
||||||
|
AddEnvVar("LOG_LEVEL", config.LogLevel.ToString()!.ToUpperInvariant());
|
||||||
|
}
|
||||||
|
if (config.StorageQuota != null)
|
||||||
|
{
|
||||||
|
AddEnvVar("STORAGE_QUOTA", config.StorageQuota.SizeInBytes.ToString()!);
|
||||||
|
}
|
||||||
|
if (config.MetricsEnabled)
|
||||||
|
{
|
||||||
|
AddEnvVar("METRICS_ADDR", "0.0.0.0");
|
||||||
|
AddInternalPortAndVar("METRICS_PORT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace DistTestCore.Codex
|
||||||
|
{
|
||||||
|
public enum CodexLogLevel
|
||||||
|
{
|
||||||
|
Trace,
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
using KubernetesWorkflow;
|
||||||
|
|
||||||
|
namespace DistTestCore.Codex
|
||||||
|
{
|
||||||
|
public class CodexStartupConfig
|
||||||
|
{
|
||||||
|
public Location Location { get; set; }
|
||||||
|
public CodexLogLevel? LogLevel { get; set; }
|
||||||
|
public ByteSize? StorageQuota { get; set; }
|
||||||
|
public bool MetricsEnabled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<RootNamespace>DistTestCore</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Nethereum.Web3" Version="4.14.0" />
|
||||||
|
<PackageReference Include="nunit" Version="3.13.3" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\KubernetesWorkflow\KubernetesWorkflow.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,100 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace DistTestCore
|
||||||
|
{
|
||||||
|
public class Http
|
||||||
|
{
|
||||||
|
private readonly string ip;
|
||||||
|
private readonly int port;
|
||||||
|
private readonly string baseUrl;
|
||||||
|
|
||||||
|
public Http(string ip, int port, string baseUrl)
|
||||||
|
{
|
||||||
|
this.ip = ip;
|
||||||
|
this.port = port;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
|
||||||
|
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
|
||||||
|
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string HttpGetString(string route)
|
||||||
|
{
|
||||||
|
return Retry(() =>
|
||||||
|
{
|
||||||
|
using var client = GetClient();
|
||||||
|
var url = GetUrl() + route;
|
||||||
|
var result = Utils.Wait(client.GetAsync(url));
|
||||||
|
return Utils.Wait(result.Content.ReadAsStringAsync());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public T HttpGetJson<T>(string route)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<T>(HttpGetString(route))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string HttpPostStream(string route, Stream stream)
|
||||||
|
{
|
||||||
|
return Retry(() =>
|
||||||
|
{
|
||||||
|
using var client = GetClient();
|
||||||
|
var url = GetUrl() + route;
|
||||||
|
|
||||||
|
var content = new StreamContent(stream);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||||
|
var response = Utils.Wait(client.PostAsync(url, content));
|
||||||
|
|
||||||
|
return Utils.Wait(response.Content.ReadAsStringAsync());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream HttpGetStream(string route)
|
||||||
|
{
|
||||||
|
return Retry(() =>
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
var url = GetUrl() + route;
|
||||||
|
|
||||||
|
return Utils.Wait(client.GetStreamAsync(url));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetUrl()
|
||||||
|
{
|
||||||
|
return $"http://{ip}:{port}{baseUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Retry<T>(Func<T> operation)
|
||||||
|
{
|
||||||
|
var retryCounter = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return operation();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Timing.HttpCallRetryDelay();
|
||||||
|
retryCounter++;
|
||||||
|
if (retryCounter > Timing.HttpCallRetryCount())
|
||||||
|
{
|
||||||
|
Assert.Fail(exception.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient GetClient()
|
||||||
|
{
|
||||||
|
var client = new HttpClient();
|
||||||
|
client.Timeout = Timing.HttpCallTimeout();
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
using DistTestCore.Codex;
|
||||||
|
using KubernetesWorkflow;
|
||||||
|
|
||||||
|
namespace DistTestCore
|
||||||
|
{
|
||||||
|
public class TestLifecycle
|
||||||
|
{
|
||||||
|
private readonly WorkflowCreator workflowCreator = new WorkflowCreator();
|
||||||
|
|
||||||
|
public void SetUpCodexNodes()
|
||||||
|
{
|
||||||
|
var config = new CodexStartupConfig()
|
||||||
|
{
|
||||||
|
StorageQuota = 10.MB(),
|
||||||
|
Location = Location.Unspecified,
|
||||||
|
LogLevel = CodexLogLevel.Error,
|
||||||
|
MetricsEnabled = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var workflow = workflowCreator.CreateWorkflow();
|
||||||
|
var startupConfig = new StartupConfig();
|
||||||
|
startupConfig.Add(config);
|
||||||
|
var containers = workflow.Start(3, new CodexContainerRecipe(), startupConfig);
|
||||||
|
|
||||||
|
foreach (var c in containers.Containers)
|
||||||
|
{
|
||||||
|
var access = new CodexAccess(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class ContainerRecipe
|
||||||
|
{
|
||||||
|
public ContainerRecipe(string name, string image, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Image = image;
|
||||||
|
ExposedPorts = exposedPorts;
|
||||||
|
InternalPorts = internalPorts;
|
||||||
|
EnvVars = envVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public string Image { get; }
|
||||||
|
public Port[] ExposedPorts { get; }
|
||||||
|
public Port[] InternalPorts { get; }
|
||||||
|
public EnvVar[] EnvVars { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Port
|
||||||
|
{
|
||||||
|
public Port(int number)
|
||||||
|
{
|
||||||
|
Number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Number { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnvVar
|
||||||
|
{
|
||||||
|
public EnvVar(string name, string value)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public string Value { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public abstract class ContainerRecipeFactory
|
||||||
|
{
|
||||||
|
private readonly List<Port> exposedPorts = new List<Port>();
|
||||||
|
private readonly List<Port> internalPorts = new List<Port>();
|
||||||
|
private readonly List<EnvVar> envVars = new List<EnvVar>();
|
||||||
|
private RecipeComponentFactory factory = null!;
|
||||||
|
|
||||||
|
public ContainerRecipe CreateRecipe(int containerNumber, RecipeComponentFactory factory, StartupConfig config)
|
||||||
|
{
|
||||||
|
this.factory = factory;
|
||||||
|
ContainerNumber = containerNumber;
|
||||||
|
|
||||||
|
Initialize(config);
|
||||||
|
|
||||||
|
var name = $"ctnr{containerNumber}";
|
||||||
|
|
||||||
|
return new ContainerRecipe(name, Image, exposedPorts.ToArray(), internalPorts.ToArray(), envVars.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract string Image { get; }
|
||||||
|
protected int ContainerNumber { get; private set; } = 0;
|
||||||
|
protected abstract void Initialize(StartupConfig config);
|
||||||
|
|
||||||
|
protected Port AddExposedPort()
|
||||||
|
{
|
||||||
|
var p = factory.CreatePort();
|
||||||
|
exposedPorts.Add(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Port AddInternalPort()
|
||||||
|
{
|
||||||
|
var p = factory.CreatePort();
|
||||||
|
internalPorts.Add(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddExposedPortAndVar(string name)
|
||||||
|
{
|
||||||
|
AddEnvVar(name, AddExposedPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddInternalPortAndVar(string name)
|
||||||
|
{
|
||||||
|
AddEnvVar(name, AddInternalPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddEnvVar(string name, string value)
|
||||||
|
{
|
||||||
|
envVars.Add(factory.CreateEnvVar(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddEnvVar(string name, Port value)
|
||||||
|
{
|
||||||
|
envVars.Add(factory.CreateEnvVar(name, value.Number));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
using k8s;
|
||||||
|
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class K8sCluster
|
||||||
|
{
|
||||||
|
public const string K8sNamespace = "codex-test-namespace";
|
||||||
|
private const string KubeConfigFile = "C:\\kube\\config";
|
||||||
|
private readonly Dictionary<Location, string> K8sNodeLocationMap = new Dictionary<Location, string>
|
||||||
|
{
|
||||||
|
{ Location.BensLaptop, "worker01" },
|
||||||
|
{ Location.BensOldGamingMachine, "worker02" },
|
||||||
|
};
|
||||||
|
|
||||||
|
private KubernetesClientConfiguration? config;
|
||||||
|
|
||||||
|
public KubernetesClientConfiguration GetK8sClientConfig()
|
||||||
|
{
|
||||||
|
if (config != null) return config;
|
||||||
|
//config = KubernetesClientConfiguration.BuildConfigFromConfigFile(KubeConfigFile);
|
||||||
|
config = KubernetesClientConfiguration.BuildDefaultConfig();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetIp()
|
||||||
|
{
|
||||||
|
var c = GetK8sClientConfig();
|
||||||
|
|
||||||
|
var host = c.Host.Replace("https://", "");
|
||||||
|
|
||||||
|
return host.Substring(0, host.IndexOf(':'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetNodeLabelForLocation(Location location)
|
||||||
|
{
|
||||||
|
if (location == Location.Unspecified) return string.Empty;
|
||||||
|
return K8sNodeLocationMap[location];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class K8sController
|
||||||
|
{
|
||||||
|
private readonly K8sCluster cluster;
|
||||||
|
|
||||||
|
public K8sController(K8sCluster cluster)
|
||||||
|
{
|
||||||
|
this.cluster = cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RunningPod BringOnline(ContainerRecipe[] containerRecipes)
|
||||||
|
{
|
||||||
|
// Ensure namespace
|
||||||
|
// create deployment
|
||||||
|
// create service if necessary
|
||||||
|
// wait until deployment online
|
||||||
|
// fetch pod info
|
||||||
|
|
||||||
|
// for each container, there is now an array of service ports available.
|
||||||
|
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<RootNamespace>KubernetesWorkflow</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="KubernetesClient" Version="10.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public enum Location
|
||||||
|
{
|
||||||
|
Unspecified,
|
||||||
|
BensLaptop,
|
||||||
|
BensOldGamingMachine
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class RecipeComponentFactory
|
||||||
|
{
|
||||||
|
private NumberSource portNumberSource = new NumberSource(8080);
|
||||||
|
|
||||||
|
public Port CreatePort()
|
||||||
|
{
|
||||||
|
return new Port(portNumberSource.GetNextNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnvVar CreateEnvVar(string name, int value)
|
||||||
|
{
|
||||||
|
return CreateEnvVar(name, value.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnvVar CreateEnvVar(string name, string value)
|
||||||
|
{
|
||||||
|
return new EnvVar(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class RunningContainers
|
||||||
|
{
|
||||||
|
public RunningContainers(StartupConfig startupConfig, RunningPod runningPod, RunningContainer[] containers)
|
||||||
|
{
|
||||||
|
StartupConfig = startupConfig;
|
||||||
|
RunningPod = runningPod;
|
||||||
|
Containers = containers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StartupConfig StartupConfig { get; }
|
||||||
|
public RunningPod RunningPod { get; }
|
||||||
|
public RunningContainer[] Containers { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RunningContainer
|
||||||
|
{
|
||||||
|
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts)
|
||||||
|
{
|
||||||
|
Pod = pod;
|
||||||
|
Recipe = recipe;
|
||||||
|
ServicePorts = servicePorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RunningPod Pod { get; }
|
||||||
|
public ContainerRecipe Recipe { get; }
|
||||||
|
public Port[] ServicePorts { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class RunningPod
|
||||||
|
{
|
||||||
|
private readonly Dictionary<ContainerRecipe, Port[]> servicePortMap;
|
||||||
|
|
||||||
|
public RunningPod(K8sCluster cluster, string name, string ip, Dictionary<ContainerRecipe, Port[]> servicePortMap)
|
||||||
|
{
|
||||||
|
Cluster = cluster;
|
||||||
|
Name = name;
|
||||||
|
Ip = ip;
|
||||||
|
this.servicePortMap = servicePortMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public K8sCluster Cluster { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string Ip { get; }
|
||||||
|
|
||||||
|
public Port[] GetServicePortsForContainerRecipe(ContainerRecipe containerRecipe)
|
||||||
|
{
|
||||||
|
return servicePortMap[containerRecipe];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class StartupConfig
|
||||||
|
{
|
||||||
|
private readonly List<object> configs = new List<object>();
|
||||||
|
|
||||||
|
public void Add(object config)
|
||||||
|
{
|
||||||
|
configs.Add(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get<T>()
|
||||||
|
{
|
||||||
|
var match = configs.Single(c => c.GetType() == typeof(T));
|
||||||
|
return (T)match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class StartupWorkflow
|
||||||
|
{
|
||||||
|
private readonly NumberSource containerNumberSource;
|
||||||
|
private readonly K8sController k8SController;
|
||||||
|
private readonly RecipeComponentFactory componentFactory = new RecipeComponentFactory();
|
||||||
|
|
||||||
|
public StartupWorkflow(NumberSource containerNumberSource, K8sController k8SController)
|
||||||
|
{
|
||||||
|
this.containerNumberSource = containerNumberSource;
|
||||||
|
this.k8SController = k8SController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||||
|
{
|
||||||
|
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
|
||||||
|
|
||||||
|
var runningPod = k8SController.BringOnline(recipes);
|
||||||
|
|
||||||
|
return new RunningContainers(startupConfig, runningPod, CreateContainers(runningPod, recipes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RunningContainer[] CreateContainers(RunningPod runningPod, ContainerRecipe[] recipes)
|
||||||
|
{
|
||||||
|
return recipes.Select(r => new RunningContainer(runningPod, r, runningPod.GetServicePortsForContainerRecipe(r))).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||||
|
{
|
||||||
|
var result = new List<ContainerRecipe>();
|
||||||
|
for (var i = 0; i < numberOfContainers; i++)
|
||||||
|
{
|
||||||
|
result.Add(recipeFactory.CreateRecipe(containerNumberSource.GetNextNumber(), componentFactory, startupConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace KubernetesWorkflow
|
||||||
|
{
|
||||||
|
public class WorkflowCreator
|
||||||
|
{
|
||||||
|
private readonly NumberSource containerNumberSource = new NumberSource(0);
|
||||||
|
private readonly K8sController controller = new K8sController(new K8sCluster());
|
||||||
|
|
||||||
|
public StartupWorkflow CreateWorkflow()
|
||||||
|
{
|
||||||
|
return new StartupWorkflow(containerNumberSource, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
namespace CodexDistTestCore.Config
|
namespace Logging
|
||||||
{
|
{
|
||||||
public class LogConfig
|
public class LogConfig
|
||||||
{
|
{
|
|
@ -0,0 +1,73 @@
|
||||||
|
namespace Logging
|
||||||
|
{
|
||||||
|
public class LogFile
|
||||||
|
{
|
||||||
|
private readonly DateTime now;
|
||||||
|
private string name;
|
||||||
|
private readonly string ext;
|
||||||
|
private readonly string filepath;
|
||||||
|
|
||||||
|
public LogFile(DateTime now, string name, string ext = "log")
|
||||||
|
{
|
||||||
|
this.now = now;
|
||||||
|
this.name = name;
|
||||||
|
this.ext = ext;
|
||||||
|
|
||||||
|
filepath = Path.Join(
|
||||||
|
LogConfig.LogRoot,
|
||||||
|
$"{now.Year}-{Pad(now.Month)}",
|
||||||
|
Pad(now.Day));
|
||||||
|
|
||||||
|
Directory.CreateDirectory(filepath);
|
||||||
|
|
||||||
|
GenerateFilename();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FullFilename { get; private set; } = string.Empty;
|
||||||
|
public string FilenameWithoutPath { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public void Write(string message)
|
||||||
|
{
|
||||||
|
WriteRaw($"{GetTimestamp()} {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteRaw(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.AppendAllLines(FullFilename, new[] { message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Writing to log has failed: " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConcatToFilename(string toAdd)
|
||||||
|
{
|
||||||
|
var oldFullName = FullFilename;
|
||||||
|
|
||||||
|
name += toAdd;
|
||||||
|
|
||||||
|
GenerateFilename();
|
||||||
|
|
||||||
|
File.Move(oldFullName, FullFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Pad(int n)
|
||||||
|
{
|
||||||
|
return n.ToString().PadLeft(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTimestamp()
|
||||||
|
{
|
||||||
|
return $"[{DateTime.UtcNow.ToString("u")}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateFilename()
|
||||||
|
{
|
||||||
|
FilenameWithoutPath = $"{Pad(now.Hour)}-{Pad(now.Minute)}-{Pad(now.Second)}Z_{name.Replace('.', '-')}.{ext}";
|
||||||
|
FullFilename = Path.Combine(filepath, FilenameWithoutPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<RootNamespace>Logging</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="nunit" Version="3.13.3" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Utils\Utils.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,74 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
|
namespace Logging
|
||||||
|
{
|
||||||
|
public class TestLog
|
||||||
|
{
|
||||||
|
private readonly NumberSource subfileNumberSource = new NumberSource(0);
|
||||||
|
private readonly LogFile file;
|
||||||
|
private readonly DateTime now;
|
||||||
|
|
||||||
|
public TestLog()
|
||||||
|
{
|
||||||
|
now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var name = GetTestName();
|
||||||
|
file = new LogFile(now, name);
|
||||||
|
|
||||||
|
Log($"Begin: {name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(string message)
|
||||||
|
{
|
||||||
|
file.Write(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message)
|
||||||
|
{
|
||||||
|
Log($"[ERROR] {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndTest()
|
||||||
|
{
|
||||||
|
var result = TestContext.CurrentContext.Result;
|
||||||
|
|
||||||
|
Log($"Finished: {GetTestName()} = {result.Outcome.Status}");
|
||||||
|
if (!string.IsNullOrEmpty(result.Message))
|
||||||
|
{
|
||||||
|
Log(result.Message);
|
||||||
|
Log($"{result.StackTrace}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
|
||||||
|
{
|
||||||
|
RenameLogFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenameLogFile()
|
||||||
|
{
|
||||||
|
file.ConcatToFilename("_FAILED");
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogFile CreateSubfile(string ext = "log")
|
||||||
|
{
|
||||||
|
return new LogFile(now, $"{GetTestName()}_{subfileNumberSource.GetNextNumber().ToString().PadLeft(6, '0')}", ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTestName()
|
||||||
|
{
|
||||||
|
var test = TestContext.CurrentContext.Test;
|
||||||
|
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
|
||||||
|
var args = FormatArguments(test);
|
||||||
|
return $"{className}.{test.MethodName}{args}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatArguments(TestContext.TestAdapter test)
|
||||||
|
{
|
||||||
|
if (test.Arguments == null || !test.Arguments.Any()) return "";
|
||||||
|
return $"[{string.Join(',', test.Arguments)}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
using CodexDistTestCore;
|
using CodexDistTestCore;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace LongTests.BasicTests
|
namespace TestsLong.BasicTests
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class LargeFileTests : DistTest
|
public class LargeFileTests : DistTest
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using CodexDistTestCore;
|
using CodexDistTestCore;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace LongTests.BasicTests
|
namespace TestsLong.BasicTests
|
||||||
{
|
{
|
||||||
public class TestInfraTests : DistTest
|
public class TestInfraTests : DistTest
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
namespace Utils
|
||||||
|
{
|
||||||
|
public class NumberSource
|
||||||
|
{
|
||||||
|
private int number;
|
||||||
|
|
||||||
|
public NumberSource(int start)
|
||||||
|
{
|
||||||
|
number = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetNextNumber()
|
||||||
|
{
|
||||||
|
var n = number;
|
||||||
|
number++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Utils
|
||||||
|
{
|
||||||
|
public static class Time
|
||||||
|
{
|
||||||
|
public static void Sleep(TimeSpan span)
|
||||||
|
{
|
||||||
|
Thread.Sleep(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Wait<T>(Task<T> task)
|
||||||
|
{
|
||||||
|
task.Wait();
|
||||||
|
return task.Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<RootNamespace>Utils</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -3,12 +3,18 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.4.33213.308
|
VisualStudioVersion = 17.4.33213.308
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{57F57B85-A537-4D3A-B7AE-B72C66B74AAB}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{57F57B85-A537-4D3A-B7AE-B72C66B74AAB}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LongTests", "LongTests\LongTests.csproj", "{AFCE270E-F844-4A7C-9006-69AE622BB1F4}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestsLong", "LongTests\TestsLong.csproj", "{AFCE270E-F844-4A7C-9006-69AE622BB1F4}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexDistTestCore", "CodexDistTestCore\CodexDistTestCore.csproj", "{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexDistTestCore", "CodexDistTestCore\CodexDistTestCore.csproj", "{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistTestCore", "DistTestCore\DistTestCore.csproj", "{47F31305-6E68-4827-8E39-7B41DAA1CE7A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubernetesWorkflow", "KubernetesWorkflow\KubernetesWorkflow.csproj", "{359123AA-3D9B-4442-80F4-19E32E3EC9EA}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{957DE3B8-9571-450A-8609-B267DCA8727C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -27,6 +33,18 @@ Global
|
||||||
{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{19306DE1-CEE5-4F7B-AA5D-FD91926D853D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{47F31305-6E68-4827-8E39-7B41DAA1CE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{47F31305-6E68-4827-8E39-7B41DAA1CE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{47F31305-6E68-4827-8E39-7B41DAA1CE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{47F31305-6E68-4827-8E39-7B41DAA1CE7A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{359123AA-3D9B-4442-80F4-19E32E3EC9EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{359123AA-3D9B-4442-80F4-19E32E3EC9EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{359123AA-3D9B-4442-80F4-19E32E3EC9EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{359123AA-3D9B-4442-80F4-19E32E3EC9EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{957DE3B8-9571-450A-8609-B267DCA8727C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{957DE3B8-9571-450A-8609-B267DCA8727C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{957DE3B8-9571-450A-8609-B267DCA8727C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{957DE3B8-9571-450A-8609-B267DCA8727C}.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