Skip to content

Commit a487b76

Browse files
committed
Write logs + status on stderr and content on stdout
1 parent baa2aaf commit a487b76

File tree

6 files changed

+61
-98
lines changed

6 files changed

+61
-98
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
* Fixed an issue where a project reference (blue) could be wrongly identified as package reference (green)
1010
* Removed the `--no-browser` option, replaced with the `-u|--url` option
11+
* Logs and status are now written to stderr. Only actual content is written to stdout, i.e.
12+
* The graph URL when `--url print` is used
13+
* The output of the `--help` and `--version` options
1114

1215
## [0.3.0][0.3.0] - 2025-06-24
1316

src/nugraph/GraphCommand.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ internal sealed partial class FileOrPackage : OneOfBase<FileSystemInfo, PackageI
2929
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "Instantiated by Spectre.Console.Cli through reflection")]
3030
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli through reflection")]
3131
[Description("Generates dependency graphs for .NET projects and NuGet packages.")]
32-
internal sealed class GraphCommand(IAnsiConsole console, DirectoryInfo currentWorkingDirectory, TextWriter stdOut, CancellationToken cancellationToken) : CancelableCommand<GraphCommandSettings>(cancellationToken)
32+
internal sealed class GraphCommand(ProgramEnvironment environment, CancellationToken cancellationToken) : CancelableCommand<GraphCommandSettings>(cancellationToken)
3333
{
3434
protected override async Task<int> ExecuteAsync(CommandContext commandContext, GraphCommandSettings settings, CancellationToken cancellationToken)
3535
{
36+
var stdOut = environment.StdOut;
37+
var console = environment.ConsoleErr;
38+
3639
if (settings.Diagnose)
3740
{
38-
return await DiagnoseAsync(settings.Sdk, cancellationToken);
41+
return await DiagnoseAsync(stdOut, settings.Sdk, cancellationToken);
3942
}
4043

41-
var source = settings.Source ?? currentWorkingDirectory;
44+
var source = settings.Source ?? environment.CurrentWorkingDirectory;
4245
var graphUrl = await console.Status().StartAsync($"Generating dependency graph for {source}".EscapeMarkup(), async context =>
4346
{
4447
var graph = await source.Match(
@@ -71,7 +74,7 @@ protected override async Task<int> ExecuteAsync(CommandContext commandContext, G
7174
return 0;
7275
}
7376

74-
private async Task<int> DiagnoseAsync(DirectoryInfo? sdk, CancellationToken cancellationToken)
77+
private static async Task<int> DiagnoseAsync(TextWriter stdOut, DirectoryInfo? sdk, CancellationToken cancellationToken)
7578
{
7679
await stdOut.WriteLineAsync("nugraph:");
7780
await stdOut.WriteLineAsync($" Version: {typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "N/A"}");

src/nugraph/Program.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
namespace nugraph;
1111

12-
public class Program(DirectoryInfo currentWorkingDirectory, IAnsiConsole consoleOut, IAnsiConsole consoleErr, TextWriter stdOut)
12+
public record ProgramEnvironment(DirectoryInfo CurrentWorkingDirectory, IAnsiConsole ConsoleOut, IAnsiConsole ConsoleErr, TextWriter StdOut);
13+
14+
public class Program(ProgramEnvironment environment)
1315
{
14-
public Program() : this(new DirectoryInfo(Environment.CurrentDirectory), RedirectionFriendlyConsole.Out, RedirectionFriendlyConsole.Error, Console.Out)
16+
public Program() : this(new ProgramEnvironment(new DirectoryInfo(Environment.CurrentDirectory), RedirectionFriendlyConsole.Out, RedirectionFriendlyConsole.Error, Console.Out))
1517
{
1618
}
1719

@@ -48,12 +50,12 @@ public async Task<int> RunAsync(string[] args)
4850
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "N/A";
4951
config.SetApplicationName(OperatingSystem.IsWindows() ? "nugraph.exe" : "nugraph");
5052
config.SetApplicationVersion(SemanticVersion.TryParse(version, out var semanticVersion) ? semanticVersion.ToNormalizedString() : version);
51-
config.ConfigureConsole(consoleOut);
52-
config.Settings.Registrar.RegisterInstance(currentWorkingDirectory);
53-
config.Settings.Registrar.RegisterInstance(stdOut);
53+
config.ConfigureConsole(environment.ConsoleOut);
54+
config.Settings.Registrar.RegisterInstance(environment);
5455
config.Settings.Registrar.RegisterInstance(cancellationTokenSource.Token);
5556
config.SetExceptionHandler((exception, _) =>
5657
{
58+
var consoleErr = environment.ConsoleErr;
5759
switch (exception)
5860
{
5961
case OperationCanceledException when cancellationTokenSource.IsCancellationRequested:

tests/nugraph.Tests/NugraphAssertions.cs

Lines changed: 18 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,19 @@ public class NugraphAssertions(NugraphResult instance, AssertionChain chain) :
2727
protected override string Identifier => "result";
2828

2929
/// <summary>
30-
/// Asserts that the nugraph result is successful.
31-
/// <list type="bullet">
32-
/// <item>Ensures that exit code is 0.</item>
33-
/// <item>Ensures that stderr is empty.</item>
34-
/// <item>Ensures that stdout matches the provided pattern.</item>
35-
/// </list>
30+
/// Asserts that the nugraph result matches the exit code, stdout and stderr content.
3631
/// </summary>
37-
/// <param name="stdOutPattern">The expected pattern that must be written on stdout.</param>
32+
/// <param name="exitCode">The expected exit code.</param>
33+
/// <param name="stdOutPattern">The expected pattern that must be written on stdout or an empty string to assert that nothing was written.</param>
34+
/// <param name="stdErrPattern">The expected pattern that must be written on stderr or an empty string to assert that nothing was written.</param>
3835
/// <param name="because">
3936
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
4037
/// </param>
4138
/// <param name="becauseArgs">
4239
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
4340
/// </param>
4441
[CustomAssertion]
45-
public AndConstraint<NugraphAssertions> Succeed(string stdOutPattern, [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
42+
public AndConstraint<NugraphAssertions> Match(int exitCode = 0, string stdOutPattern = "", string stdErrPattern = "", [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
4643
{
4744
string[] failures = [];
4845

@@ -51,9 +48,19 @@ public AndConstraint<NugraphAssertions> Succeed(string stdOutPattern, [StringSyn
5148
.ForCondition(result =>
5249
{
5350
using var scope = new AssertionScope();
54-
result.ExitCode.Should().Be(0, because, becauseArgs);
55-
result.StdErr.Should().BeEmpty(because, becauseArgs);
56-
result.StdOut.Should().MatchEquivalentOf(stdOutPattern, opt => opt.IgnoringNewlineStyle(), because, becauseArgs);
51+
52+
result.ExitCode.Should().Be(exitCode, because, becauseArgs);
53+
54+
if (string.IsNullOrEmpty(stdOutPattern))
55+
result.StdOut.Should().BeEmpty(because, becauseArgs);
56+
else
57+
result.StdOut.Should().MatchEquivalentOf(stdOutPattern, opt => opt.IgnoringNewlineStyle(), because, becauseArgs);
58+
59+
if (string.IsNullOrEmpty(stdErrPattern))
60+
result.StdErr.Should().BeEmpty(because, becauseArgs);
61+
else
62+
result.StdErr.Should().MatchEquivalentOf(stdErrPattern, opt => opt.IgnoringNewlineStyle(), because, becauseArgs);
63+
5764
failures = scope.Discard();
5865

5966
return failures.Length == 0;
@@ -100,40 +107,4 @@ public AndConstraint<NugraphAssertions> UrlHasDiagram(string expectedDiagram, [S
100107

101108
return new AndConstraint<NugraphAssertions>(this);
102109
}
103-
104-
/// <summary>
105-
/// Asserts that the nugraph result is a failure.
106-
/// <list type="bullet">
107-
/// <item>Ensures that exit code is <paramref name="failCode"/>.</item>
108-
/// <item>Ensures that stderr matches the provided pattern.</item>
109-
/// </list>
110-
/// </summary>
111-
/// <param name="failCode">The expected exit code.</param>
112-
/// <param name="stdErrPattern">The expected pattern that must be written on stderr.</param>
113-
/// <param name="because">
114-
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
115-
/// </param>
116-
/// <param name="becauseArgs">
117-
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
118-
/// </param>
119-
[CustomAssertion]
120-
public AndConstraint<NugraphAssertions> Fail(int failCode, string stdErrPattern, string because = "", params object[] becauseArgs)
121-
{
122-
string[] failures = [];
123-
124-
_chain
125-
.Given(() => Subject)
126-
.ForCondition(result =>
127-
{
128-
using var scope = new AssertionScope();
129-
result.ExitCode.Should().Be(failCode, because, becauseArgs);
130-
result.StdErr.Should().MatchEquivalentOf(stdErrPattern, opt => opt.IgnoringNewlineStyle(), because, becauseArgs);
131-
failures = scope.Discard();
132-
133-
return failures.Length == 0;
134-
})
135-
.FailWith(string.Join(Environment.NewLine, failures.Select(e => e.Replace("{", "{{").Replace("}", "}}"))));
136-
137-
return new AndConstraint<NugraphAssertions>(this);
138-
}
139110
}

tests/nugraph.Tests/NugraphProgram.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public override async Task<NugraphResult> RunAsync(string[] arguments, string? w
2424
using var consoleErr = new TestConsole();
2525
consoleErr.Profile.Width = 256;
2626
await using var stdOut = new StringWriter();
27-
var program = new Program(new DirectoryInfo(workingDirectory ?? Environment.CurrentDirectory), consoleOut, consoleErr, stdOut);
27+
var program = new Program(new ProgramEnvironment(new DirectoryInfo(workingDirectory ?? Environment.CurrentDirectory), consoleOut, consoleErr, stdOut));
2828
var args = arguments.Append("--log").Append(logLevel.ToString()).Append("--url").Append(action.ToString()).ToArray();
2929
var exitCode = await program.RunAsync(args);
3030
return new NugraphResult(exitCode, GetOutput(consoleOut, stdOut), GetOutput(consoleErr, null));

tests/nugraph.Tests/NugraphTests.cs

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System.IO;
22
using System.Threading.Tasks;
3-
using AwesomeAssertions;
4-
using AwesomeAssertions.Execution;
53
using NuGet.Common;
64

75
namespace nugraph.Tests;
@@ -15,34 +13,33 @@ public async Task Diagnose()
1513

1614
await File.WriteAllTextAsync($"{nugraph.GetType().Name}.diagnostics.txt", result.StdOut);
1715

18-
result.Should().Succeed("nugraph:*");
16+
result.Should().Match(stdOutPattern: "nugraph:*");
1917
}
2018

2119
[Test]
2220
public async Task Version()
2321
{
2422
var result = await nugraph.RunAsync(["--version"]);
2523

26-
result.Should().Succeed(nugraph.Version);
24+
result.Should().Match(stdOutPattern: nugraph.Version);
2725
}
2826

2927
[Test]
3028
public async Task Help()
3129
{
3230
var result = await nugraph.RunAsync(["--help"]);
3331

34-
result.Should().Succeed("*nugraph*[SOURCE]*");
32+
result.Should().Match(stdOutPattern: "*nugraph*[SOURCE]*");
3533
}
3634

3735
[Test]
3836
public async Task Package_Serilog()
3937
{
4038
var result = await nugraph.RunAsync(["Serilog"]);
4139

42-
result.Should().Succeed("""
40+
result.Should().Match(stdOutPattern:"https://mermaid.live/view#pako:*", stdErrPattern: """
4341
Generating dependency graph for Serilog
4442
Generating dependency graph for Serilog *
45-
https://mermaid.live/view#pako:*
4643
""");
4744
}
4845

@@ -51,10 +48,9 @@ public async Task Package_Serilog_430_net60()
5148
{
5249
var result = await nugraph.RunAsync(["Serilog/4.3.0", "--framework", "net6.0"]);
5350

54-
result.Should().Succeed("""
51+
result.Should().Match(stdOutPattern: "https://mermaid.live/view#pako:*", stdErrPattern: """
5552
Generating dependency graph for Serilog 4.3.0
5653
Generating dependency graph for Serilog 4.3.0 (net6.0)
57-
https://mermaid.live/view#pako:*
5854
""")
5955
.And.UrlHasDiagram("""
6056
---
@@ -81,10 +77,9 @@ public async Task Package_DockerRunner_MermaidSvg()
8177
{
8278
var result = await nugraph.RunAsync(["DockerRunner", "--format", "mmd.svg"]);
8379

84-
result.Should().Succeed("""
80+
result.Should().Match(stdOutPattern:"https://mermaid.ink/svg/pako:*", stdErrPattern: """
8581
Generating dependency graph for DockerRunner
8682
Generating dependency graph for DockerRunner 1.0.0-beta.2 (netstandard2.0)
87-
https://mermaid.ink/svg/pako:*
8883
""");
8984
}
9085

@@ -93,10 +88,9 @@ public async Task Package_DockerRunner_GraphvizSvg()
9388
{
9489
var result = await nugraph.RunAsync(["DockerRunner", "--format", "dot.svg"]);
9590

96-
result.Should().Succeed("""
91+
result.Should().Match(stdOutPattern:"https://kroki.io/graphviz/svg/*", stdErrPattern: """
9792
Generating dependency graph for DockerRunner
9893
Generating dependency graph for DockerRunner 1.0.0-beta.2 (netstandard2.0)
99-
https://kroki.io/graphviz/svg/*
10094
""");
10195
}
10296

@@ -105,47 +99,35 @@ public async Task Package_DoesNotExist()
10599
{
106100
var result = await nugraph.RunAsync(["DoesNotExist"], logLevel: LogLevel.Debug);
107101

108-
using (new AssertionScope())
109-
{
110-
result.Should().Fail(66, "Package DoesNotExist was not found*nuget.org [https://api.nuget.org/v3/index.json]*");
111-
result.StdOut.Should().ContainAll(
112-
"Retrieving DependencyInfoResource for nuget.org",
113-
"Resolving DoesNotExist with NuGet.Protocol.DependencyInfoResourceV3",
114-
"Generating dependency graph for DoesNotExist");
115-
}
102+
result.Should().Match(66, stdErrPattern: """
103+
*Retrieving DependencyInfoResource for nuget.org*
104+
*Resolving DoesNotExist with NuGet.Protocol.DependencyInfoResourceV3*
105+
*Package DoesNotExist was not found*nuget.org [https://api.nuget.org/v3/index.json]*
106+
""");
116107
}
117108

118109
[Test]
119110
public async Task Project_nugraph_WorkingDirectory()
120111
{
121112
var result = await nugraph.RunAsync(["-m", "gv"], workingDirectory: RepositoryDirectories.GetPath("src", "nugraph"));
122113

123-
result.Should().Succeed("""
124-
Generating dependency graph for nugraph
125-
https://edotor.net/#deflate:*
126-
""");
114+
result.Should().Match(stdOutPattern: "https://edotor.net/#deflate:*", stdErrPattern: "Generating dependency graph for nugraph");
127115
}
128116

129117
[Test]
130118
public async Task Project_nugraph_ExplicitDirectory()
131119
{
132120
var result = await nugraph.RunAsync([RepositoryDirectories.GetPath("src", "nugraph"), "-m", "graphviz"]);
133121

134-
result.Should().Succeed("""
135-
Generating dependency graph for nugraph
136-
https://edotor.net/#deflate:*
137-
""");
122+
result.Should().Match(stdOutPattern: "https://edotor.net/#deflate:*", stdErrPattern: "Generating dependency graph for nugraph");
138123
}
139124

140125
[Test]
141126
public async Task Project_mmd_ProjectFile()
142127
{
143128
var result = await nugraph.RunAsync([RepositoryDirectories.GetPath("tools", "mmd", "mmd.csproj"), "-m", "dot"]);
144129

145-
result.Should().Succeed("""
146-
Generating dependency graph for mmd.csproj
147-
https://edotor.net/#deflate:*
148-
""")
130+
result.Should().Match(stdOutPattern: "https://edotor.net/#deflate:*", stdErrPattern: "Generating dependency graph for mmd.csproj")
149131
.And.UrlHasDiagram("""
150132
# Generated by https://github.com/0xced/nugraph
151133
@@ -169,20 +151,22 @@ public async Task Project_SolutionFile()
169151
{
170152
var result = await nugraph.RunAsync([], workingDirectory: RepositoryDirectories.GetPath());
171153

172-
result.Should().Fail(65, """
173-
Solution files are not supported.
174-
Please run nugraph in a directory that contains a single project file or pass an explicit project file as the first argument.
175-
""");
154+
result.Should().Match(65, stdErrPattern: """
155+
Generating dependency graph for nugraph
156+
Solution files are not supported.
157+
Please run nugraph in a directory that contains a single project file or pass an explicit project file as the first argument.
158+
""");
176159
}
177160

178161
[Test]
179162
public async Task Project_NoProject()
180163
{
181164
var result = await nugraph.RunAsync([], workingDirectory: RepositoryDirectories.GetPath("resources"));
182165

183-
result.Should().Fail(65, """
184-
The current working directory does not contain a project file.
185-
Please run nugraph in a directory that contains a single project file or pass an explicit project file as the first argument.
186-
""");
166+
result.Should().Match(65, stdErrPattern: """
167+
Generating dependency graph for resources
168+
The current working directory does not contain a project file.
169+
Please run nugraph in a directory that contains a single project file or pass an explicit project file as the first argument.
170+
""");
187171
}
188172
}

0 commit comments

Comments
 (0)