Skip to content

Commit fda5db0

Browse files
add unit tests
1 parent f1d26b4 commit fda5db0

File tree

14 files changed

+1230
-18
lines changed

14 files changed

+1230
-18
lines changed

dotnet/src/Azure.Iot.Operations.Services/Observability/AkriObservabilityServiceStub.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// Licensed under the MIT License.
33

44
using Azure.Iot.Operations.Protocol;
5+
using Azure.Iot.Operations.Protocol.RPC;
6+
using Azure.Iot.Operations.Services.Observability.AkriObservabilityService;
57

68
namespace Azure.Iot.Operations.Services.Observability;
79

8-
public class AkriObservabilityServiceStub : AkriObservabilityService.AkriObservabilityService.Client
10+
public class AkriObservabilityServiceStub : AkriObservabilityService.AkriObservabilityService.Client, IAkriObservabilityService
911
{
1012
public AkriObservabilityServiceStub(
1113
ApplicationContext applicationContext,
@@ -16,4 +18,9 @@ public AkriObservabilityServiceStub(
1618
topicTokenMap)
1719
{
1820
}
21+
22+
public RpcCallAsync<PublishMetricsResponsePayload> PublishMetricsAsync(PublishMetricsRequestPayload request)
23+
{
24+
return base.PublishMetricsAsync(request);
25+
}
1926
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Iot.Operations.Services.Observability;
5+
6+
public class DefaultTimerFactory : ITimerFactory
7+
{
8+
public ITimer CreateTimer()
9+
{
10+
return new TimerWrapper();
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Iot.Operations.Services.Observability;
5+
6+
using Protocol.RPC;
7+
using AkriObservabilityService;
8+
9+
public interface IAkriObservabilityService
10+
{
11+
RpcCallAsync<PublishMetricsResponsePayload> PublishMetricsAsync(PublishMetricsRequestPayload request);
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Iot.Operations.Services.Observability;
5+
6+
public interface ITimer : IAsyncDisposable
7+
{
8+
void Start(Func<CancellationToken, Task> callback, CancellationToken cancellationToken, TimeSpan dueTime, TimeSpan period);
9+
Task StopAsync();
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Iot.Operations.Services.Observability;
5+
6+
public interface ITimerFactory
7+
{
8+
ITimer CreateTimer();
9+
}

dotnet/src/Azure.Iot.Operations.Services/Observability/MetricsReporterService.cs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,45 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using Azure.Iot.Operations.Protocol.RPC;
45
using Azure.Iot.Operations.Services.Observability.AkriObservabilityService;
56

67
namespace Azure.Iot.Operations.Services.Observability;
78

89
public class MetricsReporterService : IAsyncDisposable
910
{
10-
private readonly AkriObservabilityServiceStub _observabilityService;
11+
private readonly IAkriObservabilityService _observabilityService;
1112
private readonly MetricsReporterOptions _options;
1213
private readonly CounterMetricCache _counterCache;
1314
private readonly GaugeMetricCache _gaugeCache;
1415
private readonly HistogramMetricCache _histogramCache;
15-
private Timer? _reportingTimer;
16+
private readonly ITimerFactory _timerFactory;
17+
private ITimer? _reportingTimer;
1618
private bool _isDisposed;
1719
private readonly SemaphoreSlim _reportingSemaphore = new(1, 1);
1820

1921
public MetricsReporterService(
20-
AkriObservabilityServiceStub observabilityService,
21-
MetricsReporterOptions? options = null)
22+
IAkriObservabilityService observabilityService,
23+
MetricsReporterOptions? options = null,
24+
ITimerFactory? timerFactory = null)
2225
{
2326
_observabilityService = observabilityService ?? throw new ArgumentNullException(nameof(observabilityService));
2427
_options = options ?? new MetricsReporterOptions();
28+
_timerFactory = timerFactory ?? new DefaultTimerFactory();
2529

2630
_counterCache = new CounterMetricCache();
2731
_gaugeCache = new GaugeMetricCache();
2832
_histogramCache = new HistogramMetricCache();
2933
}
3034

31-
public async Task StartAsync(CancellationToken cancellationToken = default)
35+
public void Start(CancellationToken cancellationToken = default)
3236
{
33-
if (_reportingTimer != null)
34-
{
35-
throw new InvalidOperationException("MetricsReporterService has already been started.");
36-
}
37+
_reportingTimer = _timerFactory.CreateTimer();
3738

38-
_reportingTimer = new Timer(
39-
async _ => await ReportMetricsAsync(cancellationToken),
40-
null,
39+
_reportingTimer.Start(async _ => await ReportMetricsAsync(cancellationToken),
40+
cancellationToken,
4141
_options.ReportingInterval,
4242
_options.ReportingInterval);
43-
44-
await Task.CompletedTask;
4543
}
4644

4745
public async Task StopAsync()
@@ -107,12 +105,13 @@ private async Task ReportMetricsAsync(CancellationToken cancellationToken)
107105
};
108106

109107
// Only send if there are metrics to report
110-
if (request.Metrics.CounterMetrics.Count > 0 || request.Metrics.GaugeMetrics.Count > 0 || request.Metrics.HistogramMetrics.Count > 0)
108+
if (request.Metrics.CounterMetrics.Count > 0 ||
109+
request.Metrics.GaugeMetrics.Count > 0 ||
110+
request.Metrics.HistogramMetrics.Count > 0)
111111
{
112112
try
113113
{
114-
// Fire and forget
115-
var call = _observabilityService.PublishMetricsAsync(request);
114+
RpcCallAsync<PublishMetricsResponsePayload> response = _observabilityService.PublishMetricsAsync(request);
116115
}
117116
catch (Exception ex)
118117
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Iot.Operations.Services.Observability
5+
{
6+
public class TimerWrapper : ITimer
7+
{
8+
private Timer? _timer;
9+
private bool _isDisposed;
10+
11+
public void Start(Func<CancellationToken, Task> callback, CancellationToken cancellationToken, TimeSpan dueTime, TimeSpan period)
12+
{
13+
_timer = new Timer(
14+
async _ => await callback(CancellationToken.None),
15+
null,
16+
dueTime,
17+
period);
18+
}
19+
20+
public async Task StopAsync()
21+
{
22+
if (_timer != null)
23+
{
24+
await _timer.DisposeAsync();
25+
_timer = null;
26+
}
27+
}
28+
29+
public async ValueTask DisposeAsync()
30+
{
31+
if (_isDisposed)
32+
{
33+
return;
34+
}
35+
36+
await StopAsync();
37+
_isDisposed = true;
38+
}
39+
}
40+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Iot.Operations.Services.Observability;
5+
using Xunit;
6+
7+
namespace Azure.Iot.Operations.Services.UnitTests.Observability;
8+
9+
public class CachedCounterTests
10+
{
11+
private readonly string _name = "test_counter";
12+
13+
private readonly Dictionary<string, string> _labels = new()
14+
{
15+
{ "service", "test" },
16+
{ "instance", "instance1" }
17+
};
18+
19+
private readonly string _unit = "bytes";
20+
21+
[Fact]
22+
public void Constructor_InitializesProperties()
23+
{
24+
// Arrange & Act
25+
var counter = new CachedCounter(_name, _labels, _unit);
26+
27+
// Assert
28+
Assert.Equal(_name, counter.Name);
29+
Assert.Equal(_labels, counter.Labels);
30+
Assert.Equal(_unit, counter.Unit);
31+
}
32+
33+
[Fact]
34+
public void Add_AddsOperationToQueue()
35+
{
36+
// Arrange
37+
var counter = new CachedCounter(_name, _labels, _unit);
38+
var value = 5.0;
39+
40+
// Act
41+
counter.Add(value);
42+
43+
// Assert
44+
var operations = counter.GetOperationsAndClear(10);
45+
Assert.Single(operations);
46+
Assert.Equal(value, operations[0].Value);
47+
Assert.NotNull(operations[0].OperationId);
48+
Assert.True(operations[0].Timestamp <= DateTime.UtcNow);
49+
Assert.True(operations[0].Timestamp >= DateTime.UtcNow.AddMinutes(-1)); // Should be recent
50+
}
51+
52+
[Fact]
53+
public void Increment_AddsOperationWithValueOne()
54+
{
55+
// Arrange
56+
var counter = new CachedCounter(_name, _labels, _unit);
57+
58+
// Act
59+
counter.Increment();
60+
61+
// Assert
62+
var operations = counter.GetOperationsAndClear(10);
63+
Assert.Single(operations);
64+
Assert.Equal(1.0, operations[0].Value);
65+
}
66+
67+
[Fact]
68+
public void MultipleOperations_AddedCorrectly()
69+
{
70+
// Arrange
71+
var counter = new CachedCounter(_name, _labels, _unit);
72+
73+
// Act
74+
counter.Add(3.0);
75+
counter.Increment();
76+
counter.Add(7.5);
77+
78+
// Assert
79+
var operations = counter.GetOperationsAndClear(10);
80+
Assert.Equal(3, operations.Count);
81+
Assert.Equal(3.0, operations[0].Value);
82+
Assert.Equal(1.0, operations[1].Value);
83+
Assert.Equal(7.5, operations[2].Value);
84+
}
85+
86+
[Fact]
87+
public void GetOperationsAndClear_RespectsMaxCount()
88+
{
89+
// Arrange
90+
var counter = new CachedCounter(_name, _labels, _unit);
91+
92+
// Add 5 operations
93+
for (var i = 1; i <= 5; i++) counter.Add(i);
94+
95+
// Act - Get only 3 operations
96+
var firstBatch = counter.GetOperationsAndClear(3);
97+
98+
// Assert
99+
Assert.Equal(3, firstBatch.Count);
100+
Assert.Equal(1.0, firstBatch[0].Value);
101+
Assert.Equal(2.0, firstBatch[1].Value);
102+
Assert.Equal(3.0, firstBatch[2].Value);
103+
104+
// Get remaining operations
105+
var secondBatch = counter.GetOperationsAndClear(10);
106+
Assert.Equal(2, secondBatch.Count);
107+
Assert.Equal(4.0, secondBatch[0].Value);
108+
Assert.Equal(5.0, secondBatch[1].Value);
109+
}
110+
111+
[Fact]
112+
public void GetOperationsAndClear_EmptiesQueue()
113+
{
114+
// Arrange
115+
var counter = new CachedCounter(_name, _labels, _unit);
116+
counter.Add(1.0);
117+
counter.Add(2.0);
118+
119+
// Act
120+
var operations = counter.GetOperationsAndClear(10);
121+
122+
// Assert
123+
Assert.Equal(2, operations.Count);
124+
125+
// Verify queue is now empty
126+
var emptyOperations = counter.GetOperationsAndClear(10);
127+
Assert.Empty(emptyOperations);
128+
}
129+
130+
[Fact]
131+
public void GetOperationsAndClear_WithEmptyQueue_ReturnsEmptyList()
132+
{
133+
// Arrange
134+
var counter = new CachedCounter(_name, _labels, _unit);
135+
136+
// Act
137+
var operations = counter.GetOperationsAndClear(10);
138+
139+
// Assert
140+
Assert.Empty(operations);
141+
}
142+
}

0 commit comments

Comments
 (0)