diff --git a/BasicTests/DebugEndpointTests.cs b/BasicTests/DebugEndpointTests.cs new file mode 100644 index 0000000..4f8e6a4 --- /dev/null +++ b/BasicTests/DebugEndpointTests.cs @@ -0,0 +1,22 @@ +using CodexDistTests.TestCore; +using NUnit.Framework; + +namespace CodexDistTests.BasicTests +{ + [TestFixture] + public class DebugEndpointTests : DistTest + { + [Test] + public void GetDebugInfo() + { + CreateCodexNode(); + + var node = GetCodexNode(); + var debugInfo = node.GetDebugInfo(); + + Assert.That(debugInfo.spr, Is.Not.Empty); + + DestroyCodexNode(); + } + } +} diff --git a/ExampleFixtureTests.cs b/ExampleFixtureTests.cs deleted file mode 100644 index 6d6aa02..0000000 --- a/ExampleFixtureTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NUnit.Framework; - -[TestFixture] -public class ExampleFixtureTests -{ - [SetUp] - public void SetUp() - { - } - - [Test] - public void TestFail() - { - Assert.Fail(); - } - - [Test] - public void TestPass() - { - Assert.Pass(); - } -} diff --git a/TestCore/CodexNode.cs b/TestCore/CodexNode.cs new file mode 100644 index 0000000..833c7e0 --- /dev/null +++ b/TestCore/CodexNode.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System.Net.Http.Headers; + +namespace CodexDistTests.TestCore +{ + public class CodexNode + { + private readonly int port; + + public CodexNode(int port) + { + this.port = port; + } + + public CodexDebugResponse GetDebugInfo() + { + return HttpGet("debug/info"); + } + + public string UploadFile(string filename, int retryCounter = 0) + { + try + { + var url = $"http://127.0.0.1:{port}/api/codex/v1/upload"; + using var client = GetClient(); + + var byteData = File.ReadAllBytes(filename); + using var content = new ByteArrayContent(byteData); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + var response = Utils.Wait(client.PostAsync(url, content)); + + var contentId = Utils.Wait(response.Content.ReadAsStringAsync()); + return contentId; + } + catch (Exception exception) + { + if (retryCounter > 5) + { + Assert.Fail(exception.Message); + throw; + } + else + { + Timing.RetryDelay(); + return UploadFile(filename, retryCounter + 1); + } + } + } + + public byte[]? DownloadContent(string contentId) + { + return HttpGetBytes("download/" + contentId); + } + + private byte[]? HttpGetBytes(string endpoint, int retryCounter = 0) + { + try + { + using var client = GetClient(); + var url = $"http://127.0.0.1:{port}/api/codex/v1/" + endpoint; + var result = Utils.Wait(client.GetAsync(url)); + return Utils.Wait(result.Content.ReadAsByteArrayAsync()); + } + catch (Exception exception) + { + if (retryCounter > 5) + { + Assert.Fail(exception.Message); + return null; + } + else + { + Timing.RetryDelay(); + return HttpGetBytes(endpoint, retryCounter + 1); + } + } + } + + private T HttpGet(string endpoint, int retryCounter = 0) + { + try + { + using var client = GetClient(); + var url = $"http://127.0.0.1:{port}/api/codex/v1/" + endpoint; + var result = Utils.Wait(client.GetAsync(url)); + var json = Utils.Wait(result.Content.ReadAsStringAsync()); + return JsonConvert.DeserializeObject(json); + } + catch (Exception exception) + { + if (retryCounter > 5) + { + Assert.Fail(exception.Message); + throw; + } + else + { + Timing.RetryDelay(); + return HttpGet(endpoint, retryCounter + 1); + } + } + } + + private HttpClient GetClient() + { + var client = new HttpClient(); + client.Timeout = Timing.HttpCallTimeout(); + return client; + } + } + + 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; + } +} diff --git a/TestCore/DistTest.cs b/TestCore/DistTest.cs new file mode 100644 index 0000000..3a970fd --- /dev/null +++ b/TestCore/DistTest.cs @@ -0,0 +1,128 @@ +using k8s; +using k8s.Models; + +namespace CodexDistTests.TestCore +{ + public abstract class DistTest + { + private const string k8sNamespace = "codex-test-namespace"; + + private V1Namespace? activeNamespace; + private V1Deployment? activeDeployment; + private V1Service? activeService; + + public void CreateCodexNode() + { + var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(); + var client = new Kubernetes(config); + + var namespaceSpec = new V1Namespace + { + ApiVersion = "v1", + Metadata = new V1ObjectMeta + { + Name = k8sNamespace, + Labels = new Dictionary { { "name", k8sNamespace } } + } + }; + var deploymentSpec = new V1Deployment + { + ApiVersion = "apps/v1", + Metadata = new V1ObjectMeta + { + Name = "codex-demo", + NamespaceProperty = k8sNamespace + }, + Spec = new V1DeploymentSpec + { + Replicas = 1, + Selector = new V1LabelSelector + { + MatchLabels = new Dictionary { { "codex-node", "dist-test" } } + }, + Template = new V1PodTemplateSpec + { + Metadata = new V1ObjectMeta + { + Labels = new Dictionary { { "codex-node", "dist-test" } } + }, + Spec = new V1PodSpec + { + Containers = new List + { + new V1Container + { + Name = "codex-node", + Image = "thatbenbierens/nim-codex:sha-c9a62de", + Ports = new List + { + new V1ContainerPort + { + ContainerPort = 8080, + Name = "codex-api-port" + } + }, + Env = new List + { + new V1EnvVar + { + Name = "LOG_LEVEL", + Value = "WARN" + } + } + } + } + } + } + } + }; + var serviceSpec = new V1Service + { + ApiVersion = "v1", + Metadata = new V1ObjectMeta + { + Name = "codex-entrypoint", + NamespaceProperty = k8sNamespace + }, + Spec = new V1ServiceSpec + { + Type = "NodePort", + Selector = new Dictionary { { "codex-node", "dist-test" } }, + Ports = new List + { + new V1ServicePort + { + Protocol = "TCP", + Port = 8080, + TargetPort = "codex-api-port", + NodePort = 30001 + } + } + } + }; + + activeNamespace = client.CreateNamespace(namespaceSpec); + activeDeployment = client.CreateNamespacedDeployment(deploymentSpec, k8sNamespace); + activeService = client.CreateNamespacedService(serviceSpec, k8sNamespace); + + // todo: wait until online! + } + + public CodexNode GetCodexNode() + { + return new CodexNode(30001); // matches service spec. + } + + public void DestroyCodexNode() + { + var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(); + var client = new Kubernetes(config); + + client.DeleteNamespacedService(activeService.Name(), k8sNamespace); + client.DeleteNamespacedDeployment(activeDeployment.Name(), k8sNamespace); + client.DeleteNamespace(activeNamespace.Name()); + + // todo: wait until terminated! + } + } +} diff --git a/TestCore/Timing.cs b/TestCore/Timing.cs new file mode 100644 index 0000000..cca6dd5 --- /dev/null +++ b/TestCore/Timing.cs @@ -0,0 +1,15 @@ +namespace CodexDistTests.TestCore +{ + public static class Timing + { + public static TimeSpan HttpCallTimeout() + { + return TimeSpan.FromMinutes(10); + } + + public static void RetryDelay() + { + Utils.Sleep(TimeSpan.FromSeconds(3)); + } + } +} diff --git a/TestCore/Utils.cs b/TestCore/Utils.cs new file mode 100644 index 0000000..d36072e --- /dev/null +++ b/TestCore/Utils.cs @@ -0,0 +1,16 @@ +namespace CodexDistTests.TestCore +{ + public static class Utils + { + public static void Sleep(TimeSpan span) + { + Thread.Sleep(span); + } + + public static T Wait(Task task) + { + task.Wait(); + return task.Result; + } + } +}