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 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..fa50c5c 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,27 @@ 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); + } + 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(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 +49,8 @@ public static async Task ExportDependencies(string filePath, List metrics,