Skip to content

Commit 6e409de

Browse files
committed
Add unit tests for deferred telemetry pipe attachment
Cover the buffering, replay, cap, and retry interval logic added in the previous commits. Test seams (CreateDeferredForTesting, TryAttachTestListener, GetRetryInterval) are internal so only the unit test assembly can use them. Tests: - Buffer and replay telemetry messages on attach - Non-telemetry messages are not buffered - Buffer caps at 1000 messages - Second attach attempt returns false - Null/empty gitBinRoot returns false - Retry interval exponential backoff values - Events after attach go directly to listener Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent e272141 commit 6e409de

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

GVFS/GVFS.Common/Tracing/JsonTracer.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,46 @@ public JsonTracer(string providerName, Guid providerActivityId, string activityN
9191
}
9292
}
9393

94+
/// <summary>
95+
/// Creates a JsonTracer in deferred telemetry mode for testing.
96+
/// No timer is started — call TryAttachTelemetryPipe manually.
97+
/// </summary>
98+
internal static JsonTracer CreateDeferredForTesting(string providerName, string activityName)
99+
{
100+
JsonTracer tracer = new JsonTracer(null, Guid.Empty, activityName, EventLevel.Informational, Keywords.Telemetry);
101+
tracer.deferredProviderName = providerName;
102+
tracer.deferredMessages = new ConcurrentQueue<TraceEventMessage>();
103+
return tracer;
104+
}
105+
106+
/// <summary>
107+
/// Attaches the given listener as if TelemetryDaemonEventListener
108+
/// had been created, replaying buffered messages. For testing only.
109+
/// </summary>
110+
internal bool TryAttachTestListener(EventListener listener)
111+
{
112+
lock (this.deferredLock)
113+
{
114+
if (this.deferredMessages == null)
115+
{
116+
return false;
117+
}
118+
119+
this.listeners.Add(listener);
120+
121+
ConcurrentQueue<TraceEventMessage> queue = this.deferredMessages;
122+
this.deferredMessages = null;
123+
this.StopDeferredRetryTimer();
124+
125+
while (queue.TryDequeue(out TraceEventMessage message))
126+
{
127+
listener.RecordMessage(message);
128+
}
129+
130+
return true;
131+
}
132+
}
133+
94134
private JsonTracer(ConcurrentBag<EventListener> listeners, Guid parentActivityId, string activityName, EventLevel startStopLevel, Keywords startStopKeywords)
95135
{
96136
this.listeners = listeners ?? new ConcurrentBag<EventListener>();
@@ -562,7 +602,7 @@ private void OnDeferredRetryTimer(object state)
562602
}
563603
}
564604

565-
private static int GetRetryInterval(int retryCount)
605+
internal static int GetRetryInterval(int retryCount)
566606
{
567607
return retryCount switch
568608
{
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using GVFS.Common.Tracing;
2+
using GVFS.Tests.Should;
3+
using GVFS.UnitTests.Mock.Common.Tracing;
4+
using NUnit.Framework;
5+
6+
namespace GVFS.UnitTests.Common
7+
{
8+
[TestFixture]
9+
public class JsonTracerDeferredTests
10+
{
11+
[TestCase]
12+
public void DeferredMode_BuffersTelemetryMessages()
13+
{
14+
using (JsonTracer tracer = JsonTracer.CreateDeferredForTesting("TestProvider", "BufferTest"))
15+
using (MockListener replayListener = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
16+
{
17+
tracer.RelatedEvent(EventLevel.Informational, "Event1", metadata: null, keyword: Keywords.Telemetry);
18+
tracer.RelatedEvent(EventLevel.Informational, "Event2", metadata: null, keyword: Keywords.Telemetry);
19+
20+
bool attached = tracer.TryAttachTestListener(replayListener);
21+
attached.ShouldBeTrue();
22+
23+
replayListener.EventNamesRead.Count.ShouldEqual(2);
24+
replayListener.EventNamesRead.ShouldContain(name => name.Equals("Event1"));
25+
replayListener.EventNamesRead.ShouldContain(name => name.Equals("Event2"));
26+
}
27+
}
28+
29+
[TestCase]
30+
public void DeferredMode_DoesNotBufferNonTelemetryMessages()
31+
{
32+
using (JsonTracer tracer = JsonTracer.CreateDeferredForTesting("TestProvider", "NonTelemetryTest"))
33+
using (MockListener replayListener = new MockListener(EventLevel.Verbose, Keywords.Any))
34+
{
35+
tracer.RelatedEvent(EventLevel.Informational, "TelemetryEvent", metadata: null, keyword: Keywords.Telemetry);
36+
tracer.RelatedEvent(EventLevel.Informational, "NonTelemetryEvent", metadata: null, keyword: Keywords.Network);
37+
tracer.RelatedEvent(EventLevel.Informational, "PlainEvent", metadata: null);
38+
39+
bool attached = tracer.TryAttachTestListener(replayListener);
40+
attached.ShouldBeTrue();
41+
42+
// Only the telemetry event should have been buffered and replayed
43+
replayListener.EventNamesRead.Count.ShouldEqual(1);
44+
replayListener.EventNamesRead.ShouldContain(name => name.Equals("TelemetryEvent"));
45+
}
46+
}
47+
48+
[TestCase]
49+
public void DeferredMode_CapsBufferAtMaxMessages()
50+
{
51+
using (JsonTracer tracer = JsonTracer.CreateDeferredForTesting("TestProvider", "CapTest"))
52+
using (MockListener replayListener = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
53+
{
54+
// Emit 1050 telemetry events — only 1000 should be buffered
55+
for (int i = 0; i < 1050; i++)
56+
{
57+
tracer.RelatedEvent(EventLevel.Informational, $"Event{i}", metadata: null, keyword: Keywords.Telemetry);
58+
}
59+
60+
bool attached = tracer.TryAttachTestListener(replayListener);
61+
attached.ShouldBeTrue();
62+
63+
replayListener.EventNamesRead.Count.ShouldEqual(1000);
64+
replayListener.EventNamesRead[0].ShouldEqual("Event0");
65+
replayListener.EventNamesRead[999].ShouldEqual("Event999");
66+
}
67+
}
68+
69+
[TestCase]
70+
public void TryAttachTestListener_ReturnsFalseWhenNotDeferred()
71+
{
72+
using (JsonTracer tracer = new JsonTracer("TestProvider", "NotDeferredTest", disableTelemetry: true))
73+
using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any))
74+
{
75+
// Tracer was not created in deferred mode
76+
bool attached = tracer.TryAttachTestListener(listener);
77+
attached.ShouldBeFalse();
78+
}
79+
}
80+
81+
[TestCase]
82+
public void TryAttachTestListener_SecondAttachReturnsFalse()
83+
{
84+
using (JsonTracer tracer = JsonTracer.CreateDeferredForTesting("TestProvider", "DoubleAttachTest"))
85+
using (MockListener listener1 = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
86+
using (MockListener listener2 = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
87+
{
88+
tracer.TryAttachTestListener(listener1).ShouldBeTrue();
89+
90+
// Second attach should fail — no longer in deferred mode
91+
tracer.TryAttachTestListener(listener2).ShouldBeFalse();
92+
}
93+
}
94+
95+
[TestCase]
96+
public void TryAttachTelemetryPipe_ReturnsFalseWithNullGitBinRoot()
97+
{
98+
using (JsonTracer tracer = JsonTracer.CreateDeferredForTesting("TestProvider", "NullGitBinTest"))
99+
{
100+
bool attached = tracer.TryAttachTelemetryPipe(null);
101+
attached.ShouldBeFalse();
102+
103+
attached = tracer.TryAttachTelemetryPipe(string.Empty);
104+
attached.ShouldBeFalse();
105+
}
106+
}
107+
108+
[TestCase(0, 10_000)]
109+
[TestCase(1, 30_000)]
110+
[TestCase(2, 60_000)]
111+
[TestCase(3, 300_000)]
112+
[TestCase(4, 300_000)]
113+
[TestCase(100, 300_000)]
114+
public void GetRetryInterval_ReturnsExpectedValues(int retryCount, int expectedMs)
115+
{
116+
JsonTracer.GetRetryInterval(retryCount).ShouldEqual(expectedMs);
117+
}
118+
119+
[TestCase]
120+
public void DeferredMode_StopsBufferingAfterAttach()
121+
{
122+
using (JsonTracer tracer = JsonTracer.CreateDeferredForTesting("TestProvider", "StopBufferTest"))
123+
using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry))
124+
{
125+
tracer.RelatedEvent(EventLevel.Informational, "BeforeAttach", metadata: null, keyword: Keywords.Telemetry);
126+
tracer.TryAttachTestListener(listener).ShouldBeTrue();
127+
128+
// Events after attach go directly to listener, not buffered
129+
tracer.RelatedEvent(EventLevel.Informational, "AfterAttach", metadata: null, keyword: Keywords.Telemetry);
130+
131+
listener.EventNamesRead.Count.ShouldEqual(2);
132+
listener.EventNamesRead.ShouldContain(name => name.Equals("BeforeAttach"));
133+
listener.EventNamesRead.ShouldContain(name => name.Equals("AfterAttach"));
134+
}
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)