Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
65f724e
Upgrades all dependencies to latest versions and migrates to .NET 10
mscottford Apr 10, 2026
4b13967
Adds .idea to .gitignore
mscottford Apr 10, 2026
5d4e672
Refactors domain nodes and triples to use C# primary constructors
mscottford Apr 10, 2026
d933f7f
Fixes bug where projects added as transitive references were dropped …
mscottford Apr 10, 2026
b845213
Parallelizes MSBuild, Roslyn analysis, and Neo4j inserts across projects
mscottford Apr 10, 2026
4479043
Adds progress logging when loading projects into the Roslyn workspace
mscottford Apr 10, 2026
e8a4cba
Streams projects through Build and Load stages without waiting for al…
mscottford Apr 10, 2026
9691da9
Fixes concurrent build race when streaming Build into Load stage
mscottford Apr 10, 2026
1d90636
Caps concurrent MSBuild processes at the number of logical processors
mscottford Apr 10, 2026
03a7590
Increases concurrent Neo4j connections to match processor count
mscottford Apr 13, 2026
5029f42
Fixes non-deterministic node pks causing duplicate nodes across runs
mscottford Apr 13, 2026
defbbcc
Adds MSBuild binary log caching to skip rebuilds when nothing has cha…
mscottford Apr 13, 2026
396cf49
Fixes orphan project folder nodes and skips unsupported project types
mscottford Apr 13, 2026
c19e101
Adds Neo4j uniqueness constraints and indexes for upsert performance
mscottford Apr 13, 2026
5a26c23
Separates analysis pipeline from UI output via IAnalysisProgress
mscottford Apr 13, 2026
9206105
Fix null parent node crash in Extractor.GetInherits
mscottford Apr 15, 2026
b8b7dfa
Add CompositeAnalysisProgress to fan out to multiple implementations
mscottford Apr 15, 2026
033b2c1
Add FileAnalysisProgress for structured file-based run logging
mscottford Apr 15, 2026
a5b5769
Fix concurrent console output corruption in SpectreConsoleProgress
mscottford Apr 15, 2026
ec25656
Differentiate skip reason labels in console output
mscottford Apr 15, 2026
39279be
Add BuildLogDirectory and NoCache options to AnalyzerConfig
mscottford Apr 15, 2026
9c38574
Fix cache-hit path missing null check and stale deps sidecar
mscottford Apr 15, 2026
ec76909
Read build results from binlog instead of pipe, with timeout and retry
mscottford Apr 15, 2026
c4182df
Distinguish build log unreadable from build failure in skip reason
mscottford Apr 15, 2026
718a7b8
Write per-project MSBuild build logs to a configurable directory
mscottford Apr 15, 2026
24dfeec
Add --no-cache flag to force rebuild of all projects
mscottford Apr 15, 2026
9b5b266
Add --no-cache and --build-log-dir CLI options
mscottford Apr 15, 2026
a843b19
Pass NoCache and BuildLogDirectory from config into build pipeline
mscottford Apr 15, 2026
0b89a06
Wire FileAnalysisProgress and CompositeAnalysisProgress into run loop
mscottford Apr 15, 2026
d2a6ce9
Add --neo4j-url CLI option to configure the Neo4j connection URL
mscottford Apr 15, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.vs
.vscode
.idea
bin
obj
.DS_Store
app
*.DotSettings.user
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS sdk
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS sdk
RUN mkdir -p /src/Stahz
WORKDIR /src
COPY Strazh/Strazh.csproj Strazh/Strazh.csproj
Expand Down
139 changes: 139 additions & 0 deletions Strazh.Tests/AnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Buildalyzer;
using Strazh.Analysis;
using Xunit;

namespace Strazh.Tests;

