aboutsummaryrefslogtreecommitdiffstats
path: root/Tests/Test_QtVsTools.Core/Test_Concurrent.cs
blob: 0ed5581993e391f89820927af4e7c19582579c5e (plain)
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
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace QtVsTools.Test.Core
{
    [TestClass]
    public class Test_ConcurrentTests
    {
        [TestMethod]
        public async Task Test_SingleHolderLock_AllowsOnlyOneThread()
        {
            // We'll track how many threads are "inside" the protected section
            var inCritical = 0;
            var maxConcurrent = 0;

            // This action tries to acquire the lock, increment 'inCritical', sleep, and release.
            async Task LockWorkAsync(string resource)
            {
                var gotLock = await Synchronized.GetAsync(resource);
                Assert.IsTrue(gotLock, "Should acquire lock with no timeout.");

                // We are "in critical section" now
                var newCount = Interlocked.Increment(ref inCritical);
                // Check if we've exceeded previous concurrency
                var snapshot = Math.Max(newCount, maxConcurrent);
                Interlocked.Exchange(ref maxConcurrent, snapshot);

                // Simulate some work
                Thread.Sleep(200);

                // Done, decrement
                Interlocked.Decrement(ref inCritical);

                // Release so next thread can proceed
                Synchronized.Release(resource);
            }

            // We'll run 3 parallel tasks on the same resource
            const string resourceName = "SingleHolderTest";

            var t1 = Task.Run(() => LockWorkAsync(resourceName));
            var t2 = Task.Run(() => LockWorkAsync(resourceName));
            var t3 = Task.Run(() => LockWorkAsync(resourceName));

            await Task.WhenAll(t1, t2, t3);

            // If single-holder logic is correct, maxConcurrent should never exceed 1.
            Assert.IsTrue(maxConcurrent <= 1,
                "Single-holder lock should never allow more than 1 concurrent holder.");

            // Cleanup
            Synchronized.Free(resourceName);
        }

        [TestMethod]
        public void Test_LockTimesOut_IfNotReleased()
        {
            const string resourceName = "TimeoutTest";

            // Acquire and do NOT release
            var lockAcquired = Synchronized.Get(resourceName);
            Assert.IsTrue(lockAcquired, "First call should acquire successfully.");

            // Try to get it again with a small timeout
            var secondAcquired = Synchronized.Get(resourceName, 100);
            // Because we never released, this call should time out and return false
            Assert.IsFalse(secondAcquired,
                "Should fail to acquire lock due to timeout and no release.");

            // Release again after reacquiring, then free
            Synchronized.Release(resourceName);
            Synchronized.Free(resourceName);
        }

        [TestMethod]
        public void Test_LockRelease_UnblocksWaitingThread()
        {
            const string resourceName = "ReleaseUnblockTest";

            // Acquire in the main thread, then we'll have a second thread that waits
            var firstAcquired = Synchronized.Get(resourceName);
            Assert.IsTrue(firstAcquired, "First call should acquire lock.");

            var secondThreadAcquired = false;
            var second = new Thread(() =>
            {
                // This should block until we release
                var gotIt = Synchronized.Get(resourceName, 2000);
                secondThreadAcquired = gotIt;
                if (gotIt)
                    Synchronized.Release(resourceName);
            });
            second.Start();

            // Sleep briefly to ensure second thread is blocked
            Thread.Sleep(500);

            // Now release from main thread
            Synchronized.Release(resourceName);

            // Join the second thread, expect it to have eventually acquired the lock
            second.Join();

            Assert.IsTrue(secondThreadAcquired,
                "Second thread should have acquired lock after we released.");

            Synchronized.Free(resourceName);
        }

        [TestMethod]
        public async Task Test_AsyncLock_CanBeAcquiredAndReleased()
        {
            const string resourceName = "AsyncLockTest";

            // Acquire lock asynchronously
            var firstAcquired = await Synchronized.GetAsync(resourceName, timeout: 1000);
            Assert.IsTrue(firstAcquired, "Async lock acquire should succeed initially.");

            // Try to re-acquire on a different task without releasing. This should time out
            // because we haven't released yet.
            var secondAcquired = await Synchronized.GetAsync(resourceName, 500);
            Assert.IsFalse(secondAcquired,
                "Second async call should time out since we never released.");

            // Now release so we can reacquire
            Synchronized.Release(resourceName);

            // This time, we should succeed quickly
            var thirdAcquired = await Synchronized.GetAsync(resourceName, 500);
            Assert.IsTrue(thirdAcquired, "After release, a new async call should succeed.");

            // Release again after reacquiring, then free
            Synchronized.Release(resourceName);
            Synchronized.Free(resourceName);
        }

        [TestMethod]
        public void Test_Free_RemovesLockResource()
        {
            // Acquire a resource
            const string resourceName = "TestFreeResource";
            var acquired = Synchronized.Get(resourceName, 1000);
            Assert.IsTrue(acquired, "Should be able to acquire new resource lock.");

            // Release & then Free it
            Synchronized.Release(resourceName);
            Synchronized.Free(resourceName);

            // We should be able to reacquire with a new lock object because 'Free' removed the old
            // entry.
            var reacquired = Synchronized.Get(resourceName, 500);
            Assert.IsTrue(reacquired,
                "After Free, reacquiring should succeed on a new resource lock.");

            // Release again after reacquiring, then free
            Synchronized.Release(resourceName);
            Synchronized.Free(resourceName);
        }

        [TestMethod]
        public void Test_AcquireInThreadA_ReleaseFromMainThread_ShouldNotDeadlock()
        {
            const string resourceName = "CrossThreadLockTest";

            var readyToRelease = new AutoResetEvent(false);
            var acquiredSignal = new AutoResetEvent(false);

            // Thread A: Acquire the resource, signal that it is acquired, then wait until main
            // thread signals "ok, you can proceed." But we won't actually release from A.
            var threadA = new Thread(() =>
            {
                var lockAcquired = Synchronized.Get(resourceName, 2000);
                Assert.IsTrue(lockAcquired, "Thread A should acquire lock successfully.");

                // Signal main thread that we have acquired
                acquiredSignal.Set();

                // Wait here until main thread signals we can exit (meaning it tried releasing)
                readyToRelease.WaitOne();

                // We do *not* release here. We'll rely on thread B to do so.
            });
            threadA.Start();

            // Wait until Thread A signals that it has acquired the resource
            Assert.IsTrue(acquiredSignal.WaitOne(2000), "Thread A didn't acquire in time.");

            // Now, from the *main thread*, try to release the resource that thread A acquired.
            // This tests if we can cross-thread release.
            Synchronized.Release(resourceName);

            // Now let thread A finish, it should simply set IsLocked = false, so it won't
            // deadlock.
            readyToRelease.Set();
            threadA.Join();

            // Verify we can reacquire it now that we've released cross-thread.
            var reacquired = Synchronized.Get(resourceName, 500);
            Assert.IsTrue(reacquired, "Resource should be free now after cross-thread release.");

            // Release again after reacquiring, then free
            Synchronized.Release(resourceName);
            Synchronized.Free(resourceName);
        }

        [TestMethod]
        public async Task Test_AcquireInBackgroundThread_ReleaseFromMainThread_ShouldNotDeadlock()
        {
            // The resource name for our test
            const string resourceName = "CrossThreadAsyncTest";

            // Acquire in a background thread (inside GetAsync). Since GetAsync internally does
            // Task.Run(() => Get(...)), that blocking call happens on a pool thread, not the main
            // test thread.
            var acquired = await Synchronized.GetAsync(resourceName, timeout: 2000);
            Assert.IsTrue(acquired, "Should acquire resource via async call without timing out.");

            // The resource is now locked, but the background thread has finished because it only
            // needed to lock briefly to set IsLocked = true. We never released from that
            // background thread.

            //  Now *this thread* (the main MSTest thread) attempts to Release.
            Synchronized.Release(resourceName);

            // Verify we can reacquire it now that we've released cross-thread.
            var reacquired = await Synchronized.GetAsync(resourceName, 500);
            Assert.IsTrue(reacquired,
                "Resource should be free after cross-thread Release from the main thread.");

            // Release again after reacquiring, then free
            Synchronized.Release(resourceName);
            Synchronized.Free(resourceName);
        }
    }
}