Skip to content

Commit 4c194c7

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

File tree

3 files changed

+128
-9
lines changed

3 files changed

+128
-9
lines changed

Diff for: src/System.CommandLine.Tests/ObservabilityTests.cs

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
command.SetAction(async (pr, ctok) =>
86+
{
87+
throw new Exception("Something went wrong");
88+
});
89+
90+
var result = await command.Parse(Array.Empty<string>()).InvokeAsync();
91+
92+
activities
93+
.Should()
94+
.ContainSingle(
95+
a => a.OperationName == "System.CommandLine.Invoke"
96+
&& a.DisplayName == "the-command"
97+
&& a.Status == ActivityStatusCode.Error
98+
&& a.Tags.Any(t => t.Key == "command" && t.Value == "the-command")
99+
&& a.Tags.Any(t => t.Key == "invoke.type" && t.Value == "async")
100+
&& a.TagObjects.Any(t => t.Key == "exitcode" && (int)t.Value == 1)
101+
&& a.Baggage.Any(t => t.Key == "exception"));
102+
}
103+
104+
private static List<Activity> SetupListener()
105+
{
106+
List<Activity> activities = new();
107+
var listener = new ActivityListener();
108+
listener.ShouldListenTo = s => true;
109+
listener.Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData;
110+
listener.ActivityStopped = a => activities.Add(a);
111+
ActivitySource.AddActivityListener(listener);
112+
return activities;
113+
}
114+
}
115+
}

Diff for: src/System.CommandLine/Activities.cs

+7-6
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

Diff for: src/System.CommandLine/Parsing/CommandLineParser.cs

+6-3
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)