Skip to content

Commit 3f7d940

Browse files
Enhance cognitive analysis with progress reporting and file searching features (#11)
- Introduced `AnalysisProgress` enum and struct to track analysis phases and file processing status. - Updated `SourceFileFinder` to report progress during file searching. - Enhanced `CognitiveCodeAnalyser` and `CognitiveAnalysisFacade` to support progress reporting during file analysis. - Implemented `SpectreAnalysisProgressReporter` for visual feedback in the console application. - Added tests to validate progress reporting functionality during file searching and analysis. - Removed unused Spectre.Console package references from the main project and added them to the console app project. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7008a79 commit 3f7d940

14 files changed

Lines changed: 650 additions & 31 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <copyright company="Florian Krämer">
2+
/// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
/// </copyright>
4+
5+
using CognitiveCodeAnalysis.CognitiveAnalysis;
6+
7+
namespace CognitiveCodeAnalysis.Tests.CognitiveAnalysis;
8+
9+
internal sealed class AnalysisProgressCollector : IProgress<AnalysisProgress>
10+
{
11+
private readonly List<AnalysisProgress> _reports = [];
12+
private readonly object _lock = new();
13+
14+
public IReadOnlyList<AnalysisProgress> Reports
15+
{
16+
get
17+
{
18+
lock (_lock)
19+
{
20+
return _reports.ToList();
21+
}
22+
}
23+
}
24+
25+
public void Report(AnalysisProgress value)
26+
{
27+
lock (_lock)
28+
{
29+
_reports.Add(value);
30+
}
31+
}
32+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/// <copyright company="Florian Krämer">
2+
/// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
/// </copyright>
4+
5+
using CognitiveCodeAnalysis.CodeCoverage;
6+
using CognitiveCodeAnalysis.CognitiveAnalysis;
7+
using CognitiveCodeAnalysis.Configuration;
8+
using CognitiveCodeAnalysis.CouplingAnalysis;
9+
10+
namespace CognitiveCodeAnalysis.Tests.CognitiveAnalysis;
11+
12+
public class AnalysisProgressTests
13+
{
14+
private TempFiles _tempFiles = null!;
15+
private CognitiveConfiguration _configuration = null!;
16+
17+
[SetUp]
18+
public void SetUp()
19+
{
20+
_tempFiles = new TempFiles();
21+
_configuration = new CognitiveConfiguration();
22+
}
23+
24+
[TearDown]
25+
public void TearDown()
26+
{
27+
_tempFiles.CleanUp();
28+
}
29+
30+
[Test]
31+
public void FindSourceFiles_ReportsSearchPhases()
32+
{
33+
_tempFiles.CreateFileWithContent("File1.cs", "// test");
34+
_tempFiles.CreateFileWithContent("File2.cs", "// test");
35+
36+
var collector = new AnalysisProgressCollector();
37+
var finder = new SourceFileFinder();
38+
39+
finder.FindSourceFiles([_tempFiles.tmpDirectory], collector);
40+
41+
var reports = collector.Reports;
42+
using (Assert.EnterMultipleScope())
43+
{
44+
Assert.That(reports, Has.Count.EqualTo(2));
45+
Assert.That(reports[0].Phase, Is.EqualTo(AnalysisProgressPhase.SearchingFiles));
46+
Assert.That(reports[1].Phase, Is.EqualTo(AnalysisProgressPhase.SearchCompleted));
47+
Assert.That(reports[1].TotalFiles, Is.EqualTo(2));
48+
}
49+
}
50+
51+
[Test]
52+
public void FindSourceFiles_WithNoProgress_DoesNotThrow()
53+
{
54+
var file1 = _tempFiles.CreateFileWithContent("File1.cs", "// test");
55+
var file2 = _tempFiles.CreateFileWithContent("File2.cs", "// test");
56+
var finder = new SourceFileFinder();
57+
58+
var result = finder.FindSourceFiles([_tempFiles.tmpDirectory]);
59+
60+
using (Assert.EnterMultipleScope())
61+
{
62+
Assert.That(result, Has.Count.EqualTo(2));
63+
Assert.That(result, Contains.Item(file1));
64+
Assert.That(result, Contains.Item(file2));
65+
}
66+
}
67+
68+
[Test]
69+
public void FindSourceFiles_EmptyDirectory_ReportsZeroCount()
70+
{
71+
var collector = new AnalysisProgressCollector();
72+
var finder = new SourceFileFinder();
73+
74+
finder.FindSourceFiles([_tempFiles.tmpDirectory], collector);
75+
76+
var reports = collector.Reports;
77+
using (Assert.EnterMultipleScope())
78+
{
79+
Assert.That(reports, Has.Count.EqualTo(2));
80+
Assert.That(reports[1].Phase, Is.EqualTo(AnalysisProgressPhase.SearchCompleted));
81+
Assert.That(reports[1].TotalFiles, Is.EqualTo(0));
82+
}
83+
}
84+
85+
[Test]
86+
public async Task AnalyseFilesAsync_ReportsPerFileProgress()
87+
{
88+
const string content = @"
89+
namespace X {
90+
public class Y {
91+
public void Run() { }
92+
}
93+
}";
94+
_tempFiles.CreateFileWithContent("File1.cs", content);
95+
_tempFiles.CreateFileWithContent("File2.cs", content);
96+
_tempFiles.CreateFileWithContent("File3.cs", content);
97+
98+
var collector = new AnalysisProgressCollector();
99+
var analyser = new CognitiveCodeAnalyser();
100+
var files = Directory.GetFiles(_tempFiles.tmpDirectory, "*.cs").ToList();
101+
102+
await analyser.AnalyseFilesAsync(files, _configuration, collector);
103+
104+
var reports = collector.Reports;
105+
var analysingReports = reports.Where(r => r.Phase == AnalysisProgressPhase.AnalysingFiles).ToList();
106+
107+
using (Assert.EnterMultipleScope())
108+
{
109+
Assert.That(analysingReports, Has.Count.EqualTo(4));
110+
Assert.That(analysingReports[0].ProcessedFiles, Is.EqualTo(0));
111+
Assert.That(analysingReports[0].TotalFiles, Is.EqualTo(3));
112+
Assert.That(analysingReports.Skip(1).Select(r => r.ProcessedFiles), Is.EquivalentTo(new[] { 1, 2, 3 }));
113+
Assert.That(reports[^1].Phase, Is.EqualTo(AnalysisProgressPhase.AnalysisCompleted));
114+
Assert.That(reports[^1].ProcessedFiles, Is.EqualTo(3));
115+
Assert.That(reports[^1].TotalFiles, Is.EqualTo(3));
116+
}
117+
}
118+
119+
[Test]
120+
public async Task AnalyseFilesAsync_WithNoProgress_DoesNotThrow()
121+
{
122+
const string content = @"
123+
namespace X {
124+
public class Y {
125+
public void Run() { }
126+
}
127+
}";
128+
var file = _tempFiles.CreateFileWithContent("File1.cs", content);
129+
var analyser = new CognitiveCodeAnalyser();
130+
131+
var metrics = await analyser.AnalyseFilesAsync([file], _configuration);
132+
133+
Assert.That(metrics, Has.Count.EqualTo(1));
134+
}
135+
136+
[Test]
137+
public void AnalyseSourceFiles_PassesProgressToAnalyser()
138+
{
139+
const string content = @"
140+
namespace X {
141+
public class Y {
142+
public void Run() { }
143+
}
144+
}";
145+
_tempFiles.CreateFileWithContent("File1.cs", content);
146+
_tempFiles.CreateFileWithContent("File2.cs", content);
147+
148+
var collector = new AnalysisProgressCollector();
149+
var facade = new CognitiveAnalysisFacade(
150+
new SourceFileFinder(),
151+
new CognitiveCodeAnalyser(),
152+
_configuration,
153+
new ScoreCalculator(),
154+
new CoberturaReader(),
155+
new ClassCouplingAnalyser()
156+
);
157+
158+
var files = facade.FindSourceFiles(_tempFiles.tmpDirectory, collector);
159+
facade.AnalyseSourceFiles(files, _configuration, collector);
160+
161+
var reports = collector.Reports;
162+
using (Assert.EnterMultipleScope())
163+
{
164+
Assert.That(reports.Any(r => r.Phase == AnalysisProgressPhase.SearchingFiles), Is.True);
165+
Assert.That(reports.Any(r => r.Phase == AnalysisProgressPhase.SearchCompleted && r.TotalFiles == 2), Is.True);
166+
Assert.That(reports.Any(r => r.Phase == AnalysisProgressPhase.AnalysingFiles), Is.True);
167+
Assert.That(reports.Any(r => r.Phase == AnalysisProgressPhase.AnalysisCompleted), Is.True);
168+
}
169+
}
170+
}

CognitiveCodeAnalysis.sln

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 18
4-
VisualStudioVersion = 18.2.11415.280 d18.0
4+
VisualStudioVersion = 18.2.11415.280
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitiveCodeAnalysis", "CognitiveCodeAnalysis\CognitiveCodeAnalysis.csproj", "{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}"
77
EndProject
@@ -12,24 +12,66 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitiveCodeAnalysis.Tests
1212
EndProject
1313
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitiveCodeAnalysisConsoleApp", "CognitiveCodeAnalysisConsoleApp\CognitiveCodeAnalysisConsoleApp.csproj", "{7C9900F4-B313-27DD-0145-199FF8CA1F89}"
1414
EndProject
15+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CognitiveCodeAnalysisConsoleApp.Tests", "CognitiveCodeAnalysisConsoleApp.Tests\CognitiveCodeAnalysisConsoleApp.Tests.csproj", "{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}"
16+
EndProject
1517
Global
1618
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1719
Debug|Any CPU = Debug|Any CPU
20+
Debug|x64 = Debug|x64
21+
Debug|x86 = Debug|x86
1822
Release|Any CPU = Release|Any CPU
23+
Release|x64 = Release|x64
24+
Release|x86 = Release|x86
1925
EndGlobalSection
2026
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2127
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2228
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Debug|x64.ActiveCfg = Debug|Any CPU
30+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Debug|x64.Build.0 = Debug|Any CPU
31+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Debug|x86.ActiveCfg = Debug|Any CPU
32+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Debug|x86.Build.0 = Debug|Any CPU
2333
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
2434
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Release|Any CPU.Build.0 = Release|Any CPU
35+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Release|x64.ActiveCfg = Release|Any CPU
36+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Release|x64.Build.0 = Release|Any CPU
37+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Release|x86.ActiveCfg = Release|Any CPU
38+
{10A15CBC-0DB2-AE9B-F1C3-E49DFD248BAE}.Release|x86.Build.0 = Release|Any CPU
2539
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2640
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Debug|Any CPU.Build.0 = Debug|Any CPU
41+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Debug|x64.ActiveCfg = Debug|Any CPU
42+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Debug|x64.Build.0 = Debug|Any CPU
43+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Debug|x86.ActiveCfg = Debug|Any CPU
44+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Debug|x86.Build.0 = Debug|Any CPU
2745
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Release|Any CPU.ActiveCfg = Release|Any CPU
2846
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Release|x64.ActiveCfg = Release|Any CPU
48+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Release|x64.Build.0 = Release|Any CPU
49+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Release|x86.ActiveCfg = Release|Any CPU
50+
{97C3EC4D-220C-E3E8-AD0B-FA046CFAB33A}.Release|x86.Build.0 = Release|Any CPU
2951
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3052
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Debug|Any CPU.Build.0 = Debug|Any CPU
53+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Debug|x64.ActiveCfg = Debug|Any CPU
54+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Debug|x64.Build.0 = Debug|Any CPU
55+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Debug|x86.ActiveCfg = Debug|Any CPU
56+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Debug|x86.Build.0 = Debug|Any CPU
3157
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Release|Any CPU.ActiveCfg = Release|Any CPU
3258
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Release|Any CPU.Build.0 = Release|Any CPU
59+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Release|x64.ActiveCfg = Release|Any CPU
60+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Release|x64.Build.0 = Release|Any CPU
61+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Release|x86.ActiveCfg = Release|Any CPU
62+
{7C9900F4-B313-27DD-0145-199FF8CA1F89}.Release|x86.Build.0 = Release|Any CPU
63+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
64+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Debug|Any CPU.Build.0 = Debug|Any CPU
65+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Debug|x64.ActiveCfg = Debug|Any CPU
66+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Debug|x64.Build.0 = Debug|Any CPU
67+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Debug|x86.ActiveCfg = Debug|Any CPU
68+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Debug|x86.Build.0 = Debug|Any CPU
69+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Release|Any CPU.ActiveCfg = Release|Any CPU
70+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Release|Any CPU.Build.0 = Release|Any CPU
71+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Release|x64.ActiveCfg = Release|Any CPU
72+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Release|x64.Build.0 = Release|Any CPU
73+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Release|x86.ActiveCfg = Release|Any CPU
74+
{8EB9755A-AE7D-4120-9473-B8FD8A1F9D30}.Release|x86.Build.0 = Release|Any CPU
3375
EndGlobalSection
3476
GlobalSection(SolutionProperties) = preSolution
3577
HideSolutionNode = FALSE

CognitiveCodeAnalysis/CognitiveCodeAnalysis.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
1717
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
1818
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
19-
<PackageReference Include="Spectre.Console" Version="0.54.0" />
20-
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
2119
<PackageReference Include="System.Text.Json" Version="8.0.6" />
2220
</ItemGroup>
2321

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <copyright company="Florian Krämer">
2+
/// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
/// </copyright>
4+
5+
namespace CognitiveCodeAnalysis.CognitiveAnalysis;
6+
7+
public enum AnalysisProgressPhase
8+
{
9+
SearchingFiles,
10+
SearchCompleted,
11+
AnalysingFiles,
12+
AnalysisCompleted
13+
}
14+
15+
public readonly record struct AnalysisProgress(
16+
AnalysisProgressPhase Phase,
17+
int TotalFiles = 0,
18+
int ProcessedFiles = 0
19+
);

CognitiveCodeAnalysis/src/CognitiveAnalysis/CognitiveAnalysisFacade.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,32 @@ public class CognitiveAnalysisFacade(
1717
ClassCouplingAnalyser classCouplingAnalyser
1818
) {
1919
public List<string> FindSourceFiles(string[] sourcePaths)
20-
{
21-
return sourceFileFinder.FindSourceFiles(sourcePaths);
22-
}
20+
=> sourceFileFinder.FindSourceFiles(sourcePaths);
21+
22+
public List<string> FindSourceFiles(string[] sourcePaths, IProgress<AnalysisProgress>? progress)
23+
=> sourceFileFinder.FindSourceFiles(sourcePaths, progress);
2324

2425
public List<string> FindSourceFiles(string sourcePath)
25-
{
26-
return sourceFileFinder.FindSourceFiles([sourcePath]);
27-
}
26+
=> sourceFileFinder.FindSourceFiles([sourcePath]);
27+
28+
public List<string> FindSourceFiles(string sourcePath, IProgress<AnalysisProgress>? progress)
29+
=> sourceFileFinder.FindSourceFiles([sourcePath], progress);
2830

2931
public CognitiveMetricsCollection AnalyseSourceFiles(List<string> files)
3032
=> AnalyseSourceFiles(files, cognitiveConfiguration);
3133

3234
public CognitiveMetricsCollection AnalyseSourceFiles(
3335
List<string> files,
3436
CognitiveConfiguration configuration
37+
) => AnalyseSourceFiles(files, configuration, progress: null);
38+
39+
public CognitiveMetricsCollection AnalyseSourceFiles(
40+
List<string> files,
41+
CognitiveConfiguration configuration,
42+
IProgress<AnalysisProgress>? progress
3543
) {
3644
CognitiveMetricsCollection metricsCollection = analyser
37-
.AnalyseFilesAsync(files, configuration)
45+
.AnalyseFilesAsync(files, configuration, progress)
3846
.GetAwaiter()
3947
.GetResult();
4048

0 commit comments

Comments
 (0)