diff --git a/README.md b/README.md index 8c1ae92..bdc0702 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,11 @@ There's also an option to generate a nice html report for all test scenarios. Ju ![image](https://github.com/cezarypiatek/NScenario/assets/7759991/13c501d6-d26b-4406-93fb-1da29dca9bff) + +This report browser supports links to scenario steps definition. To make it work, you need to set the following msbuild properties (or environment variables): +- RepositoryUrl +- SourceRevisionId + ## Test scenario title Test scenario title is generated by removing underscores and splitting camel/pascalcase string from the test method name (`[CallerMemberName]` is used to retrieve that name). This allows for immediate review of the test name (I saw many, extremely long and totally ridiculous test method names. A good test method name should reveal the intention of the test case, not its details). You can always override the default title by setting it explicitly during test scenario creation (especially useful for parametrized test methods): diff --git a/src/NScenario/ReportExtensions.cs b/src/NScenario/ReportExtensions.cs index 529f1a0..5d8a9ae 100644 --- a/src/NScenario/ReportExtensions.cs +++ b/src/NScenario/ReportExtensions.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reflection; using System.Text.Json; namespace NScenario; @@ -8,8 +10,21 @@ public static class ReportExtensions { public static void SaveAsReport(this IReadOnlyList scenarios, string outputPath) { + var callingAssembly = Assembly.GetCallingAssembly(); + var sourceControlInfo = RepoPathResolver.GetSourceControlInfo(callingAssembly); + var repositoryRootDir = RepoPathResolver.FindRepoRootDirectory(scenarios.FirstOrDefault()?.FilePath); var template = ResourceExtractor.GetEmbeddedResourceContent("NScenario.report-browser-template.html"); - var report = template.Replace("//[DATA_PLACEHOLDER]", JsonSerializer.Serialize(scenarios)); + var scenariosPayload = JsonSerializer.Serialize(new + { + SourceControlInfo = new + { + RepositoryUrl = sourceControlInfo?.RepositoryUrl, + Revision = sourceControlInfo?.Revision, + RepositoryRootDir = repositoryRootDir + }, + Scenarios = scenarios + }); + var report = template.Replace("//[DATA_PLACEHOLDER]", scenariosPayload); File.WriteAllText(outputPath, report); } } \ No newline at end of file diff --git a/src/NScenario/Reporting/RepoPathResolver.cs b/src/NScenario/Reporting/RepoPathResolver.cs new file mode 100644 index 0000000..3107f40 --- /dev/null +++ b/src/NScenario/Reporting/RepoPathResolver.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Linq; +using System.Reflection; + +namespace NScenario; + +internal static class RepoPathResolver +{ + public static string? FindRepoRootDirectory(string? fileInTheRepo) + { + if (fileInTheRepo == null) + return null; + + var currentDirectory = new DirectoryInfo(Path.GetDirectoryName(fileInTheRepo) ?? string.Empty); + + while (currentDirectory is { Exists: true }) + { + if (Directory.Exists(Path.Combine(currentDirectory.FullName, ".git"))) + { + return currentDirectory.FullName; + } + currentDirectory = currentDirectory.Parent; + } + + return null; + } + + public static SourceControlInfo? GetSourceControlInfo(Assembly assembly) + { + var repositoryUrl = assembly.GetCustomAttributes().FirstOrDefault(x => x.Key == "RepositoryUrl")?.Value; + var revision = assembly.GetCustomAttributes().FirstOrDefault()?.InformationalVersion?.Split('+').LastOrDefault(); + + if (string.IsNullOrWhiteSpace(repositoryUrl) == false && string.IsNullOrWhiteSpace(revision) == false) + { + return new SourceControlInfo(repositoryUrl, revision); + } + + return null; + } +} \ No newline at end of file diff --git a/src/NScenario/Reporting/SourceControlInfo.cs b/src/NScenario/Reporting/SourceControlInfo.cs new file mode 100644 index 0000000..5811d64 --- /dev/null +++ b/src/NScenario/Reporting/SourceControlInfo.cs @@ -0,0 +1,3 @@ +namespace NScenario; + +internal record SourceControlInfo(string RepositoryUrl, string Revision); \ No newline at end of file diff --git a/src/NScenario/report-browser-template.html b/src/NScenario/report-browser-template.html index e69de29..38c2994 100644 --- a/src/NScenario/report-browser-template.html +++ b/src/NScenario/report-browser-template.html @@ -0,0 +1,4 @@ +NScenario - Test Scenarios
diff --git a/src/nscenario-report-browser/public/scenarios.json b/src/nscenario-report-browser/public/scenarios.json index a71de29..01abe92 100644 --- a/src/nscenario-report-browser/public/scenarios.json +++ b/src/nscenario-report-browser/public/scenarios.json @@ -1,394 +1,292 @@ -[ - { - "ScenarioTitle": "should collect info about exceptions", - "MethodName": "should_collect_info_about_exceptions", - "FilePath": "C:\\repos\\NScenario\\src\\UnitTest2.cs", - "LineNumber": 138, - "Status": 1, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 142, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0190945", - "Status": 2, - "Exception": null, - "SubSteps": [ - { - "Description": "This is the first sub-step of first step", - "LineNumber": 144, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0011362", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second sub-step of first step", - "LineNumber": 148, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0100732", - "Status": 2, - "Exception": null, - "SubSteps": [ - { - "Description": "Yet another nesting level p1", - "LineNumber": 150, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0090506", - "Status": 2, - "Exception": "System.InvalidOperationException: Something wrong\r\n at NScenario.Demo.Tests.<>c.b__5_4() in C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs:line 152\r\n at NScenario.StepExecutors.OutputScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\OutputScenarioStepExecutor.cs:line 31\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.ScenarioInfoCollectorExecutor.Step(String scenarioName, String stepDescription, MaybeAsyncAction action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\ScenarioInfoCollectorExecutor.cs:line 61", - "SubSteps": null - } - ] - } - ] - } - ] +{ + "SourceControlInfo": { + "RepositoryUrl": "https://github.com/cezarypiatek/NScenario", + "Revision": "f05d0f6f5c6c75e7cc2247b94448dbd082440d90", + "RepositoryRootDir": "C:\\repos\\NScenario" }, - { - "ScenarioTitle": "some scenario when flag set to 'False'", - "MethodName": "should_present_basic_scenario_with_explicit_title", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 58, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 60, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0003793", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 65, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001727", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 70, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001519", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "some scenario when flag set to 'True'", - "MethodName": "should_present_basic_scenario_with_explicit_title", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 58, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 60, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000399", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 65, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000201", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 70, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000520", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should present basic scenario", - "MethodName": "should_present_basic_scenario", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 13, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 15, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0003591", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 20, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0092570", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 25, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0008134", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should present basic scenario with steps returning value", - "MethodName": "should_present_basic_scenario_with_steps_returning_value", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 79, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 81, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002627", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 87, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0041451", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 94, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001964", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title", - "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 36, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 38, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002279", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 43, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001344", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 48, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001646", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should present scenario with sub steps", - "MethodName": "should_present_scenario_with_sub_steps", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 104, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 106, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0025073", - "Status": 3, - "Exception": null, - "SubSteps": [ - { - "Description": "This is the first sub-step of first step", - "LineNumber": 108, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001351", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second sub-step of first step", - "LineNumber": 112, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0012006", - "Status": 3, - "Exception": null, - "SubSteps": [ - { - "Description": "Yet another nesting level p1", - "LineNumber": 114, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001024", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "Yet another nesting level p2", - "LineNumber": 118, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000864", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - } - ] - }, - { - "Description": "This is the second step", - "LineNumber": 175, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0010105", - "Status": 3, - "Exception": null, - "SubSteps": [ - { - "Description": "This is the first sub-step of second step", - "LineNumber": 177, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000875", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second sub-step of second step", - "LineNumber": 181, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000664", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "Description": "This is the third step", - "LineNumber": 127, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0000586", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title 1", - "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 36, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 38, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002279", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 43, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001344", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 48, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001646", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - }, - { - "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title 2", - "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "LineNumber": 36, - "Status": 0, - "Steps": [ - { - "Description": "This is the first step", - "LineNumber": 38, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0002279", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the second step", - "LineNumber": 43, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001344", - "Status": 3, - "Exception": null, - "SubSteps": null - }, - { - "Description": "This is the third step", - "LineNumber": 48, - "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", - "ExecutionTime": "00:00:00.0001646", - "Status": 3, - "Exception": null, - "SubSteps": null - } - ] - } -] \ No newline at end of file + "Scenarios": [ + { + "ScenarioTitle": "should collect info about exceptions", + "MethodName": "should_collect_info_about_exceptions", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 146, + "Status": 1, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 150, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.6993193", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "This is the first sub-step of first step", + "LineNumber": 152, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0013353", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second sub-step of first step", + "LineNumber": 156, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.4782463", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "Yet another nesting level p1", + "LineNumber": 158, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.2343907", + "Status": 2, + "Exception": "System.InvalidOperationException: Something wrong\r\n at NScenario.Demo.Tests.\u003c\u003ec.\u003cshould_collect_info_about_exceptions\u003eb__5_4() in C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs:line 160\r\n at NScenario.StepExecutors.OutputScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\OutputScenarioStepExecutor.cs:line 31\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.ScenarioInfoCollectorExecutor.Step(String scenarioName, String stepDescription, MaybeAsyncAction action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\ScenarioInfoCollectorExecutor.cs:line 61", + "SubSteps": null + } + ] + } + ] + } + ] + }, + { + "ScenarioTitle": "some scenario when flag set to \u0027False\u0027", + "MethodName": "should_present_basic_scenario_with_explicit_title", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 59, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 61, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001056", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 66, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001212", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 71, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001092", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "some scenario when flag set to \u0027True\u0027", + "MethodName": "should_present_basic_scenario_with_explicit_title", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 59, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 61, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000481", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 66, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000052", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 71, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000042", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should present basic scenario", + "MethodName": "should_present_basic_scenario", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 14, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 16, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001828", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 21, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001153", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 26, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001128", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should present basic scenario with steps returning value", + "MethodName": "should_present_basic_scenario_with_steps_returning_value", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 80, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 82, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0003630", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 88, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0046366", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 95, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0002009", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should Present BASIC Scenario With Camel Case Title", + "MethodName": "shouldPresentBASICScenarioWithCamelCaseTitle", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 37, + "Status": 0, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 39, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001102", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second step", + "LineNumber": 44, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000801", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the third step", + "LineNumber": 49, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0000750", + "Status": 3, + "Exception": null, + "SubSteps": null + } + ] + }, + { + "ScenarioTitle": "should present scenario with sub steps", + "MethodName": "should_present_scenario_with_sub_steps", + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "LineNumber": 106, + "Status": 1, + "Steps": [ + { + "Description": "This is the first step", + "LineNumber": 108, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.7567549", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "This is the first sub-step of first step", + "LineNumber": 110, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001420", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "This is the second sub-step of first step", + "LineNumber": 114, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.5376479", + "Status": 2, + "Exception": null, + "SubSteps": [ + { + "Description": "Yet another nesting level p1", + "LineNumber": 116, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.0001048", + "Status": 3, + "Exception": null, + "SubSteps": null + }, + { + "Description": "Yet another nesting level p2", + "LineNumber": 120, + "FilePath": "C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs", + "ExecutionTime": "00:00:00.2110651", + "Status": 2, + "Exception": "System.InvalidOperationException: Hello\r\n at NScenario.Demo.Tests.\u003c\u003ec.\u003cshould_present_scenario_with_sub_steps\u003eb__4_5() in C:\\repos\\NScenario\\src\\NScenario.Demo\\UnitTest1.cs:line 123\r\n at NScenario.StepExecutors.OutputScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\OutputScenarioStepExecutor.cs:line 31\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.StepExecutors.LevelTrackingScenarioStepExecutor.Step(String scenarioName, String stepDescription, Func`1 action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\StepExecutors\\LevelTrackingScenarioStepExecutor.cs:line 21\r\n at NScenario.ScenarioInfoCollectorExecutor.Step(String scenarioName, String stepDescription, MaybeAsyncAction action, StepContext stepContext) in C:\\repos\\NScenario\\src\\NScenario\\ScenarioInfoCollectorExecutor.cs:line 61", + "SubSteps": null + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/nscenario-report-browser/src/App.tsx b/src/nscenario-report-browser/src/App.tsx index 6e9239f..42fa791 100644 --- a/src/nscenario-report-browser/src/App.tsx +++ b/src/nscenario-report-browser/src/App.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {createContext, useContext, useEffect, useState} from 'react'; import './App.css'; //import data from "./scenarios.json" @@ -10,6 +10,7 @@ import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import {ICodeLocation, RepoPathResolver} from "./External"; @@ -149,8 +150,10 @@ export function StatisticsCtr(props: {scenarios:Scenario[]}) { } export function StepCtr(props: {data:Step, prefix:string}) { + const globalServices = useContext(GlobalServicesContext); + const scenarioFile = globalServices.pathResolver(props.data); return ( - <> Step {props.prefix}: {`${props.data.Description}`} + <> Step {props.prefix}: {props.data.Description} {props.data.Exception != null && (

@@ -169,16 +172,18 @@ export function StepCtr(props: {data:Step, prefix:string}) {
   );
 }
 export function ScenarioTitleCtr(props: {data:Scenario}) {
+    const globalServices = useContext(GlobalServicesContext);
+    const scenarioFile = globalServices.pathResolver(props.data);
     return (
         
{props.data.Status === 0 ? () :} - { `SCENARIO: ${props.data.ScenarioTitle}`} + SCENARIO: {props.data.ScenarioTitle}
); } const ScenarioCtr = React.memo((props: {data:Scenario, isSelected:boolean}) =>{ return ( - )} style={{width:"auto", border: props.isSelected? "1px solid blue": "1px solid transparent", transition: "border-color 0.5s ease" }}> + )} style={{width:"auto", border: props.isSelected? "1px solid blue": "1px solid transparent", transition: "border-color 0.5s ease" }}> 0) { try { - return JSON.parse(dataStorage.innerText) as Scenario[]; + return JSON.parse(dataStorage.innerText) as INScenarioData; }catch { - return []; } } - return []; + return { + SourceControlInfo: {Revision: null, RepositoryUrl: null, RepositoryRootDir: null}, + Scenarios: [] + }; } +interface ISourceControlInfo +{ + RepositoryUrl:string | null, + Revision:string | null, + RepositoryRootDir:string | null +} + +interface INScenarioData +{ + SourceControlInfo: ISourceControlInfo, + Scenarios: Scenario[] +} + +interface GlobalServices{ + pathResolver: ((location: ICodeLocation) => string) +} + +const GlobalServicesContext = React.createContext({pathResolver: (location)=> location.FilePath }) + + function App() { - const scenarioData = retriveData(); - const treeData = generateTableOfContent(scenarioData); + const scenarioData = retrieveData(); + const treeData = generateTableOfContent(scenarioData.Scenarios); - const [scenarioState, setScenarioState] = useState(scenarioData) + const [scenarioState, setScenarioState] = useState(scenarioData.Scenarios) const [treeState, setTreeState] = useState(treeData) const [selectedScenario, setSelectedScenario] = useState("") + const [sourceControlState, setSourceControlState] = useState(scenarioData.SourceControlInfo) + + let pathResolver = RepoPathResolver.TryToGetPathBuilder(sourceControlState, sourceControlState.RepositoryRootDir) + useEffect( () => { (async ()=>{ - if(scenarioData.length === 0) + if(scenarioData.Scenarios.length === 0) { const response = await fetch("/scenarios.json") - const sampleData = await response.json(); - setScenarioState(sampleData as Scenario[]) - setTreeState(generateTableOfContent(sampleData)) + const sampleData : INScenarioData = await response.json(); + setScenarioState(sampleData.Scenarios); + setSourceControlState(sampleData.SourceControlInfo); + setTreeState(generateTableOfContent(sampleData.Scenarios)) } })() }); return (
+ { @@ -247,7 +280,7 @@ function App() { - + @@ -261,7 +294,7 @@ function App() { - +
); } diff --git a/src/nscenario-report-browser/src/External.ts b/src/nscenario-report-browser/src/External.ts new file mode 100644 index 0000000..83a90e9 --- /dev/null +++ b/src/nscenario-report-browser/src/External.ts @@ -0,0 +1,85 @@ +type SourceControlInfo = { + RepositoryUrl: string | null; + Revision: string | null; +}; + +type SourceControlPathBuilder = (filePath: string, line: number) => string; + +interface ISourceControlPathBuilderFactory { + TryToBuild(sourceControlInfo: SourceControlInfo): SourceControlPathBuilder | null; +} + +class GithubPathBuilderFactory implements ISourceControlPathBuilderFactory { + public TryToBuild(sourceControlInfo: SourceControlInfo): SourceControlPathBuilder | null { + const httpPattern = new RegExp(`https?://github\\.com/(?[^/]+)/(?[^/]+)`); + const httpMatch = httpPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (httpMatch && httpMatch.groups ) { + return (filePath, line) => `https://github.com/${httpMatch.groups?.['user']}/${httpMatch.groups?.['repo']}/blob/${sourceControlInfo.Revision}/${filePath}#L${line}`; + } + + const sshPattern = new RegExp(`(?:ssh://)?git@github\\.com:(?[^/]+)/(?[^.]+)\\.git`); + const sshMatch = sshPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (sshMatch && sshMatch.groups) { + return (filePath, line) => `https://github.com/${sshMatch.groups?.['user']}/${sshMatch.groups?.['repo']}/blob/${sourceControlInfo.Revision}/${filePath}#L${line}`; + } + + return null; + } +} + +class BitbucketServerPathBuilderFactory implements ISourceControlPathBuilderFactory +{ + TryToBuild(sourceControlInfo: SourceControlInfo): SourceControlPathBuilder | null { + const httpPattern = new RegExp(`https?://(?[^/]+)/scm/(?[^/]+)/(?[^.]+)\\.git`); + const httpMatch = httpPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (httpMatch && httpMatch.groups ) { + return (filePath, line) => `https://${httpMatch.groups?.['domain']}/projects/${httpMatch.groups?.['user']}/repos/${httpMatch.groups?.['repo']}/browse/${filePath}?at=${sourceControlInfo.Revision}#${line}`; + } + + const sshPattern = new RegExp(`(?:ssh://)?git@(?[^/]+)/(?[^/]+)/(?[^.]+)\\.git`); + const sshMatch = sshPattern.exec(sourceControlInfo.RepositoryUrl || ""); + if (sshMatch && sshMatch.groups) { + return (filePath, line) => `https://${sshMatch.groups?.['domain']}/projects/${sshMatch.groups?.['user']}/repos/${sshMatch.groups?.['repo']}/browse/${filePath}?at=${sourceControlInfo.Revision}#${line}`; + } + + return null; + } + +} + +export interface ICodeLocation { + FilePath: string; + LineNumber: number; +} + + +export class RepoPathResolver{ + private static readonly _factories: ISourceControlPathBuilderFactory[] = [ + new GithubPathBuilderFactory(), + new BitbucketServerPathBuilderFactory() + ]; + + private static MakePathRelative(rootPath: string, absolutePath: string): string { + const rootUrl = new URL(rootPath, 'file://'); + const absoluteUrl = new URL(absolutePath, 'file://'); + const relativePath = absoluteUrl.href.substring(rootUrl.href.length); + return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; + } + + public static TryToGetPathBuilder(sourceControlInfo: SourceControlInfo, repoRootPath:string | null) : ((location: ICodeLocation) => string ){ + if(sourceControlInfo.RepositoryUrl != null && sourceControlInfo.Revision != null && repoRootPath != null) + { + for (const factory of this._factories) { + const builder = factory.TryToBuild(sourceControlInfo); + if (builder){ + return (location:ICodeLocation) => { + const relativePath = RepoPathResolver.MakePathRelative(repoRootPath, location.FilePath); + return builder(relativePath, location.LineNumber); + } + } + } + } + + return location => location.FilePath; + } +} \ No newline at end of file