From 8240ee968cb4727bbbe34248a0edcaf0e2198117 Mon Sep 17 00:00:00 2001 From: magic56 Date: Thu, 9 Jan 2025 13:05:30 +0100 Subject: [PATCH 1/3] feat: add Moq package and enhance tests for DependencyGraphGenerator and DataExporter --- .../CodeLineCounter.Tests.csproj | 1 + CodeLineCounter.Tests/DataExporterTests.cs | 46 +++++++- .../DependencyGraphGeneratorTests.cs | 37 ++++++- CodeLineCounter.Tests/FileUtilsTests.cs | 24 +++++ .../SolutionAnalyzerTests.cs | 100 ++++++++++++------ .../Services/DependencyGraphGenerator.cs | 8 +- CodeLineCounter/Utils/DataExporter.cs | 11 +- 7 files changed, 182 insertions(+), 45 deletions(-) diff --git a/CodeLineCounter.Tests/CodeLineCounter.Tests.csproj b/CodeLineCounter.Tests/CodeLineCounter.Tests.csproj index 3fe20b0..adc377b 100644 --- a/CodeLineCounter.Tests/CodeLineCounter.Tests.csproj +++ b/CodeLineCounter.Tests/CodeLineCounter.Tests.csproj @@ -15,6 +15,7 @@ all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index c5fc571..daf59d6 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -1,5 +1,6 @@ using CodeLineCounter.Models; using CodeLineCounter.Utils; +using Moq; namespace CodeLineCounter.Tests { @@ -212,7 +213,7 @@ public void get_file_duplications_count_uses_current_dir_for_null_solution_path( // Successfully exports dependencies to specified format (CSV/JSON) and creates DOT file [Fact] - public void export_dependencies_creates_files_in_correct_formats() + public async Task export_dependencies_creates_files_in_correct_formats() { // Arrange var dependencies = new List @@ -224,7 +225,7 @@ public void export_dependencies_creates_files_in_correct_formats() var format = CoreUtils.ExportFormat.JSON; // Act - DataExporter.ExportDependencies(testFilePath, dependencies, format); + await DataExporter.ExportDependencies(testFilePath, dependencies, format); // Assert string expectedJsonPath = CoreUtils.GetExportFileNameWithExtension(testFilePath, format); @@ -240,7 +241,7 @@ public void export_dependencies_creates_files_in_correct_formats() File.Delete(expectedJsonPath); } - if (File.Exists(expectedDotPath)) + if (File.Exists(expectedDotPath)) { File.Delete(expectedDotPath); } @@ -249,12 +250,49 @@ public void export_dependencies_creates_files_in_correct_formats() { throw new IOException($"Error deleting files: {ex.Message}", ex); } - catch (UnauthorizedAccessException ex) + catch (UnauthorizedAccessException ex) { throw new UnauthorizedAccessException($"Access denied while deleting files: {ex.Message}", ex); } } + // Throws ArgumentException when file path is null + [Fact] + public void export_collection_throws_when_filepath_null() + { + // Arrange + string? filePath = null; + var testData = new List { new TestClass { Id = 1, Name = "Test" } }; + var format = CoreUtils.ExportFormat.CSV; + + // Act & Assert + var exception = Assert.Throws(() => + DataExporter.ExportCollection(filePath, testData, format)); + Assert.Equal("File path cannot be null or empty (Parameter 'filePath')", exception.Message); + } + + // ExportCollection throws IOException when there is an IO error during export + // [Fact] + // public void export_collection_throws_ioexception_on_io_error() + // { + // // Arrange + // var filePath = "invalid_path/test.csv"; + // var testData = new List { new TestClass() }; + // var format = CoreUtils.ExportFormat.CSV; + + // // Mock the export strategy to throw an IOException + // var mockExportStrategy = new Mock(); + // mockExportStrategy + // .Setup(strategy => strategy.Export(It.IsAny(), It.IsAny>())) + // .Throws(new IOException("IO error")); + + // DataExporter.ExportStrategies[format] = mockExportStrategy.Object; + + // // Act & Assert + // Assert.Throws(() => + // DataExporter.ExportCollection(filePath, testData, format)); + // } + protected virtual void Dispose(bool disposing) { if (!_disposed) diff --git a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs index b6b8fc2..b78f1bd 100644 --- a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs +++ b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs @@ -1,5 +1,7 @@ using CodeLineCounter.Models; using CodeLineCounter.Services; +using DotNetGraph.Core; +using DotNetGraph.Extensions; namespace CodeLineCounter.Tests { @@ -18,7 +20,9 @@ public async Task generate_graph_with_valid_dependencies_creates_dot_file() string outputPath = Path.Combine(Path.GetTempPath(), "test_graph.dot"); // Act - await DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); + + DotGraph graph = DependencyGraphGenerator.GenerateGraphOnly(dependencies); + await DependencyGraphGenerator.CompileGraphAndWriteToFile(outputPath, graph); // Assert Assert.True(File.Exists(outputPath)); @@ -39,7 +43,8 @@ public async Task generate_graph_with_empty_dependencies_creates_empty_graph() string outputPath = Path.Combine(Path.GetTempPath(), "empty_graph.dot"); // Act - await DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); + DotGraph graph = DependencyGraphGenerator.GenerateGraphOnly(dependencies); + await DependencyGraphGenerator.CompileGraphAndWriteToFile(outputPath, graph); // Assert Assert.True(File.Exists(outputPath)); @@ -50,6 +55,34 @@ public async Task generate_graph_with_empty_dependencies_creates_empty_graph() File.Delete(outputPath); } + [Fact] + public void create_node_sets_correct_fillcolor_and_style_incoming_greater() + { + var vertexInfo = new Dictionary + { + { "TestVertex", (3, 2) } + }; + + DotNode node = DependencyGraphGenerator.CreateNode(vertexInfo, "TestVertex"); + + Assert.Equal(DotColor.MediumSeaGreen.ToHexString(), node.FillColor.Value); + Assert.Equal(DotNodeStyle.Filled.FlagsToString(), node.Style.Value); + } + + [Fact] + public void create_node_sets_correct_fillcolor_and_style_incoming_lower() + { + var vertexInfo = new Dictionary + { + { "TestVertex", (3, 4) } + }; + + DotNode node = DependencyGraphGenerator.CreateNode(vertexInfo, "TestVertex"); + + Assert.Equal(DotColor.Salmon.ToHexString(), node.FillColor.Value); + Assert.Equal(DotNodeStyle.Filled.FlagsToString(), node.Style.Value); + } + } } diff --git a/CodeLineCounter.Tests/FileUtilsTests.cs b/CodeLineCounter.Tests/FileUtilsTests.cs index 91d146a..ea4c5f0 100644 --- a/CodeLineCounter.Tests/FileUtilsTests.cs +++ b/CodeLineCounter.Tests/FileUtilsTests.cs @@ -31,5 +31,29 @@ public void GetBasePath_Should_Return_NonEmptyString() Assert.False(string.IsNullOrEmpty(basePath), "Base path should not be null or empty."); Assert.True(Directory.Exists(basePath), "Base path should be a valid directory."); } + + [Fact] + public void get_solution_files_throws_exception_for_nonexistent_directory() + { + // Arrange + var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + // Act & Assert + Assert.Throws(() => FileUtils.GetSolutionFiles(nonExistentPath)); + } + + // Throws UnauthorizedAccessException when solution file does not exist + [Fact] + public void get_project_files_throws_when_file_not_exists() + { + // Arrange + var nonExistentPath = "nonexistent.sln"; + + // Act & Assert + var exception = Assert.Throws(() => + FileUtils.GetProjectFiles(nonExistentPath)); + + Assert.Contains(nonExistentPath, exception.Message); + } } } \ No newline at end of file diff --git a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs index bdd665e..5c8f71d 100644 --- a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs +++ b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs @@ -7,6 +7,40 @@ namespace CodeLineCounter.Tests.Services public class SolutionAnalyzerTest { + [Fact] + public void analyze_and_export_solution_succeeds_with_valid_inputs() + { + // Arrange + var basePath = FileUtils.GetBasePath(); + var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); + solutionPath = Path.Combine(solutionPath, "CodeLineCounter.sln"); + var sw = new StringWriter(); + Console.SetOut(sw); + var verbose = false; + var format = CoreUtils.ExportFormat.JSON; + + // Act & Assert + var exception = Record.Exception(() => + SolutionAnalyzer.AnalyzeAndExportSolution(solutionPath, verbose, format)); + + Assert.Null(exception); + } + + [Fact] + public void analyze_and_export_solution_throws_on_invalid_path() + { + // Arrange + var invalidPath = ""; + var verbose = false; + var format = CoreUtils.ExportFormat.JSON; + + // Act & Assert + var exception = Assert.Throws(() => + SolutionAnalyzer.AnalyzeAndExportSolution(invalidPath, verbose, format)); + + Assert.Contains("Access to the path '' is denied.", exception.Message); + } + [Fact] public void PerformAnalysis_ShouldReturnCorrectAnalysisResult() { @@ -106,7 +140,7 @@ public void OutputDetailedMetrics_ShouldPrintMetricsAndProjectTotals() SolutionAnalyzer.OutputDetailedMetrics(metrics, projectTotals); // Assert - var expectedOutput = + var expectedOutput = $"Project Project1 (/path/to/project1) - Namespace Namespace1 in file File1.cs (/path/to/project1/File1.cs) has 100 lines of code and a cyclomatic complexity of 10.{Environment.NewLine}" + $"Project Project2 (/path/to/project2) - Namespace Namespace2 in file File2.cs (/path/to/project2/File2.cs) has 200 lines of code and a cyclomatic complexity of 20.{Environment.NewLine}" + $"Project Project1 has 100 total lines of code.{Environment.NewLine}" + @@ -116,39 +150,39 @@ public void OutputDetailedMetrics_ShouldPrintMetricsAndProjectTotals() } } - // Export metrics, duplications and dependencies data in parallel for valid input -[Fact] -public void export_results_with_valid_input_exports_all_files() -{ - // Arrange - var result = new AnalysisResult - { - SolutionFileName = "TestSolution", - Metrics = new List(), - ProjectTotals = new Dictionary(), - TotalLines = 1000, - DuplicationMap = new List(), - DependencyList = new List() - }; - - var basePath = FileUtils.GetBasePath(); - var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); - - solutionPath = Path.Combine(solutionPath, "TestSolution.sln"); - var format = CoreUtils.ExportFormat.CSV; - - // Act - using (var sw = new StringWriter()) - { - Console.SetOut(sw); - SolutionAnalyzer.ExportResults(result, solutionPath, format); - } + // Export metrics, duplications and dependencies data in parallel for valid input + [Fact] + public void export_results_with_valid_input_exports_all_files() + { + // Arrange + var result = new AnalysisResult + { + SolutionFileName = "TestSolution", + Metrics = new List(), + ProjectTotals = new Dictionary(), + TotalLines = 1000, + DuplicationMap = new List(), + DependencyList = new List() + }; + + var basePath = FileUtils.GetBasePath(); + var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); + + solutionPath = Path.Combine(solutionPath, "TestSolution.sln"); + var format = CoreUtils.ExportFormat.CSV; - // Assert - Assert.True(File.Exists("TestSolution-CodeMetrics.csv")); - Assert.True(File.Exists("TestSolution-CodeDuplications.csv")); - Assert.True(File.Exists("TestSolution-CodeDependencies.csv")); -} + // Act + using (var sw = new StringWriter()) + { + Console.SetOut(sw); + SolutionAnalyzer.ExportResults(result, solutionPath, format); + } + + // Assert + Assert.True(File.Exists("TestSolution-CodeMetrics.csv")); + Assert.True(File.Exists("TestSolution-CodeDuplications.csv")); + Assert.True(File.Exists("TestSolution-CodeDependencies.csv")); + } } } \ No newline at end of file diff --git a/CodeLineCounter/Services/DependencyGraphGenerator.cs b/CodeLineCounter/Services/DependencyGraphGenerator.cs index b9331f1..d2c56b0 100644 --- a/CodeLineCounter/Services/DependencyGraphGenerator.cs +++ b/CodeLineCounter/Services/DependencyGraphGenerator.cs @@ -10,7 +10,7 @@ namespace CodeLineCounter.Services { public static class DependencyGraphGenerator { - public static async Task GenerateGraph(List dependencies, string outputPath, string? filterNamespace = null, string? filterAssembly = null) + public static DotGraph GenerateGraphOnly(List dependencies, string? filterNamespace = null, string? filterAssembly = null) { var filteredDependencies = dependencies; filteredDependencies = FilterNamespaceFromDependencies(dependencies, filterNamespace, filteredDependencies); @@ -51,7 +51,7 @@ public static async Task GenerateGraph(List dependencies, st graph.Elements.Add(edge); } - await CompileGraphAndWriteToFile(outputPath, graph); + return graph; } private static DotEdge CreateEdge(DependencyRelation dep) @@ -80,7 +80,7 @@ private static DotSubgraph CreateCluster(KeyValuePair> nsGr return cluster; } - private static DotNode CreateNode(Dictionary vertexInfo, string vertex) + public static DotNode CreateNode(Dictionary vertexInfo, string vertex) { var info = vertexInfo[vertex]; var node = new DotNode(); @@ -142,7 +142,7 @@ private static void GroupByNamespace(Dictionary ExportStrategies + { + get { return _exportStrategies; } + } + public static void Export(string filePath, T data, CoreUtils.ExportFormat format) where T : class { ArgumentNullException.ThrowIfNull(data); @@ -20,7 +26,7 @@ public static void Export(string filePath, T data, CoreUtils.ExportFormat for ExportCollection(filePath, [data], format); } - public static void ExportCollection(string filePath, IEnumerable data, CoreUtils.ExportFormat format) where T : class + public static void ExportCollection(string? filePath, IEnumerable data, CoreUtils.ExportFormat format) where T : class { if (string.IsNullOrEmpty(filePath)) throw new ArgumentException("File path cannot be null or empty", nameof(filePath)); @@ -48,7 +54,8 @@ public static async Task ExportDependencies(string filePath, List metrics, From 77670009d67ad45a80eb33fb1a11d0cee175d201 Mon Sep 17 00:00:00 2001 From: magic56 Date: Thu, 9 Jan 2025 13:23:33 +0100 Subject: [PATCH 2/3] feat: add launch configuration for CodeLineCounter in VSCode Signed-off-by: magic56 --- .vscode/launch.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index b1cf6dd..60f073a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,16 @@ "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "internalConsole" + }, + { + "name": "C#: CodeLineCounter - Isacompta", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/CodeLineCounter/bin/Debug/net9.0/CodeLineCounter.dll", + "args": ["-d", "E:/_TFS/Isagri.CO/Isagri_Dev_CO_Compta/Isagri.CO/Main/Isaco/DotNet/Sources"], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole" } ] } \ No newline at end of file From ed5338783d6b4baafb2ea1f6f6ec896e2227ffe4 Mon Sep 17 00:00:00 2001 From: magic56 Date: Thu, 9 Jan 2025 14:12:36 +0100 Subject: [PATCH 3/3] refactor: remove unused export strategy property and commented test code in DataExporterTests --- CodeLineCounter.Tests/DataExporterTests.cs | 22 ---------------------- CodeLineCounter/Utils/DataExporter.cs | 5 ----- 2 files changed, 27 deletions(-) diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index daf59d6..fa50c5c 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -271,28 +271,6 @@ public void export_collection_throws_when_filepath_null() Assert.Equal("File path cannot be null or empty (Parameter 'filePath')", exception.Message); } - // ExportCollection throws IOException when there is an IO error during export - // [Fact] - // public void export_collection_throws_ioexception_on_io_error() - // { - // // Arrange - // var filePath = "invalid_path/test.csv"; - // var testData = new List { new TestClass() }; - // var format = CoreUtils.ExportFormat.CSV; - - // // Mock the export strategy to throw an IOException - // var mockExportStrategy = new Mock(); - // mockExportStrategy - // .Setup(strategy => strategy.Export(It.IsAny(), It.IsAny>())) - // .Throws(new IOException("IO error")); - - // DataExporter.ExportStrategies[format] = mockExportStrategy.Object; - - // // Act & Assert - // Assert.Throws(() => - // DataExporter.ExportCollection(filePath, testData, format)); - // } - protected virtual void Dispose(bool disposing) { if (!_disposed) diff --git a/CodeLineCounter/Utils/DataExporter.cs b/CodeLineCounter/Utils/DataExporter.cs index 0f1745d..a39e188 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -14,11 +14,6 @@ public static class DataExporter { CoreUtils.ExportFormat.JSON, new JsonExportStrategy() } }; - public static Dictionary ExportStrategies - { - get { return _exportStrategies; } - } - public static void Export(string filePath, T data, CoreUtils.ExportFormat format) where T : class { ArgumentNullException.ThrowIfNull(data);