Skip to content

Commit a6333af

Browse files
committed
Port unit tests for synchronization library from RecRoom codebase
1 parent 583b063 commit a6333af

File tree

4 files changed

+325
-0
lines changed

4 files changed

+325
-0
lines changed

src/ImageSharp.Web/Synchronization/RefCountedConcurrentDictionary.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5+
using System.Collections;
56
using System.Collections.Concurrent;
67
using System.Collections.Generic;
78

@@ -160,6 +161,55 @@ public void Release(TKey key)
160161
}
161162
}
162163

164+
/// <summary>
165+
/// Get an enumeration over the contents of the dictionary for testing/debugging purposes
166+
/// </summary>
167+
internal IEnumerable<(TKey Key, TValue Value, int RefCount)> DebugGetContents() => new RefCountedDictionaryEnumerable(this);
168+
169+
/// <summary>
170+
/// Internal class used for testing/debugging purposes
171+
/// </summary>
172+
private class RefCountedDictionaryEnumerable : IEnumerable<(TKey Key, TValue Value, int RefCount)>
173+
{
174+
private readonly RefCountedConcurrentDictionary<TKey, TValue> dictionary;
175+
176+
internal RefCountedDictionaryEnumerable(RefCountedConcurrentDictionary<TKey, TValue> dictionary)
177+
=> this.dictionary = dictionary;
178+
179+
public IEnumerator<(TKey Key, TValue Value, int RefCount)> GetEnumerator()
180+
=> new RefCountedDictionaryEnumerator(this.dictionary);
181+
182+
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
183+
}
184+
185+
/// <summary>
186+
/// Internal class used for testing/debugging purposes
187+
/// </summary>
188+
private class RefCountedDictionaryEnumerator : IEnumerator<(TKey Key, TValue Value, int RefCount)>
189+
{
190+
private readonly IEnumerator<KeyValuePair<TKey, RefCountedValue>> enumerator;
191+
192+
public RefCountedDictionaryEnumerator(RefCountedConcurrentDictionary<TKey, TValue> dictionary)
193+
=> this.enumerator = dictionary.dictionary.GetEnumerator();
194+
195+
public (TKey Key, TValue Value, int RefCount) Current
196+
{
197+
get
198+
{
199+
KeyValuePair<TKey, RefCountedConcurrentDictionary<TKey, TValue>.RefCountedValue> keyValuePair = this.enumerator.Current;
200+
return (keyValuePair.Key, keyValuePair.Value.Value, keyValuePair.Value.RefCount);
201+
}
202+
}
203+
204+
object IEnumerator.Current => this.Current;
205+
206+
public void Dispose() => this.enumerator.Dispose();
207+
208+
public bool MoveNext() => this.enumerator.MoveNext();
209+
210+
public void Reset() => this.enumerator.Reset();
211+
}
212+
163213
/// <summary>
164214
/// Simple immutable tuple that combines a <typeparamref name="TValue"/> instance with a ref count integer.
165215
/// </summary>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Threading.Tasks;
5+
using SixLabors.ImageSharp.Web.Synchronization;
6+
using Xunit;
7+
8+
namespace SixLabors.ImageSharp.Web.Tests.Synchronization
9+
{
10+
public class AsyncLockTests
11+
{
12+
private readonly AsyncLock l = new();
13+
14+
[Fact]
15+
public async Task OneAtATime()
16+
{
17+
Task<AsyncLock.Releaser> first = this.l.LockAsync();
18+
Assert.True(first.IsCompletedSuccessfully);
19+
20+
Task<AsyncLock.Releaser> second = this.l.LockAsync();
21+
Assert.False(second.IsCompleted);
22+
23+
// Release first hold on the lock and then await the second task to confirm it completes.
24+
first.Result.Dispose();
25+
26+
// Await the second task to make sure we get the lock. The timeout specified in the [Fact] above will prevent this from running forever.
27+
await second;
28+
}
29+
}
30+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Threading.Tasks;
5+
using SixLabors.ImageSharp.Web.Synchronization;
6+
using Xunit;
7+
8+
namespace SixLabors.ImageSharp.Web.Tests.Synchronization
9+
{
10+
public class AsyncReaderWriterLockTests
11+
{
12+
private readonly AsyncReaderWriterLock l = new();
13+
14+
[Fact]
15+
public void OneWriterAtATime()
16+
{
17+
Task<AsyncReaderWriterLock.Releaser> first = this.l.WriterLockAsync();
18+
Assert.True(first.IsCompletedSuccessfully);
19+
20+
Task<AsyncReaderWriterLock.Releaser> second = this.l.WriterLockAsync();
21+
Assert.False(second.IsCompleted);
22+
23+
first.Result.Dispose();
24+
Assert.True(second.IsCompletedSuccessfully);
25+
}
26+
27+
[Fact]
28+
public void WriterBlocksReaders()
29+
{
30+
Task<AsyncReaderWriterLock.Releaser> first = this.l.WriterLockAsync();
31+
Assert.True(first.IsCompletedSuccessfully);
32+
33+
Task<AsyncReaderWriterLock.Releaser> second = this.l.ReaderLockAsync();
34+
Assert.False(second.IsCompleted);
35+
36+
first.Result.Dispose();
37+
Assert.True(second.IsCompletedSuccessfully);
38+
}
39+
40+
[Fact]
41+
public void WaitingWriterBlocksReaders()
42+
{
43+
Task<AsyncReaderWriterLock.Releaser> first = this.l.ReaderLockAsync();
44+
Assert.True(first.IsCompletedSuccessfully);
45+
46+
Task<AsyncReaderWriterLock.Releaser> second = this.l.WriterLockAsync();
47+
Assert.False(second.IsCompleted);
48+
49+
Task<AsyncReaderWriterLock.Releaser> third = this.l.ReaderLockAsync();
50+
Assert.False(third.IsCompleted);
51+
52+
first.Result.Dispose();
53+
Assert.True(second.IsCompletedSuccessfully);
54+
Assert.False(third.IsCompleted);
55+
56+
second.Result.Dispose();
57+
Assert.True(third.IsCompletedSuccessfully);
58+
}
59+
60+
[Fact]
61+
public void MultipleReadersAtOnce()
62+
{
63+
Task<AsyncReaderWriterLock.Releaser> first = this.l.ReaderLockAsync();
64+
Assert.True(first.IsCompletedSuccessfully);
65+
66+
Task<AsyncReaderWriterLock.Releaser> second = this.l.ReaderLockAsync();
67+
Assert.True(second.IsCompletedSuccessfully);
68+
69+
Task<AsyncReaderWriterLock.Releaser> third = this.l.ReaderLockAsync();
70+
Assert.True(third.IsCompletedSuccessfully);
71+
}
72+
73+
[Fact]
74+
public void AllWaitingReadersReleasedConcurrently()
75+
{
76+
Task<AsyncReaderWriterLock.Releaser> writer = this.l.WriterLockAsync();
77+
Assert.True(writer.IsCompletedSuccessfully);
78+
79+
Task<AsyncReaderWriterLock.Releaser> reader1 = this.l.ReaderLockAsync();
80+
Assert.False(reader1.IsCompleted);
81+
82+
Task<AsyncReaderWriterLock.Releaser> reader2 = this.l.ReaderLockAsync();
83+
Assert.False(reader2.IsCompleted);
84+
85+
Task<AsyncReaderWriterLock.Releaser> reader3 = this.l.ReaderLockAsync();
86+
Assert.False(reader3.IsCompleted);
87+
88+
writer.Result.Dispose();
89+
Assert.True(reader1.IsCompletedSuccessfully);
90+
Assert.True(reader2.IsCompletedSuccessfully);
91+
Assert.True(reader3.IsCompletedSuccessfully);
92+
}
93+
}
94+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using SixLabors.ImageSharp.Web.Synchronization;
9+
using Xunit;
10+
11+
namespace SixLabors.ImageSharp.Web.Tests.Synchronization
12+
{
13+
public class RefCountedConcurrentDictionaryTests
14+
{
15+
private readonly ConcurrentBag<string> released;
16+
private readonly RefCountedConcurrentDictionary<string, string> dictionary;
17+
18+
public RefCountedConcurrentDictionaryTests()
19+
{
20+
this.released = new ConcurrentBag<string>();
21+
this.dictionary = new RefCountedConcurrentDictionary<string, string>(
22+
valueFactory: this.ValueForKey,
23+
valueReleaser: value => this.released.Add(value));
24+
}
25+
26+
[Fact]
27+
public void AddThenRelease()
28+
{
29+
this.dictionary.Get("test");
30+
this.ValidateDictionary(("test", 1));
31+
this.ValidateReleased();
32+
33+
this.dictionary.Release("test");
34+
this.ValidateDictionary();
35+
this.ValidateReleased("test");
36+
}
37+
38+
[Fact]
39+
public void AddTwiceThenReleaseTwice()
40+
{
41+
this.dictionary.Get("test");
42+
this.ValidateDictionary(("test", 1));
43+
this.ValidateReleased();
44+
45+
this.dictionary.Get("test");
46+
this.ValidateDictionary(("test", 2));
47+
this.ValidateReleased();
48+
49+
this.dictionary.Release("test");
50+
this.ValidateDictionary(("test", 1));
51+
this.ValidateReleased();
52+
53+
this.dictionary.Release("test");
54+
this.ValidateDictionary();
55+
this.ValidateReleased("test");
56+
}
57+
58+
[Fact]
59+
public void AddAndReleaseABunchOfKeys()
60+
{
61+
this.dictionary.Get("a");
62+
this.dictionary.Get("b");
63+
this.dictionary.Get("c");
64+
this.dictionary.Get("a");
65+
this.dictionary.Release("b");
66+
this.dictionary.Get("b");
67+
this.ValidateDictionary(("a", 2), ("b", 1), ("c", 1));
68+
this.ValidateReleased("b");
69+
70+
this.dictionary.Release("a");
71+
this.dictionary.Release("b");
72+
this.dictionary.Release("c");
73+
this.ValidateDictionary(("a", 1));
74+
this.ValidateReleased("b", "b", "c");
75+
76+
this.dictionary.Release("a");
77+
this.ValidateDictionary();
78+
this.ValidateReleased("a", "b", "b", "c");
79+
}
80+
81+
[Fact]
82+
public async Task StressTest()
83+
{
84+
string[] keys = new string[] { "a", "b", "c", "d", "e", "f", "g", "h" };
85+
86+
async Task Worker(int workerIndex)
87+
{
88+
var random = new Random(workerIndex);
89+
for (int i = 0; i < 1000; i++)
90+
{
91+
string key = keys[random.Next(0, keys.Length)];
92+
this.dictionary.Get(key);
93+
await Task.Delay(random.Next(0, 2));
94+
this.dictionary.Release(key);
95+
}
96+
}
97+
98+
await Task.WhenAll(Enumerable.Range(0, 1000).Select(Worker));
99+
100+
Assert.Empty(this.dictionary.DebugGetContents());
101+
}
102+
103+
[Fact]
104+
public void ReleaseNonExistentThrows()
105+
=> Assert.Throws<InvalidOperationException>(() => this.dictionary.Release("test"));
106+
107+
[Fact]
108+
public void DoubleReleaseThrows()
109+
{
110+
this.dictionary.Get("test");
111+
this.dictionary.Release("test");
112+
Assert.Throws<InvalidOperationException>(() => this.dictionary.Release("test"));
113+
}
114+
115+
[Fact]
116+
public void ValueFactoryIsRequired()
117+
=> Assert.Throws<ArgumentNullException>("valueFactory", () => new RefCountedConcurrentDictionary<string, string>(null!, null));
118+
119+
private string ValueForKey(string key) => $"{key}.value";
120+
121+
/// <summary>
122+
/// Validate that the dictionary contains precisely the specified key/value/refcount tuples.
123+
/// </summary>
124+
private void ValidateDictionary(params (string Key, int RefCount)[] expectedValues)
125+
{
126+
Action<(string, string, int)> CreateElementInspector((string Key, int RefCount) expected)
127+
=> ((string Key, string Value, int RefCount) actual) =>
128+
{
129+
Assert.Equal(expected.Key, actual.Key);
130+
Assert.Equal(this.ValueForKey(expected.Key), actual.Value);
131+
Assert.Equal(expected.RefCount, actual.RefCount);
132+
};
133+
134+
Assert.Collection(
135+
collection: this.dictionary.DebugGetContents().OrderBy(v => v.Key),
136+
elementInspectors: expectedValues.OrderBy(v => v.Key).Select(CreateElementInspector).ToArray());
137+
}
138+
139+
/// <summary>
140+
/// Validate that the specific values were released.
141+
/// </summary>
142+
private void ValidateReleased(params string[] expectedValues)
143+
{
144+
Action<string> CreateElementInspector(string expected) => (string actual) => Assert.Equal(this.ValueForKey(expected), actual);
145+
146+
Assert.Collection(
147+
collection: this.released.OrderBy(v => v),
148+
elementInspectors: expectedValues.OrderBy(v => v).Select(CreateElementInspector).ToArray());
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)