public class AnalyzerTests
{
/// <summary>
/// Regression test for the bug where projects pulled into the Roslyn workspace as transitive
/// references by an earlier project's AddToWorkspace(workspace, addProjectReferences: true)
/// call were subsequently skipped by the deduplication check and never entered
/// context.Projects — causing them to receive no CONTAINS triple and no analysis.
///
/// The fixture solution (SystemUnderTest.sln) lists ProjectA before ProjectB.
/// ProjectA references ProjectB, so when ProjectA is added to the workspace with
/// addProjectReferences: true, Buildalyzer pulls ProjectB in as a reference. Without the
/// fix, ProjectB's loop iteration finds it already in the workspace and is dropped.
/// </summary>
[Fact]
public async Task GetAnalysisContext_IncludesProjectsAddedAsTransitiveReferences()
{
var solutionPath = Path.Combine(GetRepoRoot(), "SystemUnderTest", "SystemUnderTest.sln");
var manager = new AnalyzerManager(solutionPath);

var context = await Analyzer.GetAnalysisContext(manager);

var projectFileNames = context.Projects
.Select(p => Path.GetFileName(p.Item2.ProjectFilePath))
.ToList();

Assert.Contains("Strazh.Tests.ProjectA.csproj", projectFileNames);
Assert.Contains("Strazh.Tests.ProjectB.csproj", projectFileNames);
}

/// <summary>
/// Verifies that the first run with a cache directory (cache miss, binlog written) produces
/// the same results as a completely uncached build.
/// </summary>
[Fact]
public async Task GetAnalysisContext_FirstCachedRun_YieldsSameResultsAsUncachedBuild()
{
var solutionPath = Path.Combine(GetRepoRoot(), "SystemUnderTest", "SystemUnderTest.sln");
var cacheDir = Path.Combine(Path.GetTempPath(), $"strazh-test-{Guid.NewGuid():N}");
try
{
var cachedContext = await Analyzer.GetAnalysisContext(
new AnalyzerManager(solutionPath), cacheDir);

var uncachedContext = await Analyzer.GetAnalysisContext(
new AnalyzerManager(solutionPath));

AssertAnalysisContextsAreEquivalent(uncachedContext, cachedContext);
}
finally
{
if (Directory.Exists(cacheDir))
{
Directory.Delete(cacheDir, recursive: true);
}
}
}

/// <summary>
/// Verifies that replaying a populated cache (cache hit, binlog replayed) produces the same
/// results as a completely uncached build.
/// </summary>
[Fact]
public async Task GetAnalysisContext_CacheHitRun_YieldsSameResultsAsUncachedBuild()
{
var solutionPath = Path.Combine(GetRepoRoot(), "SystemUnderTest", "SystemUnderTest.sln");
var cacheDir = Path.Combine(Path.GetTempPath(), $"strazh-test-{Guid.NewGuid():N}");
try
{
// Populate the cache
await Analyzer.GetAnalysisContext(new AnalyzerManager(solutionPath), cacheDir);

// Replay from cache
var cachedContext = await Analyzer.GetAnalysisContext(
new AnalyzerManager(solutionPath), cacheDir);

var uncachedContext = await Analyzer.GetAnalysisContext(
new AnalyzerManager(solutionPath));

AssertAnalysisContextsAreEquivalent(uncachedContext, cachedContext);
}
finally
{
if (Directory.Exists(cacheDir))
{
Directory.Delete(cacheDir, recursive: true);
}
}
}

private static void AssertAnalysisContextsAreEquivalent(
Analyzer.AnalysisContext expected, Analyzer.AnalysisContext actual)
{
var expectedResults = expected.Projects
.Select(p => p.Item2)
.OrderBy(r => r.ProjectFilePath)
.ToList();

var actualResults = actual.Projects
.Select(p => p.Item2)
.OrderBy(r => r.ProjectFilePath)
.ToList();

Assert.Equal(expectedResults.Count, actualResults.Count);

for (var i = 0; i < expectedResults.Count; i++)
{
var exp = expectedResults[i];
var act = actualResults[i];

Assert.Equal(exp.ProjectFilePath, act.ProjectFilePath);
Assert.Equal(exp.Succeeded, act.Succeeded);
Assert.Equal(
exp.ProjectReferences.OrderBy(x => x).ToList(),
act.ProjectReferences.OrderBy(x => x).ToList());
Assert.Equal(
exp.SourceFiles.OrderBy(x => x).ToList(),
act.SourceFiles.OrderBy(x => x).ToList());
Assert.Equal(
exp.PackageReferences.Keys.OrderBy(x => x).ToList(),
act.PackageReferences.Keys.OrderBy(x => x).ToList());
}
}

// [CallerFilePath] gives the compile-time absolute path of this source file.
// From Strazh.Tests/AnalyzerTests.cs, two levels up reaches the repo root.
private static string GetRepoRoot([CallerFilePath] string callerFile = "") =>
Path.GetFullPath(Path.Combine(callerFile, "../.."));
}
22 changes: 22 additions & 0 deletions Strazh.Tests/Strazh.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Strazh\Strazh.csproj" />
</ItemGroup>

</Project>
Loading