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
238 changes: 238 additions & 0 deletions src/ArchlensTests/Application/UpdateGraphUseCaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using Archlens.Application;
using Archlens.Domain;
using Archlens.Domain.Interfaces;
using Archlens.Domain.Models;
using Archlens.Domain.Models.Enums;
using Archlens.Domain.Models.Records;
using ArchlensTests.Utils;

namespace ArchlensTests.Application;

public sealed class UpdateGraphUseCaseTests : IDisposable
{
private readonly TestFileSystem _fs = new();

public void Dispose() => _fs.Dispose();

private BaseOptions MakeBaseOptions() => new(
FullRootPath: _fs.Root,
ProjectRoot: _fs.Root,
ProjectName: "TestProject"
);

private ParserOptions MakeParserOptions() => new(
BaseOptions: MakeBaseOptions(),
Languages: [],
Exclusions: [],
FileExtensions: [".cs"]
);

private RenderOptions MakeRenderOptions(RenderFormat format) => new(
BaseOptions: MakeBaseOptions(),
Format: format,
Views: [new View("overview", [], [])],
SaveLocation: Path.Combine(_fs.Root, "diagrams")
);

private SnapshotOptions MakeSnapshotOptions() => new(
BaseOptions: MakeBaseOptions(),
SnapshotManager: SnapshotManager.Local,
GitInfo: new GitInfo("", "main")
);

private sealed class NullParser : IDependencyParser
{
public Task<IReadOnlyList<RelativePath>> ParseFileDependencies(string absPath, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<RelativePath>>([]);
}

private sealed class RendererSpy : RendererBase
{
public int RenderCalled { get; private set; }
public override string FileExtension => "json";

protected override string Render(RenderGraph graph, View view, RenderOptions options)
{
RenderCalled++;
return "{}";
}
}

private sealed class StubSnapshotManager(ProjectDependencyGraph? snapshot) : ISnapshotManager
{
public int SaveCalled { get; private set; }

public Task<ProjectDependencyGraph?> GetLastSavedDependencyGraphAsync(
SnapshotOptions options, CancellationToken ct = default)
=> Task.FromResult(snapshot);

public Task SaveGraphAsync(ProjectDependencyGraph graph, SnapshotOptions options, CancellationToken ct = default)
{
SaveCalled++;
return Task.CompletedTask;
}
}

private UpdateGraphUseCase MakeUseCase(
RenderFormat format,
ISnapshotManager snapshotManager,
RendererBase renderer,
bool diff = false) => new(
baseOptions: MakeBaseOptions(),
parserOptions: MakeParserOptions(),
renderOptions: MakeRenderOptions(format),
snapshotOptions: MakeSnapshotOptions(),
parsers: [new NullParser()],
renderer: renderer,
snapshotManager: snapshotManager,
diff: diff
);

private static ProjectDependencyGraph MakeEmptyGraph(string root)
{
var g = new ProjectDependencyGraph(root);
g.UpsertProjectItem(RelativePath.Directory(root, "./"), ProjectItemType.Directory);
return g;
}

private string SavedFilePath(bool diff = false)
{
var diffPart = diff ? "-diff" : "";
return Path.Combine(_fs.Root, "diagrams", $"TestProject{diffPart}-overview.json");
}

[Fact]
public async Task RunAsync_WhenFormatIsNone_DoesNotCallRenderer()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.None, stub, spy);
await sut.RunAsync();

Assert.Equal(0, spy.RenderCalled);
}

[Fact]
public async Task RunAsync_WhenFormatIsNone_StillSavesSnapshot()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.None, stub, spy);
await sut.RunAsync();

Assert.Equal(1, stub.SaveCalled);
}

[Fact]
public async Task RunAsync_WhenFormatIsNone_NoOutputFilesAreCreated()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.None, stub, spy);
await sut.RunAsync();

Assert.False(File.Exists(SavedFilePath()), "Expected no output file when format is None.");
}

[Fact]
public async Task RunAsync_WhenFormatIs_NotNoneAndNotDiff_CallsRenderViews()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.Json, stub, spy);
await sut.RunAsync();

Assert.Equal(1, spy.RenderCalled);
}

[Fact]
public async Task RunAsync_WhenFormatIs_NotNoneAndNotDiff_CreatesOutputFile()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.Json, stub, spy);
await sut.RunAsync();

Assert.True(File.Exists(SavedFilePath()), "Expected output file to be written.");
Assert.False(File.Exists(SavedFilePath(diff: true)), "Expected no diff output file.");
}

[Fact]
public async Task RunAsync_WhenFormatIs_NotNoneAndNotDiff_StillSavesSnapshot()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.Json, stub, spy);
await sut.RunAsync();

Assert.Equal(1, stub.SaveCalled);
}

[Fact]
public async Task RunAsync_WhenDiffMode_AndSnapshotExists_CreatesDiffOutputFile()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var snapshot = MakeEmptyGraph(_fs.Root);
var stub = new StubSnapshotManager(snapshot);

var sut = MakeUseCase(RenderFormat.Json, stub, spy, diff: true);
await sut.RunAsync();

Assert.True(File.Exists(SavedFilePath(diff: true)), "Expected diff output file to be written.");
Assert.False(File.Exists(SavedFilePath(diff: false)), "Expected no regular output file in diff mode.");
}

[Fact]
public async Task RunAsync_WhenDiffMode_AndSnapshotExists_StillSavesSnapshot()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var snapshot = MakeEmptyGraph(_fs.Root);
var stub = new StubSnapshotManager(snapshot);

var sut = MakeUseCase(RenderFormat.Json, stub, spy, diff: true);
await sut.RunAsync();

Assert.Equal(1, stub.SaveCalled);
}

[Fact]
public async Task RunAsync_WhenDiffMode_AndNoSnapshot_ThrowsInvalidOperationException()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

var sut = MakeUseCase(RenderFormat.Json, stub, spy, diff: true);

await Assert.ThrowsAsync<InvalidOperationException>(() => sut.RunAsync());
}

[Fact]
public async Task RunAsync_PropagatesCancellation()
{
_fs.File("src/A.cs", "class A {}");
var spy = new RendererSpy();
var stub = new StubSnapshotManager(null);

using var cts = new CancellationTokenSource();
cts.Cancel();

var sut = MakeUseCase(RenderFormat.None, stub, spy);

await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sut.RunAsync(cts.Token));
}
}
40 changes: 40 additions & 0 deletions src/ArchlensTests/Infra/Renderers/NoneRendererTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Archlens.Domain.Models;
using Archlens.Domain.Models.Enums;
using Archlens.Domain.Models.Records;
using Archlens.Infra.Renderers;
using ArchlensTests.Utils;

namespace ArchlensTests.Infra.Renderers;

public sealed class NoneRendererTests : IDisposable
{
private readonly TestFileSystem _fs = new();
private readonly NoneRenderer _renderer = new();

public void Dispose() => _fs.Dispose();

private RenderOptions Opts(
string viewName = "testView",
IReadOnlyList<Package>? packages = null,
IReadOnlyList<string>? ignore = null) => new(
BaseOptions: new(
ProjectRoot: _fs.Root,
ProjectName: "Archlens",
FullRootPath: _fs.Root),
Format: RenderFormat.None,
Views: [new View(viewName, packages ?? [], ignore ?? [])],
SaveLocation: $"{_fs.Root}/diagrams");

private ProjectDependencyGraph DefaultGraph() =>
TestDependencyGraph.MakeDependencyGraph(_fs.Root);

private string Render(ProjectDependencyGraph graph, RenderOptions opts) =>
_renderer.RenderView(graph, opts.Views[0], opts);

[Fact]
public void Render_ReturnsEmptyString()
{
var result = Render(DefaultGraph(), Opts());
Assert.Empty(result);
}
}
14 changes: 9 additions & 5 deletions src/c-sharp/Application/UpdateGraphUseCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using Archlens.Domain;
using Archlens.Domain.Interfaces;
using Archlens.Domain.Models.Enums;
using Archlens.Domain.Models.Records;

namespace Archlens.Application;
Expand All @@ -25,13 +26,16 @@ public async Task RunAsync(CancellationToken ct = default)
var projectChanges = await ChangeDetector.GetProjectChangesAsync(parserOptions, snapshotGraph, ct);
var graph = await new DependencyGraphBuilder(parsers, baseOptions).GetGraphAsync(projectChanges, snapshotGraph, ct);

if (diff)
if (renderOptions.Format != RenderFormat.None)
{
var compareGraph = await snapshotManager.GetLastSavedDependencyGraphAsync(snapshotOptions, ct) ?? throw new InvalidOperationException("Diff mode requires a saved snapshot, but none was found.");
await renderer.RenderDiffViewsAndSaveToFiles(graph, compareGraph, renderOptions, ct);
if (diff)
{
var compareGraph = await snapshotManager.GetLastSavedDependencyGraphAsync(snapshotOptions, ct) ?? throw new InvalidOperationException("Diff mode requires a saved snapshot, but none was found.");
await renderer.RenderDiffViewsAndSaveToFiles(graph, compareGraph, renderOptions, ct);
}
else
await renderer.RenderViewsAndSaveToFiles(graph, renderOptions, ct);
}
else
await renderer.RenderViewsAndSaveToFiles(graph, renderOptions, ct);

await snapshotManager.SaveGraphAsync(graph, snapshotOptions, ct);
}
Expand Down
2 changes: 2 additions & 0 deletions src/c-sharp/Domain/Models/Enums/RenderFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace Archlens.Domain.Models.Enums;

public enum RenderFormat
{
None,
Json,
PlantUML
}
Expand All @@ -12,6 +13,7 @@ public static string ToFileExtension(this RenderFormat format)
{
return format switch
{
RenderFormat.None => "none",
RenderFormat.Json => "json",
RenderFormat.PlantUML => "puml",
_ => format.ToString(),
Expand Down
1 change: 1 addition & 0 deletions src/c-sharp/Infra/ConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ private static RenderFormat MapFormat(string raw)
var s = raw.Trim().ToLowerInvariant();
return s switch
{
"none" => RenderFormat.None,
"json" or "application/json" => RenderFormat.Json,
"puml" or "plantuml" or "plant-uml" => RenderFormat.PlantUML,
_ => throw new NotSupportedException($"Unsupported format: '{raw}'.")
Expand Down
1 change: 1 addition & 0 deletions src/c-sharp/Infra/Factories/RendererFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static class RendererFactory
{
public static RendererBase SelectRenderer(RenderOptions options) => options.Format switch
{
RenderFormat.None => new NoneRenderer(),
RenderFormat.Json => new JsonRenderer(),
RenderFormat.PlantUML => new PlantUMLRenderer(),
_ => throw new ArgumentOutOfRangeException(nameof(options))
Expand Down
16 changes: 16 additions & 0 deletions src/c-sharp/Infra/Renderers/NoneRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using Archlens.Domain;
using Archlens.Domain.Models.Records;

namespace Archlens.Infra.Renderers;

public sealed class NoneRenderer : RendererBase
{
public override string FileExtension => "";

protected override string Render(RenderGraph graph, View view, RenderOptions options)
{
Console.WriteLine("Info: Renderer is none - no output will be rendered.");
return "";
}
}
Loading