diff --git a/res/KiBoards.01.TestRuns.View.ndjson b/res/KiBoards.01.TestRuns.View.ndjson new file mode 100644 index 0000000..f8a7e85 --- /dev/null +++ b/res/KiBoards.01.TestRuns.View.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"fieldAttrs":"{\"id.keyword\":{\"count\":2},\"summary.time\":{\"customLabel\":\"Time\",\"count\":2},\"context.version\":{\"count\":2},\"id\":{\"customLabel\":\"Id\",\"count\":5},\"summary.failed\":{\"customLabel\":\"Failed\",\"count\":2},\"summary.total\":{\"customLabel\":\"Total\",\"count\":2},\"userName\":{\"customLabel\":\"User\",\"count\":1},\"summary.skipped\":{\"customLabel\":\"Skipped\",\"count\":2},\"name\":{\"customLabel\":\"Name\",\"count\":3},\"summary.passed\":{\"customLabel\":\"Passed\",\"count\":1},\"hash\":{\"customLabel\":\"Hash\",\"count\":1},\"machineName\":{\"customLabel\":\"Machine\",\"count\":1},\"startTime\":{\"customLabel\":\"Started\"},\"status\":{\"customLabel\":\"Status\"}}","fieldFormatMap":"{\"summary.time\":{\"id\":\"duration\",\"params\":{\"outputFormat\":\"humanizePrecise\"}},\"status\":{\"id\":\"color\",\"params\":{\"parsedUrl\":{\"origin\":\"http://localhost:5601\",\"pathname\":\"/app/management/kibana/objects\",\"basePath\":\"\"},\"fieldType\":\"string\",\"colors\":[{\"range\":\"-Infinity:Infinity\",\"regex\":\"Failed\",\"text\":\"#E7664C\",\"background\":\"#ffffff\"},{\"range\":\"-Infinity:Infinity\",\"regex\":\"Passed\",\"text\":\"#54B399\",\"background\":\"#ffffff\"}]}},\"startTime\":{\"id\":\"date\"}}","fields":"[]","name":"Test Runs","runtimeFieldMap":"{}","sourceFilters":"[]","timeFieldName":"startTime","title":"kiboards-testruns-*","typeMeta":"{}"},"coreMigrationVersion":"8.6.2","created_at":"2023-10-16T12:40:01.427Z","id":"ac6ead8c-f3af-455a-a126-ec584b688cd9","migrationVersion":{"index-pattern":"8.0.0"},"references":[],"type":"index-pattern","updated_at":"2023-10-16T12:44:43.131Z","version":"WzI5NDYsMl0="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/res/KiBoards.02.TestRuns.Search.ndjson b/res/KiBoards.02.TestRuns.Search.ndjson new file mode 100644 index 0000000..13ad07e --- /dev/null +++ b/res/KiBoards.02.TestRuns.Search.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"columns":["id","name","status","summary.time","summary.total","summary.passed","summary.failed","summary.skipped","userName","machineName","hash"],"description":"","grid":{"columns":{"context.version":{"width":204},"id":{"width":299},"machineName":{"width":100},"name":{"width":128},"startTime":{"width":210},"status":{"width":100},"summary.failed":{"width":111},"summary.passed":{"width":100},"summary.skipped":{"width":84},"summary.time":{"width":211},"summary.total":{"width":125}}},"hideChart":false,"isTextBasedQuery":false,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["startTime","desc"],["summary.failed","asc"]],"timeRestore":false,"title":"Test Runs","usesAdHocDataView":false},"coreMigrationVersion":"8.6.2","created_at":"2023-10-16T12:40:01.427Z","id":"e8a11710-6a85-11ee-a384-fbd389354190","migrationVersion":{"search":"8.0.0"},"references":[{"id":"ac6ead8c-f3af-455a-a126-ec584b688cd9","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","updated_at":"2023-10-16T12:40:01.427Z","version":"WzI4ODAsMl0="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/src/KiBoards.Xunit/TestExtensions.cs b/src/KiBoards.Xunit/KiBoardsTestExtensions.cs similarity index 93% rename from src/KiBoards.Xunit/TestExtensions.cs rename to src/KiBoards.Xunit/KiBoardsTestExtensions.cs index def4fdb..740a31a 100644 --- a/src/KiBoards.Xunit/TestExtensions.cs +++ b/src/KiBoards.Xunit/KiBoardsTestExtensions.cs @@ -5,7 +5,7 @@ namespace KiBoards { - internal static class TestExtensions + internal static class KiBoardsTestExtensions { public static string ComputeMD5(this string value) => BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(value))).Replace("-", "").ToLower(); public static void WriteMessage(this IMessageSink messageSink, string message) => messageSink.OnMessage(new DiagnosticMessage(message)); diff --git a/src/KiBoards.Xunit/TestStartupAttribute.cs b/src/KiBoards.Xunit/KiboardsTestStartupAttribute.cs similarity index 54% rename from src/KiBoards.Xunit/TestStartupAttribute.cs rename to src/KiBoards.Xunit/KiboardsTestStartupAttribute.cs index cf4a674..003eb4e 100644 --- a/src/KiBoards.Xunit/TestStartupAttribute.cs +++ b/src/KiBoards.Xunit/KiboardsTestStartupAttribute.cs @@ -1,9 +1,9 @@ [AttributeUsage(AttributeTargets.Assembly)] -public class TestStartupAttribute : Attribute +public class KiboardsTestStartupAttribute : Attribute { public string ClassName { get; set; } - public TestStartupAttribute(string className) + public KiboardsTestStartupAttribute(string className) { ClassName = className; } diff --git a/src/KiBoards.Xunit/Services/KiBoardsTestRunner.cs b/src/KiBoards.Xunit/Services/KiBoardsTestRunner.cs index acd4a2e..5188431 100644 --- a/src/KiBoards.Xunit/Services/KiBoardsTestRunner.cs +++ b/src/KiBoards.Xunit/Services/KiBoardsTestRunner.cs @@ -30,7 +30,8 @@ public KiBoardsTestRunner(IMessageSink messageSink) Variables = new Dictionary() }; - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetCustomAttribute() != null)) + var startupAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetCustomAttribute() != null).ToArray(); + foreach (var assembly in startupAssemblies) Startup(assembly, messageSink); foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) @@ -46,7 +47,6 @@ public KiBoardsTestRunner(IMessageSink messageSink) var uriString = Environment.GetEnvironmentVariable("KIB_ELASTICSEARCH_HOST") ?? "http://localhost:9200"; var connectionSettings = new ConnectionSettings(new Uri(uriString)); - var elasticClient = new ElasticClient(connectionSettings .DefaultMappingFor(m => m @@ -73,12 +73,14 @@ public KiBoardsTestRunner(IMessageSink messageSink) private void Startup(Assembly assembly, IMessageSink messageSink) { try - { - var startup = assembly.GetCustomAttribute(); + { + var startup = assembly.GetCustomAttribute(); Type type = assembly.GetType(startup.ClassName); if (type != null) { + messageSink.WriteMessage($"Invoking {type.FullName}"); + if (type.GetConstructor(new Type[] { typeof(string), typeof(IMessageSink) }) != null) Activator.CreateInstance(type, _testRun.Id, messageSink); else if (type.GetConstructor(new Type[] { typeof(string) }) != null) diff --git a/src/KiBoards.Xunit/TestFramework.cs b/src/KiBoards.Xunit/TestFramework.cs index ca7c38e..009934a 100644 --- a/src/KiBoards.Xunit/TestFramework.cs +++ b/src/KiBoards.Xunit/TestFramework.cs @@ -32,9 +32,7 @@ public TestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvid { _testRunner = testRunner; _diagnosticMessageSink = diagnosticMessageSink; - } - - + } protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) { @@ -53,7 +51,6 @@ protected override async void RunTestCases(IEnumerable testCases } - private class TestAssemblyRunner : XunitTestAssemblyRunner { private readonly KiBoardsTestRunner _testRunner; @@ -107,7 +104,6 @@ protected override Task RunTestMethodAsync(ITestMethod testMethod, I } - private class TestResultBus : IMessageBus { private readonly IMessageBus _messageBus; diff --git a/src/KiBoards.sln b/src/KiBoards.sln index d2ff158..cc78be6 100644 --- a/src/KiBoards.sln +++ b/src/KiBoards.sln @@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KiBoards.Xunit", "KiBoards. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KiBoards.Tests", "KiBoards.Tests\KiBoards.Tests.csproj", "{BD23E63C-CC78-4714-A319-E492CC90178A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleTest", "SimpleTest\SimpleTest.csproj", "{EF1D5D1A-6836-4F3C-9BAB-9FD5BEE0EEB5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleTest", "SimpleTest\SimpleTest.csproj", "{EF1D5D1A-6836-4F3C-9BAB-9FD5BEE0EEB5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KiBoards", "KiBoards\KiBoards.csproj", "{BA545090-C497-4AF5-835E-54BFFC1031D1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +29,10 @@ Global {EF1D5D1A-6836-4F3C-9BAB-9FD5BEE0EEB5}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF1D5D1A-6836-4F3C-9BAB-9FD5BEE0EEB5}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF1D5D1A-6836-4F3C-9BAB-9FD5BEE0EEB5}.Release|Any CPU.Build.0 = Release|Any CPU + {BA545090-C497-4AF5-835E-54BFFC1031D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA545090-C497-4AF5-835E-54BFFC1031D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA545090-C497-4AF5-835E-54BFFC1031D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA545090-C497-4AF5-835E-54BFFC1031D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/KiBoards/Extensions.cs b/src/KiBoards/Extensions.cs new file mode 100644 index 0000000..780c444 --- /dev/null +++ b/src/KiBoards/Extensions.cs @@ -0,0 +1,11 @@ +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace KiBoards +{ + internal static class Extensions + { + public static void WriteMessage(this IMessageSink messageSink, string message) => messageSink.OnMessage(new DiagnosticMessage(message)); + public static void WriteException(this IMessageSink messageSink, Exception exception) => messageSink.OnMessage(new DiagnosticMessage("{0}\n{1}", exception.Message, exception.StackTrace)); + } +} diff --git a/src/KiBoards/KiBoards.csproj b/src/KiBoards/KiBoards.csproj new file mode 100644 index 0000000..a75cb55 --- /dev/null +++ b/src/KiBoards/KiBoards.csproj @@ -0,0 +1,58 @@ + + + + netstandard2.1 + ..\..\bin + enable + 10 + true + + + + none + false + + + + Matt Janda + icon.png + LICENSE + README.md + KiBoards + KiBoards + KiBoards + KiBoards offers the capability to visualise test cases and test run in Kibana. + KiBoards Xunit Kibana Elasticsearch + https://github.com/Jandini/KiBoards + https://github.com/Jandini/KiBoards + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/KiBoards/KiBoardsSavedObjectsAttribute.cs b/src/KiBoards/KiBoardsSavedObjectsAttribute.cs new file mode 100644 index 0000000..a453920 --- /dev/null +++ b/src/KiBoards/KiBoardsSavedObjectsAttribute.cs @@ -0,0 +1,11 @@ +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] +public class KiBoardsSavedObjectsAttribute : Attribute +{ + public bool Overwrite { get; set; } + public string SearchPattern { get; set; } + + public KiBoardsSavedObjectsAttribute(string searchPattern = "*.ndjson") + { + SearchPattern = searchPattern; + } +} \ No newline at end of file diff --git a/src/KiBoards/Models/Objects/ImportObjectsErrors.cs b/src/KiBoards/Models/Objects/ImportObjectsErrors.cs new file mode 100644 index 0000000..80f14f2 --- /dev/null +++ b/src/KiBoards/Models/Objects/ImportObjectsErrors.cs @@ -0,0 +1,8 @@ +namespace KiBoards.Models.Objects +{ + class ImportObjectsErrors + { + public string Type { get; set; } + public string Id { get; set; } + } +} diff --git a/src/KiBoards/Models/Objects/ImportSavedObjectsResponse.cs b/src/KiBoards/Models/Objects/ImportSavedObjectsResponse.cs new file mode 100644 index 0000000..faa6c61 --- /dev/null +++ b/src/KiBoards/Models/Objects/ImportSavedObjectsResponse.cs @@ -0,0 +1,9 @@ +namespace KiBoards.Models.Objects +{ + class ImportObjectsResponse + { + public int SuccessCount { get; set; } + public bool Success { get; set; } + public List Errors { get; set; } + } +} diff --git a/src/KiBoards/Models/Settings/KibanaSettingsChanges.cs b/src/KiBoards/Models/Settings/KibanaSettingsChanges.cs new file mode 100644 index 0000000..9d5720c --- /dev/null +++ b/src/KiBoards/Models/Settings/KibanaSettingsChanges.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace KiBoards.Models.Settings +{ + class KibanaSettingsChanges + { + [JsonPropertyName("theme:darkMode")] + public bool? ThemeDarkMode { get; set; } + } +} \ No newline at end of file diff --git a/src/KiBoards/Models/Settings/KibanaSettingsRequest.cs b/src/KiBoards/Models/Settings/KibanaSettingsRequest.cs new file mode 100644 index 0000000..7a0d55b --- /dev/null +++ b/src/KiBoards/Models/Settings/KibanaSettingsRequest.cs @@ -0,0 +1,7 @@ +namespace KiBoards.Models.Settings +{ + class KibanaSettingsRequest + { + public KibanaSettingsChanges Changes { get; set; } + } +} diff --git a/src/KiBoards/Models/Status/KibanaStatus.cs b/src/KiBoards/Models/Status/KibanaStatus.cs new file mode 100644 index 0000000..72d8200 --- /dev/null +++ b/src/KiBoards/Models/Status/KibanaStatus.cs @@ -0,0 +1,7 @@ +namespace KiBoards.Models.Status +{ + class KibanaStatus + { + public KibanaStatusOverall Overall { get; set; } + } +} diff --git a/src/KiBoards/Models/Status/KibanaStatusOverall.cs b/src/KiBoards/Models/Status/KibanaStatusOverall.cs new file mode 100644 index 0000000..1b014ca --- /dev/null +++ b/src/KiBoards/Models/Status/KibanaStatusOverall.cs @@ -0,0 +1,8 @@ +namespace KiBoards.Models.Status +{ + class KibanaStatusOverall + { + public string Level { get; set; } + public string Summary { get; set; } + } +} diff --git a/src/KiBoards/Models/Status/KibanaStatusResponse.cs b/src/KiBoards/Models/Status/KibanaStatusResponse.cs new file mode 100644 index 0000000..206de2e --- /dev/null +++ b/src/KiBoards/Models/Status/KibanaStatusResponse.cs @@ -0,0 +1,10 @@ +namespace KiBoards.Models.Status +{ + class KibanaStatusResponse + { + public string Name { get; set; } + public string Uuid { get; set; } + public KibanaVersion Version { get; set; } + public KibanaStatus Status { get; set; } + } +} diff --git a/src/KiBoards/Models/Status/KibanaVersion.cs b/src/KiBoards/Models/Status/KibanaVersion.cs new file mode 100644 index 0000000..1d08fa3 --- /dev/null +++ b/src/KiBoards/Models/Status/KibanaVersion.cs @@ -0,0 +1,10 @@ +namespace KiBoards.Models.Status +{ + class KibanaVersion + { + public string Number { get; set; } + public string BuildHash { get; set; } + public int BuildNumber { get; set; } + public bool BuildSnapshot { get; set; } + } +} diff --git a/src/KiBoards/Services/KiBoardsKibanaClient.cs b/src/KiBoards/Services/KiBoardsKibanaClient.cs new file mode 100644 index 0000000..10baaa2 --- /dev/null +++ b/src/KiBoards/Services/KiBoardsKibanaClient.cs @@ -0,0 +1,47 @@ +using System.Net.Http.Json; +using System.Text.Json; +using KiBoards.Models.Objects; +using KiBoards.Models.Settings; +using KiBoards.Models.Status; + +namespace KiBoards.Services +{ + internal class KiBoardsKibanaClient + { + private readonly HttpClient _httpClient; + + public KiBoardsKibanaClient(Uri kibanaUri, HttpClient httpClinet) + { + _httpClient = httpClinet; + _httpClient.BaseAddress = kibanaUri; + _httpClient.DefaultRequestHeaders.Add("kbn-xsrf", "true"); + } + + public async Task SetDarkModeAsync(bool darkMode, CancellationToken cancellationToken) + { + var content = JsonContent.Create(new KibanaSettingsRequest() { Changes = new KibanaSettingsChanges() { ThemeDarkMode = darkMode } }); + var response = await _httpClient.PostAsync("api/kibana/settings", content); + response.EnsureSuccessStatusCode(); + } + + + public async Task ImportSavedObjectsAsync(string ndjsonFile) => await ImportSavedObjectsAsync(ndjsonFile, false, CancellationToken.None); + public async Task ImportSavedObjectsAsync(string ndjsonFile, bool overwrite) => await ImportSavedObjectsAsync(ndjsonFile, overwrite, CancellationToken.None); + public async Task ImportSavedObjectsAsync(string ndjsonFile, bool overwrite, CancellationToken cancellationToken) + { + var multipartContent = new MultipartFormDataContent(); + var streamContent = new StreamContent(File.Open(ndjsonFile, FileMode.Open)); + multipartContent.Add(streamContent, "file", ndjsonFile); + var response = await _httpClient.PostAsync($"/api/saved_objects/_import?overwrite={overwrite.ToString().ToLower()}", multipartContent); + + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }, cancellationToken); + + return result; + } + + + public async Task GetStatus() => await GetStatus(CancellationToken.None); + public async Task GetStatus(CancellationToken cancellationToken) => await _httpClient.GetFromJsonAsync("api/status", cancellationToken); + } +} diff --git a/src/KiBoards/Startup.cs b/src/KiBoards/Startup.cs new file mode 100644 index 0000000..47d25cc --- /dev/null +++ b/src/KiBoards/Startup.cs @@ -0,0 +1,66 @@ +using KiBoards.Services; +using System.Reflection; +using Xunit.Abstractions; + +[assembly: KiboardsTestStartup("KiBoards.Startup")] + +namespace KiBoards +{ + public class Startup + { + + public Startup(IMessageSink messageSink) + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetCustomAttribute() != null); + + foreach (var assembly in assemblies) + { + var attribute = assembly.GetCustomAttribute(); + + var task = Task.Factory.StartNew(async () => + { + var httpClient = new HttpClient(); + var kibanaUri = new Uri(Environment.GetEnvironmentVariable("KIB_KIBANA_HOST") ?? "http://localhost:5601"); + var kibanaClient = new KiBoardsKibanaClient(kibanaUri, httpClient); + + messageSink.WriteMessage($"Waiting for Kibana {kibanaUri}"); + + while (true) + { + try + { + var response = await kibanaClient.GetStatus(CancellationToken.None); + + string level = response?.Status?.Overall?.Level ?? throw new Exception("Kibana status is not available."); + + if (level != "available") + throw new Exception("Kibana not available."); + + messageSink.WriteMessage($"Kibana status: {level}"); + break; + } + catch (Exception ex) + { + messageSink.WriteMessage(ex.Message); + await Task.Delay(5000); + } + } + + var ndjsonFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), attribute.SearchPattern); + + messageSink.WriteMessage($"Found {ndjsonFiles.Length} ndjson file(s)"); + + foreach (var ndjsonFile in ndjsonFiles.OrderBy(a => a)) + { + messageSink.WriteMessage($"Imporing {Path.GetFileName(ndjsonFile)}"); + var results = await kibanaClient.ImportSavedObjectsAsync(ndjsonFile, attribute.Overwrite); + messageSink.WriteMessage($"Imported {results.SuccessCount} object(s)"); + + if (!results.Success && results.SuccessCount > 0) + messageSink.WriteMessage("Warning: Some objects were not imported. Please ensure proper import order based on their dependencies."); + } + }); + } + } + } +} diff --git a/src/SimpleTest/SimpleTest.csproj b/src/SimpleTest/SimpleTest.csproj index bf2b4d2..0145354 100644 --- a/src/SimpleTest/SimpleTest.csproj +++ b/src/SimpleTest/SimpleTest.csproj @@ -9,6 +9,15 @@ true + + + PreserveNewest + + + PreserveNewest + + + @@ -23,7 +32,7 @@ - + diff --git a/src/SimpleTest/Simple_Must.cs b/src/SimpleTest/Simple_Must.cs index fc5f52e..d784638 100644 --- a/src/SimpleTest/Simple_Must.cs +++ b/src/SimpleTest/Simple_Must.cs @@ -1,12 +1,13 @@ +[assembly: KiBoardsSavedObjects()] +[assembly: KiboardsTestStartup("SimpleTest.Startup")] [assembly: TestFramework("KiBoards.TestFramework", "KiBoards.Xunit")] -[assembly: TestStartup("SimpleTest.Startup")] namespace SimpleTest { public class Simple_Must { [Theory] - [InlineData(1, 2)] + [InlineData(0, 2)] public void Not_DivideByZero(int a, int b) { var c = 1 / b; @@ -18,6 +19,8 @@ public void Not_DivideByZero(int a, int b) public void Pass() { Assert.Equal(0, 0); + + Thread.Sleep(6000); } } } \ No newline at end of file