Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
1 change: 1 addition & 0 deletions CodeLineCounter.Tests/CodeLineCounter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
24 changes: 20 additions & 4 deletions CodeLineCounter.Tests/DataExporterTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CodeLineCounter.Models;
using CodeLineCounter.Utils;
using Moq;

namespace CodeLineCounter.Tests
{
Expand Down Expand Up @@ -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<DependencyRelation>
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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<TestClass> { new TestClass { Id = 1, Name = "Test" } };
var format = CoreUtils.ExportFormat.CSV;

// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
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)
Expand Down
37 changes: 35 additions & 2 deletions CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using CodeLineCounter.Models;
using CodeLineCounter.Services;
using DotNetGraph.Core;
using DotNetGraph.Extensions;

namespace CodeLineCounter.Tests
{
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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<string, (int incoming, int outgoing)>
{
{ "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<string, (int incoming, int outgoing)>
{
{ "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);
}


}
}
24 changes: 24 additions & 0 deletions CodeLineCounter.Tests/FileUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnauthorizedAccessException>(() => 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<UnauthorizedAccessException>(() =>
FileUtils.GetProjectFiles(nonExistentPath));

Assert.Contains(nonExistentPath, exception.Message);
}
}
}
100 changes: 67 additions & 33 deletions CodeLineCounter.Tests/SolutionAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnauthorizedAccessException>(() =>
SolutionAnalyzer.AnalyzeAndExportSolution(invalidPath, verbose, format));

Assert.Contains("Access to the path '' is denied.", exception.Message);
}

[Fact]
public void PerformAnalysis_ShouldReturnCorrectAnalysisResult()
{
Expand Down Expand Up @@ -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}" +
Expand All @@ -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<NamespaceMetrics>(),
ProjectTotals = new Dictionary<string, int >(),
TotalLines = 1000,
DuplicationMap = new List<DuplicationCode>(),
DependencyList = new List<DependencyRelation>()
};

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<NamespaceMetrics>(),
ProjectTotals = new Dictionary<string, int>(),
TotalLines = 1000,
DuplicationMap = new List<DuplicationCode>(),
DependencyList = new List<DependencyRelation>()
};

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"));
}

}
}
8 changes: 4 additions & 4 deletions CodeLineCounter/Services/DependencyGraphGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{
public static class DependencyGraphGenerator
{
public static async Task GenerateGraph(List<DependencyRelation> dependencies, string outputPath, string? filterNamespace = null, string? filterAssembly = null)
public static DotGraph GenerateGraphOnly(List<DependencyRelation> dependencies, string? filterNamespace = null, string? filterAssembly = null)
{
var filteredDependencies = dependencies;
filteredDependencies = FilterNamespaceFromDependencies(dependencies, filterNamespace, filteredDependencies);
Expand Down Expand Up @@ -51,7 +51,7 @@
graph.Elements.Add(edge);
}

await CompileGraphAndWriteToFile(outputPath, graph);
return graph;
}

private static DotEdge CreateEdge(DependencyRelation dep)
Expand Down Expand Up @@ -80,7 +80,7 @@
return cluster;
}

private static DotNode CreateNode(Dictionary<string, (int incoming, int outgoing)> vertexInfo, string vertex)
public static DotNode CreateNode(Dictionary<string, (int incoming, int outgoing)> vertexInfo, string vertex)
{
var info = vertexInfo[vertex];
var node = new DotNode();
Expand Down Expand Up @@ -142,7 +142,7 @@
}
}

private static async Task CompileGraphAndWriteToFile(string outputPath, DotGraph graph)
public static async Task CompileGraphAndWriteToFile(string outputPath, DotGraph graph)
{
await using var writer = new StringWriter();
var options = new CompilationOptions
Expand All @@ -156,7 +156,7 @@
await graph.CompileAsync(context);
var result = writer.GetStringBuilder().ToString();
//using sync write for reliability with async we got some issues
File.WriteAllText(outputPath, result);

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Linux .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Linux .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Windows .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Windows .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Linux .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Linux .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / macOS .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / macOS .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Windows .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / Windows .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / macOS .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)

Check warning on line 159 in CodeLineCounter/Services/DependencyGraphGenerator.cs

View workflow job for this annotation

GitHub Actions / macOS .NET 9

Await WriteAllTextAsync instead. (https://rules.sonarsource.com/csharp/RSPEC-6966)
}

private static List<DependencyRelation> FilterAssemblyFromDependencies(string? filterAssembly, List<DependencyRelation> filteredDependencies)
Expand Down
6 changes: 4 additions & 2 deletions CodeLineCounter/Utils/DataExporter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CodeLineCounter.Models;
using CodeLineCounter.Services;
using DotNetGraph.Core;


namespace CodeLineCounter.Utils
Expand All @@ -20,7 +21,7 @@ public static void Export<T>(string filePath, T data, CoreUtils.ExportFormat for
ExportCollection(filePath, [data], format);
}

public static void ExportCollection<T>(string filePath, IEnumerable<T> data, CoreUtils.ExportFormat format) where T : class
public static void ExportCollection<T>(string? filePath, IEnumerable<T> data, CoreUtils.ExportFormat format) where T : class
{
if (string.IsNullOrEmpty(filePath))
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
Expand Down Expand Up @@ -48,7 +49,8 @@ public static async Task ExportDependencies(string filePath, List<DependencyRela
string outputFilePath = CoreUtils.GetExportFileNameWithExtension(filePath, format);
ExportCollection(outputFilePath, dependencies, format);

await DependencyGraphGenerator.GenerateGraph(dependencies, Path.ChangeExtension(outputFilePath, ".dot"));
DotGraph graph = DependencyGraphGenerator.GenerateGraphOnly(dependencies);
await DependencyGraphGenerator.CompileGraphAndWriteToFile(Path.ChangeExtension(outputFilePath, ".dot"), graph);
}

public static void ExportMetrics(string filePath, List<NamespaceMetrics> metrics,
Expand Down
Loading