Skip to content

Commit 0f15752

Browse files
committed
add some basic tests to assert on activity presence and shapes
1 parent 2706703 commit 0f15752

File tree

3 files changed

+130
-9
lines changed

3 files changed

+130
-9
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.CommandLine.Parsing;
2+
using FluentAssertions;
3+
using System.Linq;
4+
using Xunit;
5+
using System.Diagnostics;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
8+
9+
namespace System.CommandLine.Tests
10+
{
11+
public class ObservabilityTests
12+
{
13+
14+
[Fact]
15+
public void It_creates_activity_spans_for_parsing()
16+
{
17+
List<Activity> activities = SetupListener();
18+
19+
var command = new Command("the-command")
20+
{
21+
new Option<string>("--option")
22+
};
23+
24+
var args = new[] { "--option", "the-argument" };
25+
26+
var result = command.Parse(args);
27+
28+
activities
29+
.Should()
30+
.ContainSingle(
31+
a => a.OperationName == "System.CommandLine.Parse"
32+
&& a.Status == ActivityStatusCode.Ok
33+
&& a.Tags.Any(t => t.Key == "command" && t.Value == "the-command"));
34+
}
35+
36+
[Fact]
37+
public void It_creates_activity_spans_for_parsing_errors()
38+
{
39+
List<Activity> activities = SetupListener();
40+
41+
var command = new Command("the-command")
42+
{
43+
new Option<string>("--option")
44+
};
45+
46+
var args = new[] { "--opt", "the-argument" };
47+
var result = command.Parse(args);
48+
49+
activities
50+
.Should()
51+
.ContainSingle(
52+
a => a.OperationName == "System.CommandLine.Parse"
53+
&& a.Status == ActivityStatusCode.Error
54+
&& a.Tags.Any(t => t.Key == "command" && t.Value == "the-command")
55+
&& a.Baggage.Any(t => t.Key == "errors"));
56+
}
57+
58+
[Fact]
59+
public async Task It_creates_activity_spans_for_invocations()
60+
{
61+
List<Activity> activities = SetupListener();
62+
63+
var command = new Command("the-command");
64+
command.SetAction(async (pr, ctok) => await Task.FromResult(0));
65+
66+
var result = await command.Parse(Array.Empty<string>()).InvokeAsync();
67+
68+
activities
69+
.Should()
70+
.ContainSingle(
71+
a => a.OperationName == "System.CommandLine.Invoke"
72+
&& a.DisplayName == "the-command"
73+
&& a.Status == ActivityStatusCode.Ok
74+
&& a.Tags.Any(t => t.Key == "command" && t.Value == "the-command")
75+
&& a.Tags.Any(t => t.Key == "invoke.type" && t.Value == "async")
76+
&& a.TagObjects.Any(t => t.Key == "exitcode" && (int)t.Value == 0));
77+
}
78+
79+
[Fact]
80+
public async Task It_creates_activity_spans_for_invocation_errors()
81+
{
82+
List<Activity> activities = SetupListener();
83+
84+
var command = new Command("the-command");
85+
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
86+
command.SetAction(async (pr, ctok) =>
87+
{
88+
throw new Exception("Something went wrong");
89+
});
90+
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
91+
92+
var result = await command.Parse(Array.Empty<string>()).InvokeAsync();
93+
94+
activities
95+
.Should()
96+
.ContainSingle(
97+
a => a.OperationName == "System.CommandLine.Invoke"
98+
&& a.DisplayName == "the-command"
99+
&& a.Status == ActivityStatusCode.Error
100+
&& a.Tags.Any(t => t.Key == "command" && t.Value == "the-command")
101+
&& a.Tags.Any(t => t.Key == "invoke.type" && t.Value == "async")
102+
&& a.TagObjects.Any(t => t.Key == "exitcode" && (int)t.Value == 1)
103+
&& a.Baggage.Any(t => t.Key == "exception"));
104+
}
105+
106+
private static List<Activity> SetupListener()
107+
{
108+
List<Activity> activities = new();
109+
var listener = new ActivityListener();
110+
listener.ShouldListenTo = s => true;
111+
listener.Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData;
112+
listener.ActivityStopped = a => activities.Add(a);
113+
ActivitySource.AddActivityListener(listener);
114+
return activities;
115+
}
116+
}
117+
}

src/System.CommandLine/Activities.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ internal static class DiagnosticsStrings
77
internal const string LibraryNamespace = "System.CommandLine";
88
internal const string ParseMethod = LibraryNamespace + ".Parse";
99
internal const string InvokeMethod = LibraryNamespace + ".Invoke";
10-
internal const string InvokeType = nameof(InvokeType);
11-
internal const string Async = nameof(Async);
12-
internal const string Sync = nameof(Sync);
13-
internal const string ExitCode = nameof(ExitCode);
14-
internal const string Exception = nameof(Exception);
15-
internal const string Command = nameof(Command);
10+
internal const string InvokeType = "invoke.type";
11+
internal const string Async = "async";
12+
internal const string Sync = "sync";
13+
internal const string ExitCode = "exitcode";
14+
internal const string Exception = "exception";
15+
internal const string Errors = "errors";
16+
internal const string Command = "command";
1617
}
1718

1819
internal static class Activities

src/System.CommandLine/Parsing/CommandLineParser.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,15 @@ private static ParseResult Parse(
163163
rawInput);
164164

165165
var result = operation.Parse();
166-
parseActivity?.AddBaggage(DiagnosticsStrings.Command, result.CommandResult?.Command.Name);
166+
parseActivity?.SetTag(DiagnosticsStrings.Command, result.CommandResult?.Command.Name);
167167
if (result.Errors.Count > 0)
168168
{
169169
parseActivity?.SetStatus(Diagnostics.ActivityStatusCode.Error);
170-
parseActivity?.AddBaggage("Errors", string.Join("\n", result.Errors.Select(e => e.Message)));
171-
170+
parseActivity?.AddBaggage(DiagnosticsStrings.Errors, string.Join("\n", result.Errors.Select(e => e.Message)));
171+
}
172+
else
173+
{
174+
parseActivity?.SetStatus(Diagnostics.ActivityStatusCode.Ok);
172175
}
173176
return result;
174177
}

0 commit comments

Comments
 (0)