Skip to content

Commit 4313229

Browse files
authored
Parallel dependency analysis (#7)
* Parallel dependency analysis * Added automatic approval tool * Better debug output for approval tests * Split parser in separate classes
1 parent bd3307c commit 4313229

26 files changed

+799
-437
lines changed
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\CodeParser\CodeParser.csproj" />
16+
<ProjectReference Include="..\Contracts\Contracts.csproj" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<None Update="Repositories.txt">
21+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
22+
</None>
23+
</ItemGroup>
24+
25+
</Project>

ApprovalTestTool/Program.cs

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
using System.Reflection;
2+
using CodeParser.Parser;
3+
using CodeParser.Parser.Config;
4+
using LibGit2Sharp;
5+
6+
namespace ApprovalTestTool;
7+
8+
internal class TestTool
9+
{
10+
/// <summary>
11+
/// Automatic approval tool
12+
/// For each line in the repositories.txt
13+
/// 1. Clone or pull the repository
14+
/// 2. Checkout the specified commit
15+
/// 3. Run test code: Parse the solution and write the output to a file.
16+
/// 4. Compare output with reference or copy to reference folder if not exists yet.
17+
/// 5. Print test result
18+
///
19+
/// Note: The reference files are not committed, so save space.
20+
/// You can always check out an older tag and create the reference files.
21+
/// </summary>
22+
private static async Task Main(string[] args)
23+
{
24+
var referenceFolder = @"d:\\ApprovalTests\\References";
25+
var gitCloneFolder = @"d:\\ApprovalTests\\Repositories";
26+
27+
if (args.Length == 2)
28+
{
29+
referenceFolder = args[0];
30+
gitCloneFolder = args[1];
31+
}
32+
else
33+
{
34+
Console.WriteLine("Usage: TestTool <reference-folder> <git-clone-folder>");
35+
Console.WriteLine("Using default folders.");
36+
}
37+
38+
39+
Console.WriteLine("Use reference folder: " + referenceFolder);
40+
Console.WriteLine("Use git clone folder: " + gitCloneFolder);
41+
42+
43+
EnsureDirectoryExists(referenceFolder);
44+
EnsureDirectoryExists(gitCloneFolder);
45+
46+
var executablePath = Assembly.GetExecutingAssembly().Location;
47+
var executableDirectory = Path.GetDirectoryName(executablePath);
48+
var repoFile = Path.Combine(executableDirectory, "repositories.txt");
49+
50+
if (!File.Exists(repoFile))
51+
{
52+
Console.WriteLine($"Input file 'repositories.txt' not found in {executableDirectory}");
53+
return;
54+
}
55+
56+
57+
Initializer.InitializeMsBuildLocator();
58+
59+
foreach (var line in File.ReadLines(repoFile))
60+
{
61+
var parts = line.Split(',');
62+
if (parts.Length != 3)
63+
{
64+
Console.WriteLine($"Invalid input line: {line}");
65+
continue;
66+
}
67+
68+
var repoUrl = parts[0];
69+
var slnRelativePath = parts[1];
70+
var commitHash = parts[2];
71+
72+
await ProcessRepository(repoUrl, slnRelativePath, commitHash, gitCloneFolder, referenceFolder);
73+
}
74+
}
75+
76+
private static async Task ProcessRepository(string repoUrl, string slnRelativePath, string commitHash,
77+
string gitCloneFolder, string referenceFolder)
78+
{
79+
var repoName = Path.GetFileNameWithoutExtension(repoUrl);
80+
var repoPath = Path.Combine(gitCloneFolder, repoName);
81+
82+
// Clone or pull the repository
83+
if (!Directory.Exists(repoPath))
84+
{
85+
Repository.Clone(repoUrl, repoPath);
86+
}
87+
else
88+
{
89+
using (var repo = new Repository(repoPath))
90+
{
91+
Commands.Fetch(repo, "origin", Array.Empty<string>(), new FetchOptions(), null);
92+
}
93+
}
94+
95+
// Checkout the specified commit
96+
using (var repo = new Repository(repoPath))
97+
{
98+
var commit = repo.Lookup<Commit>(commitHash);
99+
if (commit == null)
100+
{
101+
Console.WriteLine($"Commit {commitHash} not found in repository {repoName}");
102+
return;
103+
}
104+
105+
Commands.Checkout(repo, commit);
106+
}
107+
108+
// Generate paths
109+
var slnPath = Path.Combine(repoPath, slnRelativePath);
110+
var outputFileName = $"{commitHash}.txt";
111+
var outputPath = Path.Combine(gitCloneFolder, outputFileName);
112+
113+
// Run test code (placeholder)
114+
await RunTestCode(slnPath, outputPath);
115+
116+
// Compare output with reference or copy to reference folder
117+
var referencePath = Path.Combine(referenceFolder, outputFileName);
118+
if (File.Exists(referencePath))
119+
{
120+
var areEqual = CompareFiles(outputPath, referencePath);
121+
PrintColoredTestResult(repoName, commitHash, areEqual);
122+
}
123+
else
124+
{
125+
File.Copy(outputPath, referencePath);
126+
Console.WriteLine($"No reference file for {repoName} at {commitHash}. Created new reference file.");
127+
}
128+
}
129+
130+
private static async Task RunTestCode(string slnPath, string outputPath)
131+
{
132+
var parserConfig = new ParserConfig(new ProjectExclusionRegExCollection(), 1);
133+
var parser = new Parser(parserConfig);
134+
var graph = await parser.ParseSolution(slnPath);
135+
await File.WriteAllTextAsync(outputPath, graph.ToDebug());
136+
}
137+
138+
private static bool CompareFiles(string file1, string file2)
139+
{
140+
using var stream1 = File.OpenRead(file1);
141+
using var stream2 = File.OpenRead(file2);
142+
if (stream1.Length != stream2.Length)
143+
{
144+
return false;
145+
}
146+
147+
using var reader1 = new StreamReader(stream1);
148+
using var reader2 = new StreamReader(stream2);
149+
while (reader1.ReadLine() is { } line1)
150+
{
151+
var line2 = reader2.ReadLine();
152+
if (line2 == null || line1 != line2)
153+
{
154+
return false;
155+
}
156+
}
157+
158+
// Since the files have the same length that should never happen.
159+
return reader2.ReadLine() == null;
160+
}
161+
162+
private static void PrintColoredTestResult(string repoName, string commitHash, bool passed)
163+
{
164+
Console.Write($"Test for {repoName} at {commitHash}: ");
165+
Console.ForegroundColor = passed ? ConsoleColor.Green : ConsoleColor.Red;
166+
Console.WriteLine(passed ? "Passed" : "Failed");
167+
Console.ResetColor();
168+
}
169+
170+
171+
private static void EnsureDirectoryExists(string path)
172+
{
173+
if (!Directory.Exists(path))
174+
{
175+
try
176+
{
177+
Directory.CreateDirectory(path);
178+
Console.WriteLine($"Created directory: {path}");
179+
}
180+
catch (Exception ex)
181+
{
182+
Console.WriteLine($"Error creating directory {path}: {ex.Message}");
183+
Environment.Exit(1);
184+
}
185+
}
186+
}
187+
}

ApprovalTestTool/Repositories.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
https://github.com/ATrefzer/Insight.git,Insight.sln,446736ffcda573c08f80d95b8ecf13675bd6a486
2+
https://github.com/ATrefzer/CSharpCodeAnalyst.git,SampleProject\SampleProject.sln,bd3307c6745e1b6bc13f3a3e0c687c276eac8299

CSharpCodeAnalyst.sln

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
2626
EndProject
2727
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contracts", "Contracts\Contracts.csproj", "{7D5EA751-84A3-43F1-BCCE-C9D10536AB1B}"
2828
EndProject
29+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApprovalTestTool", "ApprovalTestTool\ApprovalTestTool.csproj", "{767539BE-FBE3-4B46-9A5E-21D60E1B278B}"
30+
EndProject
2931
Global
3032
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3133
Debug|Any CPU = Debug|Any CPU
@@ -48,6 +50,10 @@ Global
4850
{7D5EA751-84A3-43F1-BCCE-C9D10536AB1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
4951
{7D5EA751-84A3-43F1-BCCE-C9D10536AB1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
5052
{7D5EA751-84A3-43F1-BCCE-C9D10536AB1B}.Release|Any CPU.Build.0 = Release|Any CPU
53+
{767539BE-FBE3-4B46-9A5E-21D60E1B278B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54+
{767539BE-FBE3-4B46-9A5E-21D60E1B278B}.Debug|Any CPU.Build.0 = Debug|Any CPU
55+
{767539BE-FBE3-4B46-9A5E-21D60E1B278B}.Release|Any CPU.ActiveCfg = Release|Any CPU
56+
{767539BE-FBE3-4B46-9A5E-21D60E1B278B}.Release|Any CPU.Build.0 = Release|Any CPU
5157
EndGlobalSection
5258
GlobalSection(SolutionProperties) = preSolution
5359
HideSolutionNode = FALSE

CSharpCodeAnalyst/Configuration/ApplicationSettings.cs

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ public class ApplicationSettings
55
public int WarningCodeElementLimit { get; set; } = 50;
66
public string DefaultProjectExcludeFilter { get; set; } = string.Empty;
77
public bool DefaultShowQuickHelp { get; set; }
8+
public int MaxDegreeOfParallelism { get; set; } = 8;
89
}

CSharpCodeAnalyst/MainViewModel.cs

+10-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Globalization;
55
using System.IO;
66
using System.Reflection;
7+
using System.Runtime;
78
using System.Text.Json;
89
using System.Text.Json.Serialization;
910
using System.Windows;
@@ -14,6 +15,7 @@
1415
using CodeParser.Extensions;
1516
using CodeParser.Parser;
1617
using CodeParser.Parser.Config;
18+
using Contracts.Common;
1719
using Contracts.Graph;
1820
using CSharpCodeAnalyst.Common;
1921
using CSharpCodeAnalyst.Configuration;
@@ -63,12 +65,16 @@ internal class MainViewModel : INotifyPropertyChanged
6365

6466
internal MainViewModel(MessageBus messaging, ApplicationSettings? settings)
6567
{
68+
// Default values
6669
_projectExclusionFilters = new ProjectExclusionRegExCollection();
70+
_maxDegreeOfParallelism = 8;
71+
_isInfoPanelVisible = false;
6772

6873
if (settings != null)
6974
{
7075
_isInfoPanelVisible = settings.DefaultShowQuickHelp;
7176
_projectExclusionFilters.Initialize(settings.DefaultProjectExcludeFilter, ";");
77+
_maxDegreeOfParallelism = settings.MaxDegreeOfParallelism;
7278
}
7379

7480
_messaging = messaging;
@@ -314,6 +320,7 @@ GraphSession AddSession(string name)
314320
}
315321

316322
LegendDialog? _openedLegendDialog;
323+
private readonly int _maxDegreeOfParallelism;
317324

318325
private void ShowLegend()
319326
{
@@ -525,11 +532,11 @@ private async Task LoadAndAnalyzeSolution(string solutionPath)
525532
private async Task<CodeGraph> LoadAsync(string solutionPath)
526533
{
527534
LoadMessage = "Loading ...";
528-
var parser = new Parser(new ParserConfig(_projectExclusionFilters));
529-
parser.ParserProgress += OnProgress;
535+
var parser = new Parser(new ParserConfig(_projectExclusionFilters, _maxDegreeOfParallelism));
536+
parser.Progress.ParserProgress += OnProgress;
530537
var graph = await parser.ParseSolution(solutionPath).ConfigureAwait(true);
531538

532-
parser.ParserProgress -= OnProgress;
539+
parser.Progress.ParserProgress -= OnProgress;
533540
return graph;
534541
}
535542

CSharpCodeAnalyst/appsettings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"ApplicationSettings": {
33
"WarningCodeElementLimit": 200,
44
"DefaultProjectExcludeFilter": ".*Tests",
5-
"DefaultShowQuickHelp": true
5+
"DefaultShowQuickHelp": true,
6+
"MaxDegreeOfParallelism": 8
67
}
78
}

CSharpCodeAnalyst/board.txt

-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
IMPROVEMENTS
22
---------------------
3-
- Basic approval Test for event implementation
4-
5-
- Write external approval application agains real projects.
63

74
- Configure editor to open text files.
85
- Reduce incoming calls! There are way too much wong code elements added

CodeParser/Analysis/Cycles/SearchGraph.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using CodeParser.Analysis.Shared;
2-
using GraphLib.Contracts;
2+
using Contracts.GraphInterface;
33

44
namespace CodeParser.Analysis.Cycles;
55

CodeParser/Analysis/Shared/Tarjan.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using GraphLib.Contracts;
1+
using Contracts.GraphInterface;
22

33
namespace GraphLib.Algorithms.StronglyConnectedComponents;
44

CodeParser/Parser/Artifacts.cs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Collections.ObjectModel;
2+
using Contracts.Graph;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
6+
namespace CodeParser.Parser;
7+
8+
/// <summary>
9+
/// Artifacts from the first phase of the parser.
10+
/// This information is needed to build the relationships in phase 2.
11+
/// </summary>
12+
public class Artifacts(
13+
ReadOnlyCollection<INamedTypeSymbol> allNamedTypesInSolution,
14+
ReadOnlyDictionary<string, ISymbol> elementIdToSymbolMap,
15+
ReadOnlyDictionary<IAssemblySymbol, List<GlobalStatementSyntax>> globalStatementsByAssembly,
16+
ReadOnlyDictionary<string, CodeElement> symbolKeyToElementMap)
17+
{
18+
public ReadOnlyCollection<INamedTypeSymbol> AllNamedTypesInSolution { get; } = allNamedTypesInSolution;
19+
public ReadOnlyDictionary<string, ISymbol> ElementIdToSymbolMap { get; } = elementIdToSymbolMap;
20+
public ReadOnlyDictionary<IAssemblySymbol, List<GlobalStatementSyntax>> GlobalStatementsByAssembly { get; } = globalStatementsByAssembly;
21+
public ReadOnlyDictionary<string, CodeElement> SymbolKeyToElementMap { get; } = symbolKeyToElementMap;
22+
}

CodeParser/Parser/Config/ParserConfig.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ public class ParserConfig
44
{
55
private readonly ProjectExclusionRegExCollection _projectExclusionFilters;
66

7-
public ParserConfig(ProjectExclusionRegExCollection projectExclusionFilters)
7+
public ParserConfig(ProjectExclusionRegExCollection projectExclusionFilters, int maxDegreeOfParallelism)
88
{
99
_projectExclusionFilters = projectExclusionFilters;
10+
MaxDegreeOfParallelism = maxDegreeOfParallelism;
1011
}
1112

13+
public int MaxDegreeOfParallelism { get; set; }
14+
1215
public bool IsProjectIncluded(string projectName)
1316
{
1417
return _projectExclusionFilters.IsProjectIncluded(projectName);

0 commit comments

Comments
 (0)