-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathPooledCelerityDictionaryTests.cs
More file actions
355 lines (298 loc) · 11.8 KB
/
Copy pathPooledCelerityDictionaryTests.cs
File metadata and controls
355 lines (298 loc) · 11.8 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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
using Celerity.Collections;
using Celerity.Hashing;
namespace Celerity.Tests.Collections;
// Issue #21 — ArrayPool-backed dictionary. Mirror of CelerityDictionaryTests for
// the functional surface, plus pooled-specific tests for the rent / return
// lifecycle (Dispose, double-dispose, use-after-dispose, resize returning old
// buffers, the over-provisioned-rent bounds, and reference-type clear-on-return).
public class PooledCelerityDictionaryTests
{
[Fact]
public void Indexer_ShouldInsertAndRetrieveValue()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map[10] = 100;
Assert.Equal(100, map[10]);
}
[Fact]
public void Indexer_ShouldThrowException_WhenKeyDoesNotExist()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
Assert.Throws<KeyNotFoundException>(() => { var value = map[99]; });
}
[Fact]
public void Indexer_ShouldOverwriteExistingValue()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map[5] = 500;
map[5] = 999; // Overwrite
Assert.Equal(999, map[5]);
}
[Fact]
public void ContainsKey_ShouldReturnTrue_WhenKeyExists()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map[7] = 700;
Assert.True(map.ContainsKey(7));
}
[Fact]
public void ContainsKey_ShouldReturnFalse_WhenKeyDoesNotExist()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
Assert.False(map.ContainsKey(7));
}
[Fact]
public void Remove_ShouldDeleteKeyAndMakeItUnreachable()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map[7] = 700;
bool removed = map.Remove(7);
Assert.True(removed);
Assert.False(map.ContainsKey(7));
Assert.Throws<KeyNotFoundException>(() => { var value = map[7]; });
}
[Fact]
public void Remove_ShouldReturnFalse_WhenKeyDoesNotExist()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
Assert.False(map.Remove(7));
}
[Fact]
public void Map_ShouldResize_WhenThresholdExceeded()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>(4);
map[1] = 10;
map[2] = 20;
map[3] = 30;
map[4] = 40; // Triggers resize
Assert.Equal(4, map.Count);
Assert.Equal(10, map[1]);
Assert.Equal(20, map[2]);
Assert.Equal(30, map[3]);
Assert.Equal(40, map[4]);
}
[Fact]
public void Indexer_ShouldHandleZeroIntKey()
{
using var map = new PooledCelerityDictionary<int, string, Int32WangNaiveHasher>();
map[0] = "zero";
Assert.True(map.ContainsKey(0));
Assert.Equal("zero", map[0]);
Assert.Single(map);
}
[Fact]
public void Indexer_ShouldOverwriteZeroIntKey()
{
using var map = new PooledCelerityDictionary<int, string, Int32WangNaiveHasher>();
map[0] = "zero";
map[0] = "still-zero";
Assert.Single(map);
Assert.Equal("still-zero", map[0]);
}
[Fact]
public void Remove_ShouldHandleZeroIntKey()
{
using var map = new PooledCelerityDictionary<int, string, Int32WangNaiveHasher>();
map[0] = "zero";
map[1] = "one";
Assert.True(map.Remove(0));
Assert.False(map.ContainsKey(0));
Assert.True(map.ContainsKey(1));
Assert.Single(map);
Assert.False(map.Remove(0));
}
[Fact]
public void DefaultKey_ShouldSurviveResize()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>(4);
map[0] = -1;
map[1] = 10;
map[2] = 20;
map[3] = 30;
map[4] = 40; // Triggers resize while the default-key entry is live.
Assert.Equal(5, map.Count);
Assert.Equal(-1, map[0]);
Assert.Equal(10, map[1]);
Assert.Equal(40, map[4]);
}
[Fact]
public void TryGetValue_ShouldReturnTrueAndValue_WhenKeyExists()
{
using var map = new PooledCelerityDictionary<int, string, Int32WangNaiveHasher>();
map[42] = "answer";
map[0] = "zero";
Assert.True(map.TryGetValue(42, out var v1));
Assert.Equal("answer", v1);
Assert.True(map.TryGetValue(0, out var v2));
Assert.Equal("zero", v2);
}
[Fact]
public void TryGetValue_ShouldReturnFalseAndDefault_WhenKeyMissing()
{
using var map = new PooledCelerityDictionary<int, string, Int32WangNaiveHasher>();
Assert.False(map.TryGetValue(42, out var v1));
Assert.Null(v1);
Assert.False(map.TryGetValue(0, out var v2));
Assert.Null(v2);
}
[Fact]
public void Clear_ShouldRemoveAllEntries()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
for (int i = 0; i < 32; i++)
map[i] = i * i;
map.Clear();
Assert.Empty(map);
for (int i = 0; i < 32; i++)
Assert.False(map.ContainsKey(i));
map[0] = 100;
map[5] = 500;
Assert.Equal(2, map.Count);
Assert.Equal(100, map[0]);
Assert.Equal(500, map[5]);
}
[Fact]
public void Indexer_ShouldHandleGuidEmptyKey()
{
using var map = new PooledCelerityDictionary<Guid, string, GuidIdentityHasher>();
map[Guid.Empty] = "empty";
var other = Guid.NewGuid();
map[other] = "other";
Assert.True(map.ContainsKey(Guid.Empty));
Assert.Equal("empty", map[Guid.Empty]);
Assert.Equal("other", map[other]);
Assert.Equal(2, map.Count);
}
[Fact]
public void Indexer_ShouldThrowKeyNotFound_ForAbsentDefaultKey()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
Assert.Throws<KeyNotFoundException>(() => _ = map[0]);
}
[Fact]
public void Clear_ShouldBeNoOp_WhenAlreadyEmpty()
{
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map.Clear(); // _count == 0 early-return path
Assert.Empty(map);
}
[Fact]
public void ContainsValue_ShouldFindStoredValuesAndDefaultKeyValue()
{
using var map = new PooledCelerityDictionary<int, string, Int32WangNaiveHasher>();
map[1] = "one";
map[0] = "zero"; // out-of-band default-key value
Assert.True(map.ContainsValue("one"));
Assert.True(map.ContainsValue("zero"));
Assert.False(map.ContainsValue("missing"));
}
[Fact]
public void IEnumerableConstructor_ShouldCopyAllPairs()
{
var source = new Dictionary<int, int> { [1] = 10, [2] = 20, [3] = 30 };
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>(source);
Assert.Equal(3, map.Count);
Assert.Equal(10, map[1]);
Assert.Equal(20, map[2]);
Assert.Equal(30, map[3]);
}
// ---------------------------------------------------------------
// Pooled-specific lifecycle tests
// ---------------------------------------------------------------
[Fact]
public void Dispose_ShouldBeIdempotent()
{
var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map[1] = 10;
map.Dispose();
// Double-dispose must not throw and must not return the (now-pooled)
// buffers a second time, which could hand the same array to two owners.
var ex = Record.Exception(() => map.Dispose());
Assert.Null(ex);
}
[Fact]
public void UseAfterDispose_ShouldThrowObjectDisposedException()
{
var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>();
map[1] = 10;
map.Dispose();
Assert.Throws<ObjectDisposedException>(() => map[1] = 2);
Assert.Throws<ObjectDisposedException>(() => _ = map[1]);
Assert.Throws<ObjectDisposedException>(() => map.ContainsKey(1));
Assert.Throws<ObjectDisposedException>(() => map.ContainsValue(10));
Assert.Throws<ObjectDisposedException>(() => map.TryGetValue(1, out _));
Assert.Throws<ObjectDisposedException>(() => map.Remove(1));
Assert.Throws<ObjectDisposedException>(() => map.Add(2, 2));
Assert.Throws<ObjectDisposedException>(() => map.TryAdd(2, 2));
Assert.Throws<ObjectDisposedException>(() => map.Clear());
Assert.Throws<ObjectDisposedException>(() => map.GetEnumerator());
Assert.Throws<ObjectDisposedException>(() => map.EnsureCapacity(64));
Assert.Throws<ObjectDisposedException>(() => map.TrimExcess());
// Read accessors must also honour the disposed contract: Dispose returns
// the backing arrays to the pool, so a silent Count / Keys / Values here
// would report over — or enumerate — buffers the pool may have re-handed
// out. Regression for #296.
Assert.Throws<ObjectDisposedException>(() => _ = map.Count);
Assert.Throws<ObjectDisposedException>(() => _ = map.Keys);
Assert.Throws<ObjectDisposedException>(() => _ = map.Values);
}
[Fact]
public void Resize_ShouldStayCorrect_AcrossManyGrowths_AndReturnOldBuffers()
{
// Each Resize rents a doubled buffer and returns the old one to the pool.
// Driving many resizes from a tiny capacity stresses that the rent/return
// dance never corrupts the table (a returned buffer handed back out as the
// *next* rent must be re-cleared, which the type does).
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>(capacity: 2);
for (int i = 1; i <= 1000; i++)
map[i] = i * 3;
Assert.Equal(1000, map.Count);
for (int i = 1; i <= 1000; i++)
Assert.Equal(i * 3, map[i]);
}
[Fact]
public void RentedBuffer_OverProvisioning_DoesNotLeakGarbageIntoEnumeration()
{
// ArrayPool.Rent may hand back an array larger than requested; the tail
// beyond the logical size is never cleared and holds whatever the last
// tenant left. Enumeration / Count / ContainsValue must bound by the
// logical size, not array.Length, so that garbage never surfaces.
// Repeatedly building and disposing primes the pool with dirty buffers.
for (int round = 0; round < 5; round++)
{
using var primer = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>(64);
for (int i = 1; i <= 40; i++)
primer[i] = i;
}
using var map = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>(8);
map[100] = 1;
map[200] = 2;
int entries = 0;
foreach (var kvp in map)
{
Assert.True(kvp.Key == 100 || kvp.Key == 200, $"Unexpected key {kvp.Key} from rented tail.");
entries++;
}
Assert.Equal(2, entries);
Assert.Equal(2, map.Count);
Assert.False(map.ContainsValue(999));
}
[Fact]
public void ReferenceTypeValues_ShouldBeReleased_OnDispose()
{
// Reference-type buffers are cleared on return so the pool does not pin
// the values after disposal. We can't observe the pool directly, but we
// can confirm the round-trip works and dispose completes cleanly for a
// reference value type (the clear-on-return path).
var map = new PooledCelerityDictionary<string, object, StringFnV1AHasher>();
var payload = new object();
map["k"] = payload;
Assert.Same(payload, map["k"]);
var ex = Record.Exception(() => map.Dispose());
Assert.Null(ex);
}
private struct GuidIdentityHasher : IHashProvider<Guid>
{
public int Hash(Guid key) => key.GetHashCode();
}
}