1
1
package com.onesignal.core.internal.operations
2
2
3
3
import com.onesignal.common.threading.Waiter
4
+ import com.onesignal.common.threading.WaiterWithValue
4
5
import com.onesignal.core.internal.operations.impl.OperationModelStore
5
6
import com.onesignal.core.internal.operations.impl.OperationRepo
7
+ import com.onesignal.core.internal.operations.impl.OperationRepo.OperationQueueItem
6
8
import com.onesignal.core.internal.time.impl.Time
7
9
import com.onesignal.debug.LogLevel
8
10
import com.onesignal.debug.internal.logging.Logging
@@ -11,6 +13,7 @@ import io.kotest.core.spec.style.FunSpec
11
13
import io.kotest.matchers.shouldBe
12
14
import io.mockk.CapturingSlot
13
15
import io.mockk.coEvery
16
+ import io.mockk.coVerify
14
17
import io.mockk.coVerifyOrder
15
18
import io.mockk.every
16
19
import io.mockk.just
@@ -43,17 +46,16 @@ private class Mocks {
43
46
mockExecutor
44
47
}
45
48
46
- val operationRepo: OperationRepo =
47
- run {
48
- spyk(
49
- OperationRepo (
50
- listOf (executor),
51
- operationModelStore,
52
- configModelStore,
53
- Time (),
54
- ),
55
- )
56
- }
49
+ val operationRepo: OperationRepo by lazy {
50
+ spyk(
51
+ OperationRepo (
52
+ listOf (executor),
53
+ operationModelStore,
54
+ configModelStore,
55
+ Time (),
56
+ ),
57
+ )
58
+ }
57
59
}
58
60
59
61
class OperationRepoTests : FunSpec ({
@@ -102,7 +104,7 @@ class OperationRepoTests : FunSpec({
102
104
// 1st: gets the operation
103
105
// 2nd: will be empty
104
106
// 3rd: shouldn't be called, loop should be waiting on next operation
105
- mocks.operationRepo.getNextOps()
107
+ mocks.operationRepo.getNextOps(withArg { Any () } )
106
108
}
107
109
}
108
110
@@ -135,7 +137,8 @@ class OperationRepoTests : FunSpec({
135
137
test("enqueue operation executes and is removed when executed after retry") {
136
138
// Given
137
139
val mocks = Mocks ()
138
- coEvery { mocks.operationRepo.delayBeforeRetry(any()) } just runs
140
+ val opRepo = mocks.operationRepo
141
+ coEvery { opRepo.delayBeforeRetry(any()) } just runs
139
142
coEvery {
140
143
mocks.executor.execute(any())
141
144
} returns ExecutionResponse (ExecutionResult .FAIL_RETRY ) andThen ExecutionResponse (ExecutionResult .SUCCESS )
@@ -144,8 +147,8 @@ class OperationRepoTests : FunSpec({
144
147
val operation = mockOperation(operationIdSlot = operationIdSlot)
145
148
146
149
// When
147
- mocks.operationRepo .start()
148
- val response = mocks.operationRepo .enqueueAndWait(operation)
150
+ opRepo .start()
151
+ val response = opRepo .enqueueAndWait(operation)
149
152
150
153
// Then
151
154
response shouldBe true
@@ -158,7 +161,7 @@ class OperationRepoTests : FunSpec({
158
161
it[0] shouldBe operation
159
162
},
160
163
)
161
- mocks.operationRepo .delayBeforeRetry(1)
164
+ opRepo .delayBeforeRetry(1)
162
165
mocks.executor.execute(
163
166
withArg {
164
167
it.count() shouldBe 1
@@ -370,6 +373,118 @@ class OperationRepoTests : FunSpec({
370
373
}
371
374
response shouldBe true
372
375
}
376
+
377
+ // This ensures a misbehaving app can't add operations (such as addTag())
378
+ // in a tight loop and cause a number of back-to-back operations without delay.
379
+ test("operations enqueued while repo is executing should be executed only after the next opRepoExecutionInterval") {
380
+ // Given
381
+ val mocks = Mocks ()
382
+ mocks.configModelStore.model.opRepoExecutionInterval = 100
383
+ val enqueueAndWaitMaxTime = mocks.configModelStore.model.opRepoExecutionInterval / 2
384
+ val opRepo = mocks.operationRepo
385
+
386
+ val executeOperationsCall = mockExecuteOperations(opRepo)
387
+
388
+ // When
389
+ opRepo.start()
390
+ opRepo.enqueue(mockOperationNonGroupable())
391
+ executeOperationsCall.waitForWake()
392
+ val secondEnqueueResult =
393
+ withTimeoutOrNull(enqueueAndWaitMaxTime) {
394
+ opRepo.enqueueAndWait(mockOperationNonGroupable())
395
+ }
396
+
397
+ // Then
398
+ secondEnqueueResult shouldBe null
399
+ coVerify(exactly = 1) {
400
+ opRepo.executeOperations(any())
401
+ }
402
+ }
403
+
404
+ // This ensures there are no off-by-one errors with the same scenario as above, but on a 2nd
405
+ // pass of OperationRepo
406
+ test("operations enqueued while repo is executing should be executed only after the next opRepoExecutionInterval, 2nd pass") {
407
+ // Given
408
+ val mocks = Mocks ()
409
+ mocks.configModelStore.model.opRepoExecutionInterval = 100
410
+ val enqueueAndWaitMaxTime = mocks.configModelStore.model.opRepoExecutionInterval / 2
411
+ val opRepo = mocks.operationRepo
412
+
413
+ val executeOperationsCall = mockExecuteOperations(opRepo)
414
+
415
+ // When
416
+ opRepo.start()
417
+ opRepo.enqueue(mockOperationNonGroupable())
418
+ executeOperationsCall.waitForWake()
419
+ opRepo.enqueueAndWait(mockOperationNonGroupable())
420
+ val thirdEnqueueResult =
421
+ withTimeoutOrNull(enqueueAndWaitMaxTime) {
422
+ opRepo.enqueueAndWait(mockOperationNonGroupable())
423
+ }
424
+
425
+ // Then
426
+ thirdEnqueueResult shouldBe null
427
+ coVerify(exactly = 2) {
428
+ opRepo.executeOperations(any())
429
+ }
430
+ }
431
+
432
+ // Starting operations are operations we didn't process the last time the app was running.
433
+ // We want to ensure we process them, but only after the standard batching delay to be as
434
+ // optional as possible with network calls.
435
+ test("starting OperationModelStore should be processed, following normal delay rules") {
436
+ // Given
437
+ val mocks = Mocks ()
438
+ mocks.configModelStore.model.opRepoExecutionInterval = 100
439
+ every { mocks.operationModelStore.list() } returns listOf(mockOperation())
440
+ val executeOperationsCall = mockExecuteOperations(mocks.operationRepo)
441
+
442
+ // When
443
+ mocks.operationRepo.start()
444
+ val immediateResult =
445
+ withTimeoutOrNull(100) {
446
+ executeOperationsCall.waitForWake()
447
+ }
448
+ val delayedResult =
449
+ withTimeoutOrNull(200) {
450
+ executeOperationsCall.waitForWake()
451
+ }
452
+
453
+ // Then
454
+ immediateResult shouldBe null
455
+ delayedResult shouldBe true
456
+ }
457
+
458
+ test("ensure results from executeOperations are added to beginning of the queue") {
459
+ // Given
460
+ val mocks = Mocks ()
461
+ val executor = mocks.executor
462
+ val opWithResult = mockOperationNonGroupable()
463
+ val opFromResult = mockOperationNonGroupable()
464
+ coEvery {
465
+ executor.execute(listOf(opWithResult))
466
+ } coAnswers {
467
+ ExecutionResponse (ExecutionResult .SUCCESS , operations = listOf(opFromResult))
468
+ }
469
+ val firstOp = mockOperationNonGroupable()
470
+ val secondOp = mockOperationNonGroupable()
471
+
472
+ // When
473
+ mocks.operationRepo.start()
474
+ mocks.operationRepo.enqueue(firstOp)
475
+ mocks.operationRepo.executeOperations(
476
+ listOf(OperationQueueItem (opWithResult, bucket = 0)),
477
+ )
478
+ mocks.operationRepo.enqueueAndWait(secondOp)
479
+
480
+ // Then
481
+ coVerifyOrder {
482
+ executor.execute(withArg { it[0] shouldBe opWithResult })
483
+ executor.execute(withArg { it[0] shouldBe opFromResult })
484
+ executor.execute(withArg { it[0] shouldBe firstOp })
485
+ executor.execute(withArg { it[0] shouldBe secondOp })
486
+ }
487
+ }
373
488
}) {
374
489
companion object {
375
490
private fun mockOperation (
@@ -395,5 +510,17 @@ class OperationRepoTests : FunSpec({
395
510
396
511
return operation
397
512
}
513
+
514
+ private fun mockOperationNonGroupable () = mockOperation(groupComparisonType = GroupComparisonType .NONE )
515
+
516
+ private fun mockExecuteOperations (opRepo : OperationRepo ): WaiterWithValue <Boolean > {
517
+ val executeWaiter = WaiterWithValue <Boolean >()
518
+ coEvery { opRepo.executeOperations(any()) } coAnswers {
519
+ executeWaiter.wake(true )
520
+ delay(10 )
521
+ firstArg<List <OperationRepo .OperationQueueItem >>().forEach { it.waiter?.wake(true ) }
522
+ }
523
+ return executeWaiter
524
+ }
398
525
}
399
526
}
0 commit comments