Skip to content

Commit 666cada

Browse files
committed
test: update the mock to handle exclusive locks
1 parent 03851f5 commit 666cada

File tree

2 files changed

+149
-14
lines changed

2 files changed

+149
-14
lines changed

tests/setup.ts

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mocked } from 'jest-mock';
55
import { configure as mobxConfigure } from 'mobx';
66

77
import { ElectronFiddleMock } from './mocks/mocks';
8+
import { getOrCreateMapValue } from './utils';
89

910
enzymeConfigure({ adapter: new Adapter() });
1011

@@ -76,47 +77,159 @@ delete (window as any).localStorage;
7677
window.navigator = window.navigator ?? {};
7778
(window.navigator.clipboard as any) = {};
7879

80+
type MockLock = Lock & {
81+
abortController: AbortController;
82+
};
83+
7984
class FakeNavigatorLocks implements LockManager {
8085
locks = {
81-
held: new Set<Lock>(),
82-
pending: new Set<Lock>(),
86+
held: new Map<string, Map<LockMode, Set<MockLock>>>(),
8387
};
8488

8589
query = async () => {
8690
const result = {
87-
held: [...this.locks.held],
88-
pending: [...this.locks.pending],
91+
held: [...this.locks.held.values()].reduce((acc, item) => {
92+
acc.push(...[...item.get('exclusive')!.values()]);
93+
acc.push(...[...item.get('shared')!.values()]);
94+
95+
return acc;
96+
}, [] as MockLock[]),
8997
};
9098

9199
return result as LockManagerSnapshot;
92100
};
93101

94-
/**
95-
* WIP. Right now, this is a **very** naive mock that will just happily grant a shared lock when one is requested,
96-
* but I'll add some bookkeeping and expand it to cover the exclusive lock case as well.
97-
*
98-
* @TODO remove this comment
99-
*/
100102
request = (async (...args: Parameters<LockManager['request']>) => {
101103
const [
102104
name,
103105
options = {
106+
ifAvailable: false,
104107
mode: 'exclusive',
108+
steal: false,
105109
},
106110
cb,
107111
] = args;
108112

109-
const { mode } = options;
113+
const { ifAvailable, mode, steal } = options;
114+
115+
const lock = {
116+
name,
117+
mode,
118+
abortController: new AbortController(),
119+
} as MockLock;
120+
121+
const heldLocksWithSameName = getOrCreateMapValue(
122+
this.locks.held,
123+
name,
124+
new Map<LockMode, Set<MockLock>>(),
125+
);
126+
127+
const exclusiveLocksWithSameName = getOrCreateMapValue(
128+
heldLocksWithSameName,
129+
'exclusive',
130+
new Set<MockLock>(),
131+
);
110132

111-
const lock = { name, mode, cb } as Lock;
133+
const sharedLocksWithSameName = getOrCreateMapValue(
134+
heldLocksWithSameName,
135+
'shared',
136+
new Set<MockLock>(),
137+
);
112138

113139
if (mode === 'shared') {
114-
this.locks.held.add(lock);
140+
sharedLocksWithSameName.add(lock);
115141

116-
await cb(lock);
142+
try {
143+
await cb(lock);
144+
} finally {
145+
sharedLocksWithSameName.delete(lock);
146+
}
147+
148+
return;
149+
}
150+
151+
// exclusive lock
152+
153+
// no locks with this name -> grant an exclusive lock
154+
if (
155+
exclusiveLocksWithSameName.size === 0 &&
156+
sharedLocksWithSameName.size === 0
157+
) {
158+
exclusiveLocksWithSameName.add(lock);
159+
160+
try {
161+
await cb(lock);
162+
} finally {
163+
exclusiveLocksWithSameName.delete(lock);
164+
}
117165

118166
return;
119167
}
168+
169+
// steal any currently held locks
170+
if (steal) {
171+
for (const lock of sharedLocksWithSameName) {
172+
lock.abortController.abort();
173+
}
174+
175+
for (const lock of exclusiveLocksWithSameName) {
176+
lock.abortController.abort();
177+
}
178+
179+
sharedLocksWithSameName.clear();
180+
exclusiveLocksWithSameName.clear();
181+
182+
exclusiveLocksWithSameName.add(lock);
183+
184+
try {
185+
await cb(lock);
186+
} finally {
187+
exclusiveLocksWithSameName.delete(lock);
188+
}
189+
190+
return;
191+
}
192+
193+
// run the callback without waiting for the lock to be released
194+
if (ifAvailable) {
195+
// just run the callback without waiting for it
196+
cb(null);
197+
198+
return;
199+
}
200+
201+
// @TODO add the lock to the list of pending locks?
202+
203+
// it's an exclusive lock, so there's only one value
204+
const currentLock = exclusiveLocksWithSameName.values().next()
205+
.value as MockLock;
206+
207+
const { abortController: currentLockAbortController } = currentLock;
208+
209+
// wait for the current lock to be released
210+
await new Promise<void>((resolve, reject) => {
211+
currentLockAbortController.signal.onabort = () => resolve();
212+
213+
const { abortController: pendingLockAbortController } = lock;
214+
215+
// this allows the locking mechanism to release this lock
216+
pendingLockAbortController.signal.onabort = () => reject();
217+
});
218+
219+
// clear the exclusive locks
220+
exclusiveLocksWithSameName.clear();
221+
222+
// grant our lock
223+
exclusiveLocksWithSameName.add(lock);
224+
225+
try {
226+
// run the callback
227+
await cb(lock);
228+
} finally {
229+
exclusiveLocksWithSameName.delete(lock);
230+
}
231+
232+
return;
120233
}) as LockManager['request'];
121234
}
122235

tests/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,25 @@ export function emitEvent(type: FiddleEvent, ...args: any[]) {
147147
},
148148
);
149149
}
150+
151+
export function getOrCreateMapValue<T extends Map<unknown, unknown>>(
152+
map: T,
153+
key: MapKey<T>,
154+
fallbackValue: MapValue<T>,
155+
): MapValue<T> {
156+
if (!map.has(key)) {
157+
map.set(key, fallbackValue);
158+
159+
return fallbackValue;
160+
}
161+
162+
return map.get(key) as MapValue<T>;
163+
}
164+
165+
type MapKey<T extends Map<unknown, unknown>> = T extends Map<infer I, unknown>
166+
? I
167+
: never;
168+
169+
type MapValue<T extends Map<unknown, unknown>> = T extends Map<unknown, infer I>
170+
? I
171+
: never;

0 commit comments

Comments
 (0)