|
1 | | -#include <QAtomicInt> |
2 | | -#include <QElapsedTimer> |
3 | 1 | #include <QSemaphore> |
4 | | -#include <QThread> |
5 | 2 | #include <QtTest> |
6 | 3 |
|
7 | 4 | #include <thread> |
|
12 | 9 |
|
13 | 10 | namespace tests { |
14 | 11 |
|
15 | | -// Fake ISyncNotebookService — sleeps in each phase so we can observe gate |
16 | | -// hold windows from the outside via contention timing. |
| 12 | +// Hold each phase until the test has observed whether another save could acquire |
| 13 | +// the notebook gate. Scheduling delays must not change the observation window. |
17 | 14 | class FakeSyncNotebookService : public vnotex::ISyncNotebookService { |
18 | 15 | public: |
19 | 16 | VxCoreError syncStageOnly(const QString &p_notebookId, |
20 | 17 | VxCoreSyncCancellation *p_cancellationToken, |
21 | 18 | bool *p_didCommit) override { |
22 | 19 | Q_UNUSED(p_notebookId); |
23 | 20 | Q_UNUSED(p_cancellationToken); |
24 | | - m_stageEntered.storeRelaxed(1); |
25 | | - QThread::msleep(50); |
| 21 | + m_stageEntered.release(); |
| 22 | + m_continueStage.acquire(); |
26 | 23 | if (p_didCommit) { |
27 | 24 | *p_didCommit = true; |
28 | 25 | } |
29 | | - m_stageExited.storeRelaxed(1); |
30 | 26 | return VXCORE_OK; |
31 | 27 | } |
32 | 28 |
|
33 | 29 | VxCoreError syncNetworkPhase(const QString &p_notebookId, |
34 | 30 | VxCoreSyncCancellation *p_cancellationToken) override { |
35 | 31 | Q_UNUSED(p_notebookId); |
36 | 32 | Q_UNUSED(p_cancellationToken); |
37 | | - m_networkEntered.storeRelaxed(1); |
38 | | - QThread::msleep(200); |
39 | | - m_networkExited.storeRelaxed(1); |
| 33 | + m_networkEntered.release(); |
| 34 | + m_continueNetwork.acquire(); |
40 | 35 | return VXCORE_OK; |
41 | 36 | } |
42 | 37 |
|
43 | | - QAtomicInt m_stageEntered{0}; |
44 | | - QAtomicInt m_stageExited{0}; |
45 | | - QAtomicInt m_networkEntered{0}; |
46 | | - QAtomicInt m_networkExited{0}; |
| 38 | + QSemaphore m_stageEntered; |
| 39 | + QSemaphore m_continueStage; |
| 40 | + QSemaphore m_networkEntered; |
| 41 | + QSemaphore m_continueNetwork; |
47 | 42 | }; |
48 | 43 |
|
49 | 44 | class TestSyncOpsGateRelease : public QObject { |
50 | 45 | Q_OBJECT |
51 | 46 |
|
52 | 47 | private slots: |
53 | 48 | void testGateReleasedBeforeNetworkPhase(); |
54 | | - void testStagedPatternCompletesSuccessfully(); |
55 | 49 | }; |
56 | 50 |
|
57 | 51 | void TestSyncOpsGateRelease::testGateReleasedBeforeNetworkPhase() { |
58 | 52 | FakeSyncNotebookService fake; |
59 | 53 | vnotex::NotebookIoGate gate; |
60 | 54 | const QString notebookId = QStringLiteral("nb-1"); |
61 | | - |
62 | | - QSemaphore finishedSem(0); |
63 | | - QAtomicInt finishedCode{-1}; |
64 | | - auto onFinished = [&](VxCoreError code) { |
65 | | - finishedCode.storeRelaxed(static_cast<int>(code)); |
66 | | - finishedSem.release(); |
67 | | - }; |
68 | | - |
69 | | - // 1. Hold the gate on the main thread. |
70 | | - auto mainLock = std::make_unique<vnotex::NotebookIoGate::ScopedLock>(gate, notebookId); |
71 | | - |
72 | | - // 2. Dispatch triggerSync on a worker. It will block in stage acquiring the gate. |
73 | | - std::thread worker( |
74 | | - [&]() { vnotex::SyncOps::triggerSync(&fake, notebookId, nullptr, onFinished, &gate); }); |
75 | | - |
76 | | - // 3. Sleep so the worker is definitely blocked in gate acquire. |
77 | | - QThread::msleep(100); |
78 | | - QVERIFY2(fake.m_stageEntered.loadRelaxed() == 0, |
79 | | - "Worker should still be blocked on gate acquire (stage not entered)"); |
80 | | - |
81 | | - // 4. Release main's lock so worker proceeds. |
82 | | - QElapsedTimer sinceRelease; |
83 | | - sinceRelease.start(); |
84 | | - mainLock.reset(); |
85 | | - |
86 | | - // 5. Wait long enough for worker to enter (and likely exit) stage, then |
87 | | - // be inside the network phase (network sleeps 200ms). |
88 | | - // Stage = 50ms. After ~120ms post-release we should be solidly in the |
89 | | - // network phase. |
90 | | - QThread::msleep(120); |
91 | | - QVERIFY2(fake.m_networkEntered.loadRelaxed() == 1, "Worker should be in network phase by now"); |
92 | | - QVERIFY2(fake.m_networkExited.loadRelaxed() == 0, |
93 | | - "Worker should NOT have finished network phase yet (sleeps 200ms)"); |
94 | | - |
95 | | - // 6. KEY ASSERTION: try to re-acquire the gate from the main thread. |
96 | | - // If the gate were still held during network phase, this would block |
97 | | - // ~80ms (remaining network time). If released, it's essentially instant. |
98 | | - QElapsedTimer acquireTimer; |
99 | | - acquireTimer.start(); |
100 | | - { |
101 | | - vnotex::NotebookIoGate::ScopedLock probe(gate, notebookId); |
102 | | - const qint64 acquireMs = acquireTimer.elapsed(); |
103 | | - QVERIFY2(acquireMs < 30, |
104 | | - qPrintable(QStringLiteral("Gate re-acquire took %1ms; expected <30ms " |
105 | | - "(gate should be released during network phase)") |
106 | | - .arg(acquireMs))); |
| 55 | + VxCoreError finishedCode = VXCORE_ERR_UNKNOWN; |
| 56 | + int finishedCount = 0; |
| 57 | + std::thread worker([&]() { |
| 58 | + vnotex::SyncOps::triggerSync( |
| 59 | + &fake, notebookId, nullptr, |
| 60 | + [&](VxCoreError p_code) { |
| 61 | + finishedCode = p_code; |
| 62 | + ++finishedCount; |
| 63 | + }, |
| 64 | + &gate); |
| 65 | + }); |
| 66 | + |
| 67 | + const bool stageEntered = fake.m_stageEntered.tryAcquire(1, 5000); |
| 68 | + bool gateHeldDuringStage = false; |
| 69 | + if (stageEntered) { |
| 70 | + vnotex::NotebookIoGate::ScopedTryLock probe(gate, notebookId, 0); |
| 71 | + gateHeldDuringStage = !probe.isLocked(); |
107 | 72 | } |
| 73 | + fake.m_continueStage.release(); |
108 | 74 |
|
109 | | - // 7. Wait for worker completion. |
110 | | - QVERIFY(finishedSem.tryAcquire(1, 5000)); |
111 | | - QCOMPARE(static_cast<VxCoreError>(finishedCode.loadRelaxed()), VXCORE_OK); |
112 | | - worker.join(); |
113 | | -} |
114 | | - |
115 | | -void TestSyncOpsGateRelease::testStagedPatternCompletesSuccessfully() { |
116 | | - FakeSyncNotebookService fake; |
117 | | - vnotex::NotebookIoGate gate; |
118 | | - const QString notebookId = QStringLiteral("nb-2"); |
119 | | - |
120 | | - QSemaphore finishedSem(0); |
121 | | - QAtomicInt finishedCode{-1}; |
122 | | - auto onFinished = [&](VxCoreError code) { |
123 | | - finishedCode.storeRelaxed(static_cast<int>(code)); |
124 | | - finishedSem.release(); |
125 | | - }; |
126 | | - |
127 | | - QElapsedTimer timer; |
128 | | - timer.start(); |
129 | | - std::thread worker( |
130 | | - [&]() { vnotex::SyncOps::triggerSync(&fake, notebookId, nullptr, onFinished, &gate); }); |
| 75 | + const bool networkEntered = fake.m_networkEntered.tryAcquire(1, 5000); |
| 76 | + bool gateReleasedDuringNetwork = false; |
| 77 | + if (networkEntered) { |
| 78 | + vnotex::NotebookIoGate::ScopedTryLock probe(gate, notebookId, 0); |
| 79 | + gateReleasedDuringNetwork = probe.isLocked(); |
| 80 | + } |
| 81 | + fake.m_continueNetwork.release(); |
131 | 82 |
|
132 | | - QVERIFY(finishedSem.tryAcquire(1, 5000)); |
133 | | - const qint64 elapsed = timer.elapsed(); |
| 83 | + // Join before asserting so a failed observation cannot destroy a joinable |
| 84 | + // std::thread and abort the test process instead of reporting the failure. |
134 | 85 | worker.join(); |
135 | | - |
136 | | - QCOMPARE(static_cast<VxCoreError>(finishedCode.loadRelaxed()), VXCORE_OK); |
137 | | - QCOMPARE(fake.m_stageExited.loadRelaxed(), 1); |
138 | | - QCOMPARE(fake.m_networkExited.loadRelaxed(), 1); |
139 | | - |
140 | | - // Stage 50ms + network 200ms = ~250ms; allow generous CI headroom. |
141 | | - QVERIFY2(elapsed >= 240, qPrintable(QStringLiteral("Expected >=240ms, got %1ms").arg(elapsed))); |
142 | | - QVERIFY2(elapsed < 600, qPrintable(QStringLiteral("Expected <600ms, got %1ms").arg(elapsed))); |
| 86 | + QVERIFY2(stageEntered, "Worker did not enter the staging phase"); |
| 87 | + QVERIFY2(gateHeldDuringStage, "Staging must exclude concurrent notebook saves"); |
| 88 | + QVERIFY2(networkEntered, "Worker did not enter the network phase"); |
| 89 | + QVERIFY2(gateReleasedDuringNetwork, "Network work must not block notebook saves"); |
| 90 | + QCOMPARE(finishedCode, VXCORE_OK); |
| 91 | + QCOMPARE(finishedCount, 1); |
143 | 92 | } |
144 | 93 |
|
145 | 94 | } // namespace tests |
|
0 commit comments