Repository navigation
Expand file tree
/
Copy pathQuick.Core.Options.Monitor.Tests.pas
More file actions
296 lines (247 loc) · 9.25 KB
/
Copy pathQuick.Core.Options.Monitor.Tests.pas
File metadata and controls
296 lines (247 loc) · 9.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
unit Quick.Core.Options.Monitor.Tests;
{ Tests: T-OPT-01..08
T-OPT-01 CurrentValue returns the registered options instance
T-OPT-02 OnChange callback fires when OnConfigReloaded is triggered
T-OPT-03 Callback receives the updated options value (modified before reload)
T-OPT-04 Multiple OnChange callbacks are all invoked on reload
T-OPT-05 Unregister stops the callback from being invoked
T-OPT-06 Unregister is idempotent (no error on double call)
T-OPT-07 IOptionsSnapshot.Value returns the current options value
T-OPT-08 nil callback in OnChange raises EArgumentNilException
}
interface
uses
DUnitX.TestFramework,
System.SysUtils,
System.Generics.Collections,
Quick.Options,
Quick.Core.Options.Monitor;
// ── Minimal null serializer ───────────────────────────────────────────────────
type
TNullOptionsSerializer = class(TInterfacedObject, IOptionsSerializer)
public
function Load(aSections: TSectionList; aFailOnSectionNotExists: Boolean): Boolean;
function LoadSection(aSections: TSectionList; aOptions: TOptions): Boolean;
procedure Save(aSections: TSectionList);
function GetFileSectionNames(out oSections: TArray<string>): Boolean;
function ConfigExists: Boolean;
end;
// ── Test options type ─────────────────────────────────────────────────────────
{$M+}
TDatabaseOptions = class(TOptions)
private
fConnectionString : string;
fMaxPoolSize : Integer;
public
constructor Create; override;
published
property ConnectionString : string read fConnectionString write fConnectionString;
property MaxPoolSize : Integer read fMaxPoolSize write fMaxPoolSize;
end;
{$M-}
// ── Test fixture ──────────────────────────────────────────────────────────────
[TestFixture]
TOptionsMonitorTests = class
private
FSerializer : IOptionsSerializer;
FContainer : TOptionsContainer;
FOptions : TDatabaseOptions;
procedure TriggerReload;
public
[Setup]
procedure SetUp;
[TearDown]
procedure TearDown;
[Test]
procedure T_OPT_01_CurrentValue_ReturnsRegisteredOptions;
[Test]
procedure T_OPT_02_OnChange_CallbackFiresOnReload;
[Test]
procedure T_OPT_03_OnChange_CallbackReceivesUpdatedValue;
[Test]
procedure T_OPT_04_MultipleCallbacks_AllFiredOnReload;
[Test]
procedure T_OPT_05_Unregister_StopsCallbackInvocation;
[Test]
procedure T_OPT_06_Unregister_IsIdempotent;
[Test]
procedure T_OPT_07_OptionsSnapshot_ReturnsCurrentValue;
[Test]
procedure T_OPT_08_OnChange_NilCallback_Raises;
end;
implementation
// ── TNullOptionsSerializer ────────────────────────────────────────────────────
function TNullOptionsSerializer.Load(aSections: TSectionList; aFailOnSectionNotExists: Boolean): Boolean;
begin
Result := True;
end;
function TNullOptionsSerializer.LoadSection(aSections: TSectionList; aOptions: TOptions): Boolean;
begin
Result := True;
end;
procedure TNullOptionsSerializer.Save(aSections: TSectionList);
begin
end;
function TNullOptionsSerializer.GetFileSectionNames(out oSections: TArray<string>): Boolean;
begin
oSections := [];
Result := False;
end;
function TNullOptionsSerializer.ConfigExists: Boolean;
begin
Result := False;
end;
// ── TDatabaseOptions ──────────────────────────────────────────────────────────
constructor TDatabaseOptions.Create;
begin
inherited;
Name := 'Database';
fConnectionString := 'Server=localhost;Database=test';
fMaxPoolSize := 10;
end;
// ── TOptionsMonitorTests ──────────────────────────────────────────────────────
procedure TOptionsMonitorTests.SetUp;
begin
FSerializer := TNullOptionsSerializer.Create;
FContainer := TOptionsContainer.Create(FSerializer);
FOptions := TDatabaseOptions.Create;
FContainer.AddOption(FOptions);
end;
procedure TOptionsMonitorTests.TearDown;
begin
FContainer.Free;
FSerializer := nil;
end;
procedure TOptionsMonitorTests.TriggerReload;
var
handler: TLoadConfigEvent;
begin
// Simulate what TFileOptionsContainer does after detecting a changed file:
// fire OnConfigReloaded so the monitor and all chained handlers run.
handler := FContainer.OnConfigReloaded;
if Assigned(handler) then
handler;
end;
{ T-OPT-01 }
procedure TOptionsMonitorTests.T_OPT_01_CurrentValue_ReturnsRegisteredOptions;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
Assert.IsNotNull(monitor.CurrentValue);
Assert.AreEqual('Server=localhost;Database=test', monitor.CurrentValue.ConnectionString);
end;
{ T-OPT-02 }
procedure TOptionsMonitorTests.T_OPT_02_OnChange_CallbackFiresOnReload;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
callCount : Integer;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
callCount := 0;
monitor.OnChange(procedure(o: TDatabaseOptions)
begin
Inc(callCount);
end);
TriggerReload;
Assert.AreEqual(1, callCount, 'callback must be called exactly once per reload');
end;
{ T-OPT-03 }
procedure TOptionsMonitorTests.T_OPT_03_OnChange_CallbackReceivesUpdatedValue;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
capturedConnStr : string;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
monitor.OnChange(procedure(o: TDatabaseOptions)
begin
capturedConnStr := o.ConnectionString;
end);
// Simulate a config reload: update the options object then trigger reload
FOptions.ConnectionString := 'Server=prod;Database=live';
TriggerReload;
Assert.AreEqual('Server=prod;Database=live', capturedConnStr,
'callback must receive the updated connection string');
end;
{ T-OPT-04 }
procedure TOptionsMonitorTests.T_OPT_04_MultipleCallbacks_AllFiredOnReload;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
count1 : Integer;
count2 : Integer;
count3 : Integer;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
count1 := 0;
count2 := 0;
count3 := 0;
monitor.OnChange(procedure(o: TDatabaseOptions) begin Inc(count1) end);
monitor.OnChange(procedure(o: TDatabaseOptions) begin Inc(count2) end);
monitor.OnChange(procedure(o: TDatabaseOptions) begin Inc(count3) end);
TriggerReload;
Assert.AreEqual(1, count1, 'callback 1 must fire');
Assert.AreEqual(1, count2, 'callback 2 must fire');
Assert.AreEqual(1, count3, 'callback 3 must fire');
end;
{ T-OPT-05 }
procedure TOptionsMonitorTests.T_OPT_05_Unregister_StopsCallbackInvocation;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
callCount : Integer;
token : IOptionsChangeToken;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
callCount := 0;
token := monitor.OnChange(procedure(o: TDatabaseOptions)
begin
Inc(callCount);
end);
TriggerReload; // call 1 — should fire
token.Unregister;
TriggerReload; // call 2 — must NOT fire
Assert.AreEqual(1, callCount, 'callback must not be called after Unregister');
end;
{ T-OPT-06 }
procedure TOptionsMonitorTests.T_OPT_06_Unregister_IsIdempotent;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
token : IOptionsChangeToken;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
token := monitor.OnChange(procedure(o: TDatabaseOptions) begin end);
// Assert.WillNotRaise cannot match the anon-proc overload in this compiler;
// calling directly: if it raises the test will fail automatically.
token.Unregister;
token.Unregister; // second call must not raise
end;
{ T-OPT-07 }
procedure TOptionsMonitorTests.T_OPT_07_OptionsSnapshot_ReturnsCurrentValue;
var
snapshot : IOptionsSnapshot<TDatabaseOptions>;
begin
snapshot := TOptionsSnapshot<TDatabaseOptions>.Create(FContainer);
Assert.IsNotNull(snapshot.Value);
Assert.AreEqual('Server=localhost;Database=test', snapshot.Value.ConnectionString);
// Modify option and verify snapshot sees the change (live read)
FOptions.ConnectionString := 'Server=updated;Database=new';
Assert.AreEqual('Server=updated;Database=new', snapshot.Value.ConnectionString,
'snapshot.Value must reflect updated options');
end;
{ T-OPT-08 }
procedure TOptionsMonitorTests.T_OPT_08_OnChange_NilCallback_Raises;
var
monitor : IOptionsMonitor<TDatabaseOptions>;
nilCallback : TOptionsChangeCallback<TDatabaseOptions>;
begin
monitor := TOptionsMonitor<TDatabaseOptions>.Create(FContainer);
nilCallback := nil;
try
monitor.OnChange(nilCallback);
Assert.Fail('Expected EArgumentNilException was not raised');
except
on E: EArgumentNilException do ; // expected
end;
end;
initialization
TDUnitX.RegisterTestFixture(TOptionsMonitorTests);
end.