diff --git a/res/MessageBus.drawio b/res/MessageBus.drawio
new file mode 100644
index 0000000..e4da87a
--- /dev/null
+++ b/res/MessageBus.drawio
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/MessageBus.png b/res/MessageBus.png
new file mode 100644
index 0000000..1f0d121
Binary files /dev/null and b/res/MessageBus.png differ
diff --git a/res/MessageBus.txt b/res/MessageBus.txt
new file mode 100644
index 0000000..d530433
--- /dev/null
+++ b/res/MessageBus.txt
@@ -0,0 +1,66 @@
+[xUnit.net 00:00:02.64] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodStarting
+
+[xUnit.net 00:00:02.67] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseStarting
+[xUnit.net 00:00:02.67] KiBoards.Tests: MessageBus: Xunit.Sdk.TestStarting
+[xUnit.net 00:00:02.67] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionStarting
+[xUnit.net 00:00:02.67] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionFinished
+[xUnit.net 00:00:02.68] KiBoards.Tests: MessageBus: Xunit.Sdk.TestPassed
+[xUnit.net 00:00:02.68] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFinished
+[xUnit.net 00:00:02.68] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseFinished
+
+[xUnit.net 00:00:02.71] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseStarting
+[xUnit.net 00:00:02.71] KiBoards.Tests: MessageBus: Xunit.Sdk.TestStarting
+[xUnit.net 00:00:02.71] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionStarting
+[xUnit.net 00:00:02.71] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionFinished
+[xUnit.net 00:00:02.72] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFailed
+[xUnit.net 00:00:02.72] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFinished
+[xUnit.net 00:00:02.72] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseFinished
+
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseStarting
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestStarting
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionStarting
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionFinished
+
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestPassed
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFinished
+
+[xUnit.net 00:00:02.73] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseFinished
+
+[xUnit.net 00:00:02.74] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodFinished
+
+
+[xUnit.net 00:00:02.74] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodStarting
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseStarting
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestStarting
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionStarting
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionFinished
+
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestPassed
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFinished
+
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseFinished
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodFinished
+
+[xUnit.net 00:00:02.75] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodStarting
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseStarting
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestStarting
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionStarting
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestClassConstructionFinished
+
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFailed
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFinished
+
+[xUnit.net 00:00:02.76] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseFinished
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodFinished
+
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodStarting
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseStarting
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestStarting
+
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestSkipped
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestFinished
+
+[xUnit.net 00:00:02.77] KiBoards.Tests: MessageBus: Xunit.Sdk.TestCaseFinished
+[xUnit.net 00:00:02.78] KiBoards.Tests: MessageBus: Xunit.Sdk.TestMethodFinished
+
+Failed! - Failed: 2, Passed: 3, Skipped: 1, Total: 6, Duration: 85 ms - KiBoards.Tests.dll (net7.0)
diff --git a/res/TestCaseStatus.ndjson b/res/TestCaseStatus.ndjson
new file mode 100644
index 0000000..87d6a87
--- /dev/null
+++ b/res/TestCaseStatus.ndjson
@@ -0,0 +1,5 @@
+{"attributes":{"fieldAttrs":"{\"displayName\":{\"count\":2},\"state\":{\"count\":1},\"uniqueId\":{\"customLabel\":\"Id\",\"count\":1},\"status\":{\"customLabel\":\"Status\",\"count\":1},\"summary.time\":{\"count\":1}}","fieldFormatMap":"{\"status\":{\"id\":\"color\",\"params\":{\"parsedUrl\":{\"origin\":\"http://localhost:5601\",\"pathname\":\"/app/home\",\"basePath\":\"\"},\"fieldType\":\"string\",\"colors\":[{\"range\":\"-Infinity:Infinity\",\"regex\":\"Success\",\"text\":\"#459b83\",\"background\":\"#ffffff\"},{\"range\":\"-Infinity:Infinity\",\"regex\":\"Failure\",\"text\":\"#E7664C\",\"background\":\"#ffffff\"},{\"range\":\"-Infinity:Infinity\",\"regex\":\"Skipped\",\"text\":\"#D6BF57\",\"background\":\"#ffffff\"}]}}}","fields":"[]","name":"Test Case Status View","runtimeFieldMap":"{}","sourceFilters":"[]","timeFieldName":"","title":"kiboards-testcase-status-*","typeMeta":"{}"},"coreMigrationVersion":"8.8.0","created_at":"2023-09-21T21:40:51.247Z","id":"641b8927-249c-4669-9dea-a6bf989c8556","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2023-09-21T21:40:51.247Z","version":"WzUsMl0="}
+{"attributes":{"columns":["uniqueId","status","displayName"],"description":"","grid":{"columns":{"status":{"width":86},"uniqueId":{"width":313}}},"hideChart":false,"isTextBasedQuery":false,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"rowHeight":-1,"sort":[],"timeRestore":false,"title":"Test Case Status Search","usesAdHocDataView":false,"viewMode":"documents"},"coreMigrationVersion":"8.8.0","created_at":"2023-09-21T21:40:51.247Z","id":"fd2b4c00-34a7-11ee-8f8e-e312ee55ca24","managed":false,"references":[{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","typeMigrationVersion":"8.0.0","updated_at":"2023-09-21T21:40:51.247Z","version":"WzYsMl0="}
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.8.2\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":0,\"w\":48,\"h\":29,\"i\":\"f0134ca4-cbcd-483e-9107-6e93fba7de2f\"},\"panelIndex\":\"f0134ca4-cbcd-483e-9107-6e93fba7de2f\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Details\",\"panelRefName\":\"panel_f0134ca4-cbcd-483e-9107-6e93fba7de2f\"}]","timeRestore":false,"title":"Test Case Details","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-09-21T21:40:51.247Z","id":"00115f80-34a9-11ee-8f8e-e312ee55ca24","managed":false,"references":[{"id":"fd2b4c00-34a7-11ee-8f8e-e312ee55ca24","name":"f0134ca4-cbcd-483e-9107-6e93fba7de2f:panel_f0134ca4-cbcd-483e-9107-6e93fba7de2f","type":"search"}],"type":"dashboard","typeMigrationVersion":"8.9.0","updated_at":"2023-09-21T21:40:51.247Z","version":"WzcsMl0="}
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":8,\"h\":6,\"i\":\"10c370c6-5da2-4507-ad86-af1119649a69\"},\"panelIndex\":\"10c370c6-5da2-4507-ad86-af1119649a69\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"be565c3a-655e-4ed9-97f2-7b4435618318\",\"accessor\":\"c442ed00-679d-451f-b0b1-da6b8674d186\",\"layerType\":\"data\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#afb8c6\",\"stop\":1},{\"color\":\"#343741\",\"stop\":20}],\"continuity\":\"above\",\"maxSteps\":5,\"colorStops\":[{\"color\":\"#afb8c6\",\"stop\":0},{\"color\":\"#343741\",\"stop\":1}]}},\"textAlign\":\"center\",\"size\":\"l\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"be565c3a-655e-4ed9-97f2-7b4435618318\":{\"columns\":{\"c442ed00-679d-451f-b0b1-da6b8674d186\":{\"label\":\"Total\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0,\"suffix\":\"\"}}},\"customLabel\":true}},\"columnOrder\":[\"c442ed00-679d-451f-b0b1-da6b8674d186\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":true,\"description\":\"Currently running test cases\",\"enhancements\":{}},\"title\":\"\"},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":8,\"y\":0,\"w\":8,\"h\":6,\"i\":\"0b4751ed-1f30-4405-9801-1ccf24a0b956\"},\"panelIndex\":\"0b4751ed-1f30-4405-9801-1ccf24a0b956\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"be565c3a-655e-4ed9-97f2-7b4435618318\",\"accessor\":\"c442ed00-679d-451f-b0b1-da6b8674d186\",\"layerType\":\"data\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#afb8c6\",\"stop\":20},{\"color\":\"#6d7482\",\"stop\":50},{\"color\":\"#343741\",\"stop\":100}],\"continuity\":\"above\",\"maxSteps\":5,\"colorStops\":[{\"color\":\"#afb8c6\",\"stop\":0},{\"color\":\"#6d7482\",\"stop\":20},{\"color\":\"#343741\",\"stop\":50}]}},\"titlePosition\":\"top\",\"size\":\"l\",\"textAlign\":\"center\"},\"query\":{\"query\":\"status.keyword : \\\"Discovered\\\" \",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"be565c3a-655e-4ed9-97f2-7b4435618318\":{\"columns\":{\"c442ed00-679d-451f-b0b1-da6b8674d186\":{\"label\":\"Discovered\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0,\"suffix\":\"\"}}},\"customLabel\":true}},\"columnOrder\":[\"c442ed00-679d-451f-b0b1-da6b8674d186\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":true,\"description\":\"Currently running test cases\",\"enhancements\":{}},\"title\":\"\"},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":16,\"y\":0,\"w\":8,\"h\":6,\"i\":\"408ce981-f6e4-48c8-85c0-78231aa10b65\"},\"panelIndex\":\"408ce981-f6e4-48c8-85c0-78231aa10b65\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"be565c3a-655e-4ed9-97f2-7b4435618318\",\"accessor\":\"c442ed00-679d-451f-b0b1-da6b8674d186\",\"layerType\":\"data\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#afb8c6\",\"stop\":20},{\"color\":\"#6d7482\",\"stop\":50},{\"color\":\"#343741\",\"stop\":100}],\"continuity\":\"above\",\"maxSteps\":5,\"colorStops\":[{\"color\":\"#afb8c6\",\"stop\":0},{\"color\":\"#6d7482\",\"stop\":20},{\"color\":\"#343741\",\"stop\":50}]}},\"textAlign\":\"center\",\"size\":\"l\"},\"query\":{\"query\":\"status.keyword : \\\"Running\\\" \",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"be565c3a-655e-4ed9-97f2-7b4435618318\":{\"columns\":{\"c442ed00-679d-451f-b0b1-da6b8674d186\":{\"label\":\"Running\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0,\"suffix\":\"\"}}},\"customLabel\":true}},\"columnOrder\":[\"c442ed00-679d-451f-b0b1-da6b8674d186\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":true,\"description\":\"Currently running test cases\",\"enhancements\":{}},\"title\":\"Running\"},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":8,\"h\":6,\"i\":\"ba96163c-6fff-4602-bbe1-bd6e4c1d01b7\"},\"panelIndex\":\"ba96163c-6fff-4602-bbe1-bd6e4c1d01b7\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"be565c3a-655e-4ed9-97f2-7b4435618318\",\"accessor\":\"c442ed00-679d-451f-b0b1-da6b8674d186\",\"layerType\":\"data\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#00000054\",\"stop\":1},{\"color\":\"#209280\",\"stop\":18}],\"continuity\":\"above\",\"maxSteps\":5,\"colorStops\":[{\"color\":\"#00000054\",\"stop\":0},{\"color\":\"#209280\",\"stop\":1}]}},\"textAlign\":\"center\",\"size\":\"l\"},\"query\":{\"query\":\"status.keyword : \\\"Success\\\" \",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"be565c3a-655e-4ed9-97f2-7b4435618318\":{\"columns\":{\"c442ed00-679d-451f-b0b1-da6b8674d186\":{\"label\":\"Success\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0,\"suffix\":\"\"}}},\"customLabel\":true}},\"columnOrder\":[\"c442ed00-679d-451f-b0b1-da6b8674d186\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":true,\"description\":\"Currently running test cases\",\"enhancements\":{}},\"title\":\"\"},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":32,\"y\":0,\"w\":8,\"h\":6,\"i\":\"446f37b5-2f7f-4814-8d3f-f7b1cae9e007\"},\"panelIndex\":\"446f37b5-2f7f-4814-8d3f-f7b1cae9e007\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"be565c3a-655e-4ed9-97f2-7b4435618318\",\"accessor\":\"c442ed00-679d-451f-b0b1-da6b8674d186\",\"layerType\":\"data\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#27987e\",\"stop\":1},{\"color\":\"#E7664C\",\"stop\":18}],\"continuity\":\"above\",\"maxSteps\":5,\"colorStops\":[{\"color\":\"#27987e\",\"stop\":0},{\"color\":\"#E7664C\",\"stop\":1}]}},\"textAlign\":\"center\",\"size\":\"l\"},\"query\":{\"query\":\"status.keyword : \\\"Failure\\\" \",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"be565c3a-655e-4ed9-97f2-7b4435618318\":{\"columns\":{\"c442ed00-679d-451f-b0b1-da6b8674d186\":{\"label\":\"Failure\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0,\"suffix\":\"\"}}},\"customLabel\":true}},\"columnOrder\":[\"c442ed00-679d-451f-b0b1-da6b8674d186\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":true,\"description\":\"Currently running test cases\",\"enhancements\":{}},\"title\":\"\"},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":40,\"y\":0,\"w\":8,\"h\":6,\"i\":\"f33640a2-03d3-4cb8-9514-ac7eddb8342c\"},\"panelIndex\":\"f33640a2-03d3-4cb8-9514-ac7eddb8342c\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"layerId\":\"be565c3a-655e-4ed9-97f2-7b4435618318\",\"accessor\":\"c442ed00-679d-451f-b0b1-da6b8674d186\",\"layerType\":\"data\",\"colorMode\":\"Labels\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":3,\"name\":\"custom\",\"reverse\":false,\"rangeType\":\"number\",\"rangeMin\":0,\"rangeMax\":null,\"progression\":\"fixed\",\"stops\":[{\"color\":\"#0000003b\",\"stop\":1},{\"color\":\"#D6BF57\",\"stop\":100}],\"continuity\":\"above\",\"maxSteps\":5,\"colorStops\":[{\"color\":\"#0000003b\",\"stop\":0},{\"color\":\"#D6BF57\",\"stop\":1}]}},\"textAlign\":\"center\",\"size\":\"l\"},\"query\":{\"query\":\"status.keyword : \\\"Skipped\\\" \",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"be565c3a-655e-4ed9-97f2-7b4435618318\":{\"columns\":{\"c442ed00-679d-451f-b0b1-da6b8674d186\":{\"label\":\"Skipped\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0,\"suffix\":\"\"}}},\"customLabel\":true}},\"columnOrder\":[\"c442ed00-679d-451f-b0b1-da6b8674d186\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":true,\"description\":\"Currently running test cases\",\"enhancements\":{}},\"title\":\"\"},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":6,\"w\":16,\"h\":15,\"i\":\"2be22eb0-e753-4d1d-8ec5-bdde80a69fd5\"},\"panelIndex\":\"2be22eb0-e753-4d1d-8ec5-bdde80a69fd5\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Test Cases\",\"description\":\"\",\"visualizationType\":\"lnsPie\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-6f0b74bd-09b6-4d1a-a8b5-92e6cf653216\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"shape\":\"donut\",\"palette\":{\"type\":\"palette\",\"name\":\"positive\"},\"layers\":[{\"layerId\":\"6f0b74bd-09b6-4d1a-a8b5-92e6cf653216\",\"primaryGroups\":[\"6fc4244c-c2d2-4558-8016-cc3d656f4dba\"],\"metrics\":[\"742e3600-35ca-4446-aac6-519f26cb3b13\"],\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"hide\",\"nestedLegend\":false,\"layerType\":\"data\",\"emptySizeRatio\":0.54,\"truncateLegend\":true,\"legendPosition\":\"left\",\"allowMultipleMetrics\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"6f0b74bd-09b6-4d1a-a8b5-92e6cf653216\":{\"columns\":{\"6fc4244c-c2d2-4558-8016-cc3d656f4dba\":{\"label\":\"Top 6 values of status.keyword\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"status.keyword\",\"isBucketed\":true,\"params\":{\"size\":6,\"orderBy\":{\"type\":\"column\",\"columnId\":\"742e3600-35ca-4446-aac6-519f26cb3b13\"},\"orderDirection\":\"asc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}},\"742e3600-35ca-4446-aac6-519f26cb3b13\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true}}},\"columnOrder\":[\"6fc4244c-c2d2-4558-8016-cc3d656f4dba\",\"742e3600-35ca-4446-aac6-519f26cb3b13\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"bf63f130-7338-4092-ba4d-8b2b9a9d478c\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\",\"name\":\"Go to test case details...\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":false,\"openInNewTab\":false}}}]}},\"hidePanelTitles\":true}},{\"version\":\"8.9.2\",\"type\":\"visualization\",\"gridData\":{\"x\":16,\"y\":6,\"w\":16,\"h\":15,\"i\":\"873943d5-ace4-4e99-bbd2-221623eaf293\"},\"panelIndex\":\"873943d5-ace4-4e99-bbd2-221623eaf293\",\"embeddableConfig\":{\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":26,\"maxFontSize\":72,\"showLabel\":false,\"palette\":{\"type\":\"palette\",\"name\":\"status\"}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{\"emptyAsNull\":false},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"status.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":true,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"includeIsRegex\":true,\"excludeIsRegex\":true},\"schema\":\"segment\"}],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"a3885ac3-b583-429d-bfbd-36aa4b5ac2a3\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\",\"name\":\"Go to Dashboard\",\"config\":{\"useCurrentFilters\":false,\"useCurrentDateRange\":false,\"openInNewTab\":false}}}]}}}},{\"version\":\"8.9.2\",\"type\":\"lens\",\"gridData\":{\"x\":32,\"y\":6,\"w\":16,\"h\":15,\"i\":\"d0939bca-79c1-4c67-bdd6-51c445abcf56\"},\"panelIndex\":\"d0939bca-79c1-4c67-bdd6-51c445abcf56\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Test Cases\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"641b8927-249c-4669-9dea-a6bf989c8556\",\"name\":\"indexpattern-datasource-layer-6f0b74bd-09b6-4d1a-a8b5-92e6cf653216\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":false,\"position\":\"bottom\",\"showSingleSeries\":false,\"isInside\":false,\"shouldTruncate\":false},\"valueLabels\":\"show\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":false,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":false,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":false,\"yLeft\":false,\"yRight\":true},\"preferredSeriesType\":\"bar_horizontal_stacked\",\"layers\":[{\"layerId\":\"6f0b74bd-09b6-4d1a-a8b5-92e6cf653216\",\"seriesType\":\"bar_horizontal_stacked\",\"xAccessor\":\"6fc4244c-c2d2-4558-8016-cc3d656f4dba\",\"accessors\":[\"742e3600-35ca-4446-aac6-519f26cb3b13\",\"64851214-2c34-44e3-838b-0af27323929b\",\"56018286-5231-4400-8d02-7653525b322a\",\"ee233af9-d011-4bcd-a19e-275aaf9f36f1\",\"2724574b-d6f9-4f13-8eb0-646a4b863619\"],\"yConfig\":[{\"forAccessor\":\"64851214-2c34-44e3-838b-0af27323929b\",\"color\":\"#54b399\"},{\"forAccessor\":\"742e3600-35ca-4446-aac6-519f26cb3b13\",\"color\":\"#e7664c\"},{\"forAccessor\":\"56018286-5231-4400-8d02-7653525b322a\",\"color\":\"#d6bf57\"},{\"forAccessor\":\"ee233af9-d011-4bcd-a19e-275aaf9f36f1\",\"color\":\"#aaaaaa\",\"axisMode\":\"auto\"},{\"forAccessor\":\"2724574b-d6f9-4f13-8eb0-646a4b863619\",\"color\":\"#cccccc\"}],\"layerType\":\"data\"}],\"yLeftExtent\":{\"mode\":\"full\",\"niceValues\":false}},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"6f0b74bd-09b6-4d1a-a8b5-92e6cf653216\":{\"columns\":{\"6fc4244c-c2d2-4558-8016-cc3d656f4dba\":{\"label\":\"Status\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"status.keyword\",\"isBucketed\":true,\"params\":{\"size\":6,\"orderBy\":{\"type\":\"custom\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false,\"orderAgg\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"params\":{\"emptyAsNull\":true}},\"secondaryFields\":[]},\"customLabel\":true},\"742e3600-35ca-4446-aac6-519f26cb3b13\":{\"label\":\"Failure\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"status.keyword\",\"params\":{\"emptyAsNull\":false,\"format\":{\"id\":\"number\",\"params\":{\"decimals\":0}}},\"customLabel\":true,\"filter\":{\"query\":\"status.keyword : \\\"Failure\\\" \",\"language\":\"kuery\"}},\"64851214-2c34-44e3-838b-0af27323929b\":{\"label\":\"Success\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"status.keyword\",\"filter\":{\"query\":\"status.keyword : \\\"Success\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"56018286-5231-4400-8d02-7653525b322a\":{\"label\":\"Skipped\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"status.keyword\",\"filter\":{\"query\":\"status.keyword : \\\"Skipped\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"ee233af9-d011-4bcd-a19e-275aaf9f36f1\":{\"label\":\"Running\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"status.keyword\",\"filter\":{\"query\":\"status.keyword : \\\"Running\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"2724574b-d6f9-4f13-8eb0-646a4b863619\":{\"label\":\"Discovered\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"status.keyword\",\"filter\":{\"query\":\"status.keyword : \\\"Discovered\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true}},\"columnOrder\":[\"6fc4244c-c2d2-4558-8016-cc3d656f4dba\",\"742e3600-35ca-4446-aac6-519f26cb3b13\",\"64851214-2c34-44e3-838b-0af27323929b\",\"56018286-5231-4400-8d02-7653525b322a\",\"ee233af9-d011-4bcd-a19e-275aaf9f36f1\",\"2724574b-d6f9-4f13-8eb0-646a4b863619\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"2b53c149-d516-401e-a487-c6efda1ade02\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\",\"name\":\"Go to test case details...\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":false,\"openInNewTab\":false}}}]}},\"hidePanelTitles\":true},\"title\":\"\"}]","timeRestore":false,"title":"Test Case Status","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-09-21T22:06:31.557Z","id":"1b934de0-34a9-11ee-8f8e-e312ee55ca24","managed":false,"references":[{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"10c370c6-5da2-4507-ad86-af1119649a69:indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318","type":"index-pattern"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"0b4751ed-1f30-4405-9801-1ccf24a0b956:indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318","type":"index-pattern"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"408ce981-f6e4-48c8-85c0-78231aa10b65:indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318","type":"index-pattern"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"ba96163c-6fff-4602-bbe1-bd6e4c1d01b7:indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318","type":"index-pattern"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"446f37b5-2f7f-4814-8d3f-f7b1cae9e007:indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318","type":"index-pattern"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"f33640a2-03d3-4cb8-9514-ac7eddb8342c:indexpattern-datasource-layer-be565c3a-655e-4ed9-97f2-7b4435618318","type":"index-pattern"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"2be22eb0-e753-4d1d-8ec5-bdde80a69fd5:indexpattern-datasource-layer-6f0b74bd-09b6-4d1a-a8b5-92e6cf653216","type":"index-pattern"},{"id":"00115f80-34a9-11ee-8f8e-e312ee55ca24","name":"2be22eb0-e753-4d1d-8ec5-bdde80a69fd5:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:bf63f130-7338-4092-ba4d-8b2b9a9d478c:dashboardId","type":"dashboard"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"873943d5-ace4-4e99-bbd2-221623eaf293:kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"00115f80-34a9-11ee-8f8e-e312ee55ca24","name":"873943d5-ace4-4e99-bbd2-221623eaf293:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:a3885ac3-b583-429d-bfbd-36aa4b5ac2a3:dashboardId","type":"dashboard"},{"id":"641b8927-249c-4669-9dea-a6bf989c8556","name":"d0939bca-79c1-4c67-bdd6-51c445abcf56:indexpattern-datasource-layer-6f0b74bd-09b6-4d1a-a8b5-92e6cf653216","type":"index-pattern"},{"id":"00115f80-34a9-11ee-8f8e-e312ee55ca24","name":"d0939bca-79c1-4c67-bdd6-51c445abcf56:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:2b53c149-d516-401e-a487-c6efda1ade02:dashboardId","type":"dashboard"}],"type":"dashboard","typeMigrationVersion":"8.9.0","updated_at":"2023-09-21T22:06:31.557Z","version":"WzE2OSwyXQ=="}
+{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":4,"missingRefCount":0,"missingReferences":[]}
\ No newline at end of file
diff --git a/src/KiBoards.Tests/KiBoards.Tests.csproj b/src/KiBoards.Tests/KiBoards.Tests.csproj
new file mode 100644
index 0000000..ae75601
--- /dev/null
+++ b/src/KiBoards.Tests/KiBoards.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net7.0
+ enable
+ enable
+ en
+ ..\..\bin
+ false
+ true
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/KiBoards.Tests/UnitTest1.cs b/src/KiBoards.Tests/UnitTest1.cs
new file mode 100644
index 0000000..8ffea7b
--- /dev/null
+++ b/src/KiBoards.Tests/UnitTest1.cs
@@ -0,0 +1,52 @@
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+[assembly: TestFramework("KiBoards.TestFramework", "KiBoards")]
+
+namespace KiBoards.Tests
+{
+
+ public class UnitTest1 : IClassFixture
+ {
+ readonly TestContextFixture _testContextFixture;
+ readonly ITestOutputHelper _testOutputHelper;
+
+ public UnitTest1(TestContextFixture testContextFixture, ITestOutputHelper testOutputHelper)
+ {
+ _testContextFixture = testContextFixture;
+ _testOutputHelper = testOutputHelper;
+
+ _testContextFixture.SetContext(new { Version = "Context via Fixture", Hello = "World", Input = 1 });
+ }
+
+ [Fact]
+ public void Test1()
+ {
+
+ var testCase = _testOutputHelper.GetTestCase();
+
+ _testContextFixture.SetContext(new { Version = "12345", TestCase = testCase }) ;
+ Thread.Sleep(1000);
+ }
+
+ [Fact]
+ public void Test2()
+ {
+ Assert.Equal(1, 2);
+ }
+
+
+ [Fact]
+ public void Test3()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Fact(Skip = "Not required.")]
+ public void Test4()
+ {
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/KiBoards.Tests/UnitTest2.cs b/src/KiBoards.Tests/UnitTest2.cs
new file mode 100644
index 0000000..19ea6ec
--- /dev/null
+++ b/src/KiBoards.Tests/UnitTest2.cs
@@ -0,0 +1,53 @@
+
+using Microsoft.VisualStudio.TestPlatform.Utilities;
+using System.Reflection;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards.Tests
+{
+
+ public class UnitTest2 : IClassFixture
+ {
+
+ public UnitTest2(TestContextFixture testContextFixture, ITestOutputHelper outputHelper)
+ {
+ // This is how to get messageBus
+
+ testContextFixture.SetContext(new { Version = "1234" });
+
+ outputHelper.WriteLine("HELLO WORLD MESSAGE BUS");
+ }
+
+ [Fact(Timeout = 1000)]
+ public void Test5()
+ {
+ Thread.Sleep(5000);
+
+
+ }
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(2, 2)]
+ [InlineData(3, 3)]
+ public void Test6(int a, int b)
+ {
+ Assert.Equal(a, b);
+ }
+
+
+ [Fact]
+ public void Test7()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Fact(Skip = "Not required.")]
+ public void Test8()
+ {
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/KiBoards.Tests/UnitTest3.cs b/src/KiBoards.Tests/UnitTest3.cs
new file mode 100644
index 0000000..9918374
--- /dev/null
+++ b/src/KiBoards.Tests/UnitTest3.cs
@@ -0,0 +1,49 @@
+using Xunit.Abstractions;
+
+namespace KiBoards.Tests
+{
+
+ public class UnitTest3 : IClassFixture
+ {
+
+ public UnitTest3(TestContextFixture testContextFixture, ITestOutputHelper outputHelper)
+ {
+ // This is how to get messageBus
+
+ testContextFixture.SetContext(new { Version = "1234" });
+
+ outputHelper.WriteLine("HELLO WORLD MESSAGE BUS");
+ }
+
+ [Fact(Timeout = 1000)]
+ public void Test5()
+ {
+ Thread.Sleep(5000);
+
+
+ }
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(2, 2)]
+ [InlineData(3, 3)]
+ public void Test6(int a, int b)
+ {
+ Assert.Equal(a, b);
+ }
+
+
+ [Fact]
+ public void Test7()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Fact(Skip = "Not required.")]
+ public void Test8()
+ {
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/KiBoards.Tests/Usings.cs b/src/KiBoards.Tests/Usings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/src/KiBoards.Tests/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/src/KiBoards.sln b/src/KiBoards.sln
index 43d734a..dbc6024 100644
--- a/src/KiBoards.sln
+++ b/src/KiBoards.sln
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33516.290
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FDCEF06B-A89A-43CC-B505-4608DF29BBF7}") = "KiBoards", "KiBoards\KiBoards.csproj", "{5DC70E65-1093-4D72-9129-F0FFE3AF5151}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KiBoards", "KiBoards\KiBoards.csproj", "{644D75DA-7457-4DC2-AD9B-2376A7C20BC1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KiBoards.Tests", "KiBoards.Tests\KiBoards.Tests.csproj", "{BD23E63C-CC78-4714-A319-E492CC90178A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,15 +13,19 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {5DC70E65-1093-4D72-9129-F0FFE3AF5151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5DC70E65-1093-4D72-9129-F0FFE3AF5151}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5DC70E65-1093-4D72-9129-F0FFE3AF5151}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5DC70E65-1093-4D72-9129-F0FFE3AF5151}.Release|Any CPU.Build.0 = Release|Any CPU
+ {644D75DA-7457-4DC2-AD9B-2376A7C20BC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {644D75DA-7457-4DC2-AD9B-2376A7C20BC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {644D75DA-7457-4DC2-AD9B-2376A7C20BC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {644D75DA-7457-4DC2-AD9B-2376A7C20BC1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD23E63C-CC78-4714-A319-E492CC90178A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD23E63C-CC78-4714-A319-E492CC90178A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD23E63C-CC78-4714-A319-E492CC90178A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD23E63C-CC78-4714-A319-E492CC90178A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {58F0F7CE-57BF-43A9-8390-A15E0C1A9A66}
+ SolutionGuid = {F7A87B3B-2908-4E34-AC90-13E68B977B90}
EndGlobalSection
EndGlobal
diff --git a/src/KiBoards/Class1.cs b/src/KiBoards/Class1.cs
deleted file mode 100644
index 16e7b85..0000000
--- a/src/KiBoards/Class1.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace KiBoards
-{
- public class Class1
- {
-
- }
-}
\ No newline at end of file
diff --git a/src/KiBoards/ITestContextMessage.cs b/src/KiBoards/ITestContextMessage.cs
new file mode 100644
index 0000000..0b6df27
--- /dev/null
+++ b/src/KiBoards/ITestContextMessage.cs
@@ -0,0 +1,9 @@
+using Xunit.Abstractions;
+
+namespace KiBoards
+{
+ internal interface ITestContextMessage : IMessageSinkMessage
+ {
+ public object Context { get; set; }
+ }
+}
diff --git a/src/KiBoards/KiBoards.csproj b/src/KiBoards/KiBoards.csproj
index c8246b3..6260214 100644
--- a/src/KiBoards/KiBoards.csproj
+++ b/src/KiBoards/KiBoards.csproj
@@ -4,7 +4,6 @@
netstandard2.1
..\..\bin
enable
- enable
10
@@ -28,15 +27,16 @@
-
+
-
+
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+
+
+
diff --git a/src/KiBoards/Models/KiBoardsModelsExtensions.cs b/src/KiBoards/Models/KiBoardsModelsExtensions.cs
new file mode 100644
index 0000000..5734afa
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsModelsExtensions.cs
@@ -0,0 +1,39 @@
+using Nest;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards.Models
+{
+ internal static class KiBoardsModelsExtensions
+ {
+
+ internal static KiBoardsTestCaseStatusDto ToKiBoardsTestCase(this IXunitTestCase testCase, ITestMethod testMethod, KiBoardsTestCaseStatus status, KiBoardsTestCaseState state, object context = null) => new KiBoardsTestCaseStatusDto()
+ {
+ UniqueId = testCase.UniqueID,
+ DisplayName = testCase.DisplayName,
+ SkipReason = testCase.SkipReason,
+ UpdatedOn = DateTime.Now.ToUniversalTime(),
+ Status = status.ToString(),
+ State = state.ToString(),
+ TestMethod = new KiBoardsTestMethodDto()
+ {
+ TestClass = new KiBoardsTestClassDto()
+ {
+ Name = testMethod?.TestClass.Class.Name
+ },
+ Method = new KiBoardsTestMethodInfoDto()
+ {
+ Name = testMethod?.Method.Name
+ }
+ },
+ Context = context
+ };
+
+
+ internal static IEnumerable ToKiBoardsTestCases(this IEnumerable testCases, KiBoardsTestCaseStatus status, KiBoardsTestCaseState state, object context = null) =>
+ testCases.Select(x => x.ToKiBoardsTestCase(null, status, state, context));
+
+ internal static KiBoardsTestCaseStatus ToKiBoardsTestCaseStatus(this RunSummary summary)
+ => summary.Failed > 0 ? KiBoardsTestCaseStatus.Failure : summary.Skipped > 0 ? KiBoardsTestCaseStatus.Skipped : KiBoardsTestCaseStatus.Success;
+ }
+}
diff --git a/src/KiBoards/Models/KiBoardsTestCaseState.cs b/src/KiBoards/Models/KiBoardsTestCaseState.cs
new file mode 100644
index 0000000..6862720
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsTestCaseState.cs
@@ -0,0 +1,8 @@
+namespace KiBoards.Models
+{
+ internal enum KiBoardsTestCaseState
+ {
+ Active,
+ Inactive
+ }
+}
diff --git a/src/KiBoards/Models/KiBoardsTestCaseStatus.cs b/src/KiBoards/Models/KiBoardsTestCaseStatus.cs
new file mode 100644
index 0000000..c1cafa7
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsTestCaseStatus.cs
@@ -0,0 +1,11 @@
+namespace KiBoards.Models
+{
+ internal enum KiBoardsTestCaseStatus
+ {
+ Discovered,
+ Running,
+ Success,
+ Failure,
+ Skipped
+ }
+}
diff --git a/src/KiBoards/Models/KiBoardsTestCaseStatusDto.cs b/src/KiBoards/Models/KiBoardsTestCaseStatusDto.cs
new file mode 100644
index 0000000..11043de
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsTestCaseStatusDto.cs
@@ -0,0 +1,14 @@
+namespace KiBoards.Models
+{
+ class KiBoardsTestCaseStatusDto
+ {
+ public string UniqueId { get; set; }
+ public string DisplayName { get; set; }
+ public DateTime UpdatedOn { get; set; }
+ public string Status { get; set; }
+ public string State { get; set; }
+ public string SkipReason { get; set; }
+ public KiBoardsTestMethodDto TestMethod { get; set; }
+ public object Context { get; set; }
+ }
+}
diff --git a/src/KiBoards/Models/KiBoardsTestClassDto.cs b/src/KiBoards/Models/KiBoardsTestClassDto.cs
new file mode 100644
index 0000000..c3fbb69
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsTestClassDto.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace KiBoards.Models
+{
+ class KiBoardsTestClassDto
+ {
+ public string Name { get; set; }
+
+ }
+}
diff --git a/src/KiBoards/Models/KiBoardsTestMethodDto.cs b/src/KiBoards/Models/KiBoardsTestMethodDto.cs
new file mode 100644
index 0000000..4011dc1
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsTestMethodDto.cs
@@ -0,0 +1,8 @@
+namespace KiBoards.Models
+{
+ class KiBoardsTestMethodDto
+ {
+ public KiBoardsTestClassDto TestClass { get; set; }
+ public KiBoardsTestMethodInfoDto Method { get; set; }
+ }
+}
diff --git a/src/KiBoards/Models/KiBoardsTestMethodInfoDto.cs b/src/KiBoards/Models/KiBoardsTestMethodInfoDto.cs
new file mode 100644
index 0000000..7f33eab
--- /dev/null
+++ b/src/KiBoards/Models/KiBoardsTestMethodInfoDto.cs
@@ -0,0 +1,8 @@
+namespace KiBoards.Models
+{
+ class KiBoardsTestMethodInfoDto
+ {
+ public string Name { get; set; }
+
+ }
+}
diff --git a/src/KiBoards/Services/IKiBoardsElasticService.cs b/src/KiBoards/Services/IKiBoardsElasticService.cs
new file mode 100644
index 0000000..fb4e1de
--- /dev/null
+++ b/src/KiBoards/Services/IKiBoardsElasticService.cs
@@ -0,0 +1,11 @@
+using KiBoards.Models;
+
+namespace KiBoards.Services
+{
+ internal interface IKiBoardsElasticService
+ {
+ Task IndexTestCasesAsync(IEnumerable testCases);
+ Task IndexTestCaseAsync(KiBoardsTestCaseStatusDto testCase);
+
+ }
+}
\ No newline at end of file
diff --git a/src/KiBoards/Services/IKiBoardsTestRunnerService.cs b/src/KiBoards/Services/IKiBoardsTestRunnerService.cs
new file mode 100644
index 0000000..4713b0b
--- /dev/null
+++ b/src/KiBoards/Services/IKiBoardsTestRunnerService.cs
@@ -0,0 +1,18 @@
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards.Services
+{
+ internal interface IKiBoardsTestRunnerService
+ {
+ Guid RunId { get; }
+
+ Task BeginTestCasesRunAsync(IEnumerable testCases);
+ Task StartTestCaseAsync(IXunitTestCase testCase, ITestMethod testMethod);
+ Task FinishTestCaseAsync(IXunitTestCase testCase, ITestMethod testMethod, ExceptionAggregator exceptionAggregator, RunSummary result);
+ Task ErrorTestCaseAsync(IXunitTestCase testCase, ITestMethod testMethod, Exception ex);
+ Task EndTestCasesRunAsync(RunSummary results);
+ Task ErrorTestCasesRunAsync(IEnumerable testCases, Exception ex);
+ void SetContext(ITestContextMessage testContext);
+ }
+}
\ No newline at end of file
diff --git a/src/KiBoards/Services/KiBoardsElasticService.cs b/src/KiBoards/Services/KiBoardsElasticService.cs
new file mode 100644
index 0000000..b7c6d40
--- /dev/null
+++ b/src/KiBoards/Services/KiBoardsElasticService.cs
@@ -0,0 +1,54 @@
+using KiBoards.Models;
+using Nest;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards.Services
+{
+ internal class KiBoardsElasticService : IKiBoardsElasticService
+ {
+ private readonly IElasticClient _elasticClient;
+ private readonly IMessageSink _messageSink;
+
+ public KiBoardsElasticService(IElasticClient elasticClient, IMessageSink messageSink)
+ {
+ _elasticClient = elasticClient;
+ _messageSink = messageSink;
+ }
+
+ public async Task IndexTestCasesAsync(IEnumerable testCases)
+ {
+ try
+ {
+ _messageSink.OnMessage(new DiagnosticMessage($"Indexing {testCases.Count()} test cases"));
+ var result = await _elasticClient.IndexManyAsync(testCases);
+
+ if (!result.IsValid)
+ {
+ _messageSink.OnMessage(new DiagnosticMessage(result.DebugInformation));
+ }
+ }
+ catch (Exception ex)
+ {
+ _messageSink.OnMessage(new DiagnosticMessage(ex.Message));
+ }
+ }
+
+ public async Task IndexTestCaseAsync(KiBoardsTestCaseStatusDto testCase)
+ {
+ try
+ {
+ var result = await _elasticClient.IndexDocumentAsync(testCase);
+
+ if (!result.IsValid)
+ {
+ _messageSink.OnMessage(new DiagnosticMessage(result.DebugInformation));
+ }
+ }
+ catch (Exception ex)
+ {
+ _messageSink.OnMessage(new DiagnosticMessage(ex.Message));
+ }
+ }
+ }
+}
diff --git a/src/KiBoards/Services/KiBoardsElasticServiceExtensions.cs b/src/KiBoards/Services/KiBoardsElasticServiceExtensions.cs
new file mode 100644
index 0000000..25bb63d
--- /dev/null
+++ b/src/KiBoards/Services/KiBoardsElasticServiceExtensions.cs
@@ -0,0 +1,29 @@
+using KiBoards.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Nest;
+
+namespace KiBoards.Services
+{
+ internal static class KiBoardsElasticServiceExtensions
+ {
+ internal static IServiceCollection AddElasticServices(this IServiceCollection services)
+ {
+ return services
+ .AddSingleton(new ElasticClient(ConfigureIndexes(new ConnectionSettings(new Uri($"http://localhost:9200"))
+ .MaxRetryTimeout(TimeSpan.FromMinutes(5))
+ // This resolves internal errors with bulk index Invalid NEST response built from a successful (200) low level call on POST: /_bulk
+ .EnableApiVersioningHeader()
+ .MaximumRetries(3))))
+ .AddTransient();
+ }
+
+ internal static ConnectionSettings ConfigureIndexes(ConnectionSettings connectionSettings)
+ {
+ connectionSettings.DefaultMappingFor(m => m
+ .IndexName($"kiboards-testcase-status-{DateTime.UtcNow:yyyy-MM}")
+ .IdProperty(p => p.UniqueId));
+
+ return connectionSettings;
+ }
+ }
+}
diff --git a/src/KiBoards/Services/KiBoardsTestRunnerService.cs b/src/KiBoards/Services/KiBoardsTestRunnerService.cs
new file mode 100644
index 0000000..cb767ac
--- /dev/null
+++ b/src/KiBoards/Services/KiBoardsTestRunnerService.cs
@@ -0,0 +1,79 @@
+using KiBoards.Models;
+using System.Reflection;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards.Services
+{
+ internal class KiBoardsTestRunnerService : IKiBoardsTestRunnerService, IDisposable
+ {
+ private readonly IMessageSink _messageSink;
+ private readonly IKiBoardsElasticService _elasticService;
+
+ public Guid RunId { get; private set; } = Guid.NewGuid();
+
+ public object Context { get; private set; }
+
+ public KiBoardsTestRunnerService(IMessageSink messageSink, IKiBoardsElasticService elasticService)
+ {
+ _messageSink = messageSink;
+ _elasticService = elasticService;
+
+ var version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion;
+ messageSink.OnMessage(new DiagnosticMessage($"KiBoards: {version}"));
+ messageSink.OnMessage(new DiagnosticMessage($"RunId: {RunId}"));
+ }
+
+
+ public void Dispose()
+ {
+ //_messageSink.OnMessage(new DiagnosticMessage("KiBoards run finished."));
+ }
+
+ public async Task BeginTestCasesRunAsync(IEnumerable testCases)
+ {
+ //foreach (var testCase in testCases)
+ // _messageSink.OnMessage(new DiagnosticMessage($"Discovered: {testCase.UniqueID} {testCase.DisplayName}"));
+
+ await _elasticService.IndexTestCasesAsync(testCases.ToKiBoardsTestCases(KiBoardsTestCaseStatus.Discovered, KiBoardsTestCaseState.Active, Context));
+ }
+
+
+ public async Task ErrorTestCaseAsync(IXunitTestCase testCase, ITestMethod testMethod, Exception ex)
+ {
+ //_messageSink.OnMessage(new DiagnosticMessage($"Fatal: {testCase.UniqueID} ({ex.Message})"));
+ await Task.CompletedTask;
+ }
+
+ public async Task FinishTestCaseAsync(IXunitTestCase testCase, ITestMethod testMethod, ExceptionAggregator exceptionAggregator, RunSummary summary)
+ {
+ //_messageSink.OnMessage(new DiagnosticMessage($"{(summary.Failed > 0 ? "Failure" : summary.Skipped > 0 ? "Skipped" : "Success")}: {testCase.UniqueID} {testCase.DisplayName} ({summary.Time}s)"));
+ await _elasticService.IndexTestCaseAsync(testCase.ToKiBoardsTestCase(testMethod, summary.ToKiBoardsTestCaseStatus(), KiBoardsTestCaseState.Inactive, Context));
+ }
+
+ public async Task StartTestCaseAsync(IXunitTestCase testCase, ITestMethod testMethod)
+ {
+ //_messageSink.OnMessage(new DiagnosticMessage($"Started: {testCase.UniqueID} {testCase.DisplayName}"));
+ await _elasticService.IndexTestCaseAsync(testCase.ToKiBoardsTestCase(testMethod, KiBoardsTestCaseStatus.Running, KiBoardsTestCaseState.Active, Context));
+ }
+
+
+ public async Task EndTestCasesRunAsync(RunSummary results)
+ {
+ //_messageSink.OnMessage(new DiagnosticMessage("KiBoards run complete."));
+ await Task.CompletedTask;
+
+ }
+
+ public async Task ErrorTestCasesRunAsync(IEnumerable testCases, Exception ex)
+ {
+ //_messageSink.OnMessage(new DiagnosticMessage($"Fatal: Run {RunId} failed. ({ex.Message})"));
+ await Task.CompletedTask;
+ }
+
+ public void SetContext(ITestContextMessage testContext)
+ {
+ Context = testContext.Context;
+ }
+ }
+}
diff --git a/src/KiBoards/TestContextFixture.cs b/src/KiBoards/TestContextFixture.cs
new file mode 100644
index 0000000..8da59cf
--- /dev/null
+++ b/src/KiBoards/TestContextFixture.cs
@@ -0,0 +1,19 @@
+using Xunit.Abstractions;
+
+namespace KiBoards
+{
+ public class TestContextFixture
+ {
+ private readonly IMessageSink _messageSink;
+
+ public TestContextFixture(IMessageSink messageSink)
+ {
+ _messageSink = messageSink;
+ }
+
+ public void SetContext(object context)
+ {
+ _messageSink.OnMessage(new TestContextMessage(context));
+ }
+ }
+}
diff --git a/src/KiBoards/TestContextMessage.cs b/src/KiBoards/TestContextMessage.cs
new file mode 100644
index 0000000..e8d414c
--- /dev/null
+++ b/src/KiBoards/TestContextMessage.cs
@@ -0,0 +1,12 @@
+namespace KiBoards
+{
+ internal class TestContextMessage : ITestContextMessage
+ {
+ public object Context { get; set; }
+
+ public TestContextMessage(object context)
+ {
+ Context = context;
+ }
+ }
+}
diff --git a/src/KiBoards/TestExtensions.cs b/src/KiBoards/TestExtensions.cs
new file mode 100644
index 0000000..454fe79
--- /dev/null
+++ b/src/KiBoards/TestExtensions.cs
@@ -0,0 +1,11 @@
+using System.Reflection;
+using Xunit.Abstractions;
+
+namespace KiBoards
+{
+ public static class TestExtensions
+ {
+ public static ITestCase GetTestCase(this ITestOutputHelper output)
+ => (output.GetType().GetField("test", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(output) as ITest).TestCase;
+ }
+}
diff --git a/src/KiBoards/TestFramework.cs b/src/KiBoards/TestFramework.cs
new file mode 100644
index 0000000..b6b9dd3
--- /dev/null
+++ b/src/KiBoards/TestFramework.cs
@@ -0,0 +1,184 @@
+using System.Reflection;
+using KiBoards.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards
+{
+ public class TestFramework : XunitTestFramework, IDisposable
+ {
+ private readonly ServiceProvider _serviceProvider;
+
+ public TestFramework(IMessageSink messageSink)
+ : base(messageSink)
+ {
+ IServiceCollection serviceCollection = new ServiceCollection();
+
+ serviceCollection
+ .AddSingleton(messageSink)
+ .AddElasticServices()
+ .AddSingleton();
+
+ _serviceProvider = serviceCollection.BuildServiceProvider();
+ }
+
+ protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
+ {
+ return new TestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink, _serviceProvider.GetRequiredService());
+ }
+
+ public new async void Dispose()
+ {
+ await Task.Delay(1);
+ _serviceProvider.Dispose();
+ base.Dispose();
+ }
+
+
+ private class TestFrameworkExecutor : XunitTestFrameworkExecutor
+ {
+ private readonly IKiBoardsTestRunnerService _testRunner;
+
+ public TestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink, IKiBoardsTestRunnerService testRunner)
+ : base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
+ {
+ _testRunner = testRunner;
+ }
+
+ protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+ {
+ try
+ {
+ await _testRunner.BeginTestCasesRunAsync(testCases);
+ using var assemblyRunner = new TestAssemblyRunner(TestAssembly, testCases, new TestMessageSink(DiagnosticMessageSink, _testRunner), executionMessageSink, executionOptions, _testRunner);
+
+ var results = await assemblyRunner.RunAsync();
+ await _testRunner.EndTestCasesRunAsync(results);
+ }
+ catch (Exception ex)
+ {
+ await _testRunner.ErrorTestCasesRunAsync(testCases, ex);
+ }
+ }
+ }
+
+
+
+ private class TestAssemblyRunner : XunitTestAssemblyRunner
+ {
+ private readonly IKiBoardsTestRunnerService _testRunner;
+ private readonly IMessageSink _messageSink;
+
+ public TestAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions, IKiBoardsTestRunnerService testRunner)
+ : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
+ {
+ _testRunner = testRunner;
+ _messageSink = diagnosticMessageSink;
+ }
+
+
+
+
+ protected override async Task RunTestCollectionAsync(IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource)
+ {
+ var collectionRunner = new TestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource, _testRunner);
+ return await collectionRunner.RunAsync();
+ }
+
+ protected override string GetTestFrameworkDisplayName()
+ {
+ var version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion;
+ return $"KiBoards {version}";
+ }
+
+ protected override Task AfterTestAssemblyStartingAsync()
+ {
+ _messageSink.OnMessage(new DiagnosticMessage("AfterTestAssemblyStartingAsync"));
+ return base.AfterTestAssemblyStartingAsync();
+ }
+
+
+ protected override Task BeforeTestAssemblyFinishedAsync()
+ {
+ _messageSink.OnMessage(new DiagnosticMessage("BeforeTestAssemblyFinishedAsync"));
+ return base.BeforeTestAssemblyFinishedAsync();
+ }
+ }
+
+
+ private class TestCollectionRunner : XunitTestCollectionRunner
+ {
+ private readonly IKiBoardsTestRunnerService _testRunner;
+
+ public TestCollectionRunner(ITestCollection testCollection, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, IKiBoardsTestRunnerService testRunner)
+ : base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
+ {
+ _testRunner = testRunner;
+ }
+
+ protected override Task RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases)
+ => new TestClassRunner(testClass, @class, testCases, DiagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, CollectionFixtureMappings, _testRunner)
+ .RunAsync();
+ }
+
+
+ private class TestClassRunner : XunitTestClassRunner
+ {
+ private readonly IKiBoardsTestRunnerService _testRunner;
+
+ public TestClassRunner(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings, IKiBoardsTestRunnerService testRunner)
+ : base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource, collectionFixtureMappings)
+ {
+ _testRunner = testRunner;
+ }
+
+ protected override Task RunTestMethodAsync(ITestMethod testMethod, IReflectionMethodInfo method, IEnumerable testCases, object[] constructorArguments)
+ => new TestMethodRunner(testMethod, Class, method, testCases, DiagnosticMessageSink, new TestResultBus(MessageBus), new ExceptionAggregator(Aggregator), CancellationTokenSource, constructorArguments, _testRunner)
+ .RunAsync();
+
+ protected override async Task RunTestMethodsAsync()
+ {
+ return await base.RunTestMethodsAsync();
+ }
+ }
+
+
+
+ private class TestMethodRunner : XunitTestMethodRunner
+ {
+ private readonly IKiBoardsTestRunnerService _testRunner;
+ private readonly TestResultSink _resultSink;
+ private readonly TestResultBus _resultBus;
+
+ public TestMethodRunner(ITestMethod testMethod, IReflectionTypeInfo @class, IReflectionMethodInfo method, IEnumerable testCases, IMessageSink diagnosticMessageSink, TestResultBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, object[] constructorArguments, IKiBoardsTestRunnerService testRunner)
+ : base(testMethod, @class, method, testCases, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource, constructorArguments)
+
+ {
+ _resultBus = messageBus;
+ _testRunner = testRunner;
+ }
+
+ protected override async Task RunTestCaseAsync(IXunitTestCase testCase)
+ {
+ try
+ {
+ await _testRunner.StartTestCaseAsync(testCase, TestMethod);
+ var result = await base.RunTestCaseAsync(testCase);
+
+ var testResult = _resultBus.TestResult;
+
+ await _testRunner.FinishTestCaseAsync(testCase, TestMethod, Aggregator, result);
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ await _testRunner.ErrorTestCaseAsync(testCase, TestMethod, ex);
+ throw;
+ }
+ }
+ }
+ }
+}
+
diff --git a/src/KiBoards/TestMessageSink.cs b/src/KiBoards/TestMessageSink.cs
new file mode 100644
index 0000000..42139c2
--- /dev/null
+++ b/src/KiBoards/TestMessageSink.cs
@@ -0,0 +1,158 @@
+using KiBoards.Services;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards
+{
+ internal class TestMessageSink : IMessageSink
+ {
+ private readonly IKiBoardsTestRunnerService _testRunner;
+ private IMessageSink _messageSink;
+
+ internal TestMessageSink(IMessageSink messageSink, IKiBoardsTestRunnerService testRunner)
+ {
+ _messageSink = messageSink;
+ _testRunner = testRunner;
+ }
+
+ private void LogMessage(string message)
+ {
+ _messageSink.OnMessage(new DiagnosticMessage(message));
+ }
+
+ private void LogTestCase(string messageType, ITestCase testCase)
+ {
+ LogMessage($"{messageType}: TestCase.UniqueID: {testCase.UniqueID}");
+ LogMessage($"{messageType}: TestCase.DisplayName: {testCase.DisplayName}");
+ LogMessage($"{messageType}: TestCase.Traits.Count: {testCase.Traits.Count}");
+
+ if (testCase.SourceInformation != null)
+ {
+ LogMessage($"{messageType}: TestCase.SourceInformation.FileName: {testCase.SourceInformation.FileName}");
+ LogMessage($"{messageType}: TestCase.SourceInformation.LineNumber: {testCase.SourceInformation.LineNumber}");
+ }
+ }
+
+
+ public bool OnMessage(IMessageSinkMessage message)
+ {
+ HandleMessageSinkMessage(message);
+ return _messageSink.OnMessage(message);
+ }
+
+ private void HandleMessageSinkMessage(IMessageSinkMessage message)
+ {
+ switch (message)
+ {
+
+ case ITestContextMessage testContext:
+ LogMessage($"TestContext: {testContext}");
+ _testRunner.SetContext(testContext);
+
+ break;
+
+ case ITestAssemblyStarting testAssemblyStarting:
+ LogMessage($"TestAssemblyStarting: StartTime: {testAssemblyStarting.StartTime}");
+ LogMessage($"TestAssemblyStarting: TestFrameworkDisplayName: {testAssemblyStarting.TestFrameworkDisplayName}");
+ LogMessage($"TestAssemblyStarting: TestEnvironment: {testAssemblyStarting.TestEnvironment}");
+ LogMessage($"TestAssemblyStarting: TestCases.Count: {testAssemblyStarting.TestCases.Count()}");
+ break;
+
+ case ITestCollectionStarting testCollectionStarting:
+ LogMessage($"TestCollectionStarting: TestCollection.DisplayName: {testCollectionStarting.TestCollection.DisplayName}");
+ LogMessage($"TestCollectionStarting: TestCollection.UniqueID: {testCollectionStarting.TestCollection.UniqueID}");
+ LogMessage($"TestCollectionStarting: TestCollection.TestCases.Count: {testCollectionStarting.TestCases.Count()}");
+ LogMessage($"TestCollectionStarting: TestAssembly.Assembly.Name: {testCollectionStarting.TestAssembly.Assembly.Name}");
+ break;
+
+
+ case ITestMethodStarting testMethodStarting:
+ LogMessage($"TestMethodStarting: TestCollection.DisplayName: {testMethodStarting.TestCollection.DisplayName}");
+ LogMessage($"TestMethodStarting: TestCollection.UniqueID: {testMethodStarting.TestCollection.UniqueID}");
+ LogMessage($"TestMethodStarting: TestCollection.TestAssembly.Assembly.AssemblyPath: {testMethodStarting.TestCollection.TestAssembly.Assembly.AssemblyPath}");
+ LogMessage($"TestMethodStarting: TestCases.Count: {testMethodStarting.TestCases.Count()}");
+
+ LogMessage($"TestMethodStarting: TestClass.Class.Name: {testMethodStarting.TestClass.Class.Name}");
+ break;
+
+ case ITestCaseStarting testCaseStarting:
+ LogMessage("TestCaseStarting");
+ break;
+
+ case ITestStarting testStarting:
+ LogMessage($"TestStarting: Test.DisplayName: {testStarting.Test.DisplayName}");
+ break;
+
+ case ITestClassConstructionStarting classConstructionStarting:
+ LogMessage($"TestClassConstructionStarting: Test.DisplayName: {classConstructionStarting.Test.DisplayName}");
+ LogMessage($"TestClassConstructionStarting: Test.TestCase.DisplayName: {classConstructionStarting.Test.TestCase.DisplayName}");
+ LogMessage($"TestClassConstructionStarting: Test.TestCase.UniqueID: {classConstructionStarting.Test.TestCase.UniqueID}");
+ LogMessage($"TestClassConstructionStarting: Test.TestCase.TestMethod.Method.Name: {classConstructionStarting.Test.TestCase.TestMethod.Method.Name}");
+ break;
+
+ case ITestClassConstructionFinished classConstructionFinished:
+ // Code to handle Xunit.Sdk.TestClassConstructionFinished case
+ break;
+
+
+ case ITestSkipped testSkipped:
+ // Code to handle Xunit.Sdk.TestSkipped case
+ break;
+
+ case ITestPassed testPassed:
+ LogMessage($"TestPassed: Output: {testPassed.Output}");
+ LogMessage($"TestPassed: ExcecutionTime: {testPassed.ExecutionTime}");
+ LogMessage($"TestPassed: Test.DisplayName: {testPassed.Test.DisplayName}");
+ LogMessage($"TestPassed: TestCase.UniqueID: {testPassed.TestCase.UniqueID}");
+ LogMessage($"TestPassed: TestCase.DisplayName: {testPassed.TestCase.DisplayName}");
+ LogMessage($"TestPassed: TestCases.Count: {testPassed.TestCases.Count()}");
+
+ // Code to handle Xunit.Sdk.TestPassed case
+ break;
+
+ case ITestFailed testFailed:
+ LogMessage($"TestFailed: ExecutionTime: {testFailed.ExecutionTime}");
+ LogMessage($"TestFailed: Messages: {string.Join('\n', testFailed.Messages)}");
+ LogMessage($"TestFailed: Output: {testFailed.Output}");
+ // Code to handle Xunit.Sdk.TestFailed case
+ break;
+
+ case ITestFinished testFinished:
+ // Code to handle Xunit.Sdk.TestFinished case
+ break;
+
+ case ITestCaseFinished testCaseFinished:
+ // Code to handle Xunit.Sdk.TestCaseFinished case
+ break;
+
+
+ case ITestMethodFinished testMethodFinished:
+ // Code to handle Xunit.Sdk.TestMethodFinished case
+ break;
+
+ case ITestClassStarting testClassStarting:
+ break;
+
+ case ITestOutput TestOutput:
+ break;
+
+ case ITestClassFinished testClassFinished:
+ break;
+
+ case ITestCollectionFinished testCollectionFinished:
+ break;
+
+ case ITestAssemblyFinished testAssemblyFinished:
+ LogMessage($"TestAssemblyFinished: TestsRun: {testAssemblyFinished.TestsRun}");
+ LogMessage($"TestAssemblyFinished: TestsFailed: {testAssemblyFinished.TestsFailed}");
+ LogMessage($"TestAssemblyFinished: TestsSkipped: {testAssemblyFinished.TestsSkipped}");
+ LogMessage($"TestAssemblyFinished: ExecutionTime: {testAssemblyFinished.ExecutionTime}");
+ break;
+
+ default:
+ LogMessage($"UNKNOWN: {message.GetType().Name}: {message}");
+ break;
+ }
+ }
+ }
+}
diff --git a/src/KiBoards/TestResultBus.cs b/src/KiBoards/TestResultBus.cs
new file mode 100644
index 0000000..6aa1c6b
--- /dev/null
+++ b/src/KiBoards/TestResultBus.cs
@@ -0,0 +1,28 @@
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace KiBoards
+{
+ internal class TestResultBus : IMessageBus
+ {
+ private readonly IMessageBus _messageBus;
+
+ public ITestResultMessage TestResult { get; private set; }
+
+ internal TestResultBus(IMessageBus messsageBus) => _messageBus = messsageBus ?? throw new ArgumentNullException(nameof(messsageBus));
+
+
+ public bool QueueMessage(IMessageSinkMessage message)
+ {
+ if (message is ITestResultMessage result)
+ TestResult = result;
+
+ return _messageBus.QueueMessage(message);
+ }
+
+ public void Dispose()
+ {
+ _messageBus.Dispose();
+ }
+ }
+}
diff --git a/src/KiBoards/TestResultSink.cs b/src/KiBoards/TestResultSink.cs
new file mode 100644
index 0000000..a49a4cd
--- /dev/null
+++ b/src/KiBoards/TestResultSink.cs
@@ -0,0 +1,24 @@
+using Xunit.Abstractions;
+
+namespace KiBoards
+{
+ internal class TestResultSink : IMessageSink
+ {
+ private IMessageSink _messageSink;
+
+ public ITestResultMessage TestResult { get; private set; }
+
+ internal TestResultSink(IMessageSink messageSink)
+ {
+ _messageSink = messageSink;
+ }
+
+ public bool OnMessage(IMessageSinkMessage message)
+ {
+ if (message is ITestResultMessage result)
+ TestResult = result;
+
+ return _messageSink.OnMessage(message);
+ }
+ }
+}