Skip to content

Commit 38248cf

Browse files
author
Pablo Orgaz
committed
Fix critical bug for multiple stores listening to same action, add tests for it
1 parent 5158108 commit 38248cf

File tree

7 files changed

+176
-16
lines changed

7 files changed

+176
-16
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ buildscript {
1111
}
1212

1313
dependencies {
14-
classpath 'com.android.tools.build:gradle:3.3.1'
14+
classpath 'com.android.tools.build:gradle:3.3.2'
1515
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1616
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
1717
classpath 'com.github.triplet.gradle:play-publisher:1.2.0'

mini-common/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
compile "io.reactivex.rxjava2:rxjava:$rx_version"
1919

2020
testCompile 'junit:junit:4.12'
21+
testImplementation "org.amshove.kluent:kluent:1.44"
2122
}
2223

2324
apply plugin: 'idea'

mini-common/src/main/java/mini/Dispatcher.kt

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mini
22

33
import io.reactivex.disposables.Disposable
44
import java.util.*
5+
import java.util.concurrent.atomic.AtomicInteger
56
import kotlin.reflect.KClass
67

78
/**
@@ -66,7 +67,12 @@ class Dispatcher {
6667
synchronized(subscriptions) {
6768
val reg = Registration(this, clazz.java, priority, callback as (Any) -> Unit)
6869
val set = subscriptions.getOrPut(clazz.java) {
69-
TreeSet(kotlin.Comparator { a, b -> a.priority.compareTo(b.priority) })
70+
TreeSet(kotlin.Comparator { a, b ->
71+
//Sort by priority, then by id for equal priority
72+
val p = a.priority.compareTo(b.priority)
73+
if (p == 0) a.id.compareTo(b.id)
74+
else p
75+
})
7076
}
7177
set.add(reg)
7278
return reg
@@ -85,15 +91,22 @@ class Dispatcher {
8591
* the main thread.
8692
*/
8793
fun dispatch(action: Any) {
88-
onUiSync {
89-
if (dispatching != null) {
90-
throw IllegalStateException("Nested dispatch calls. Currently dispatching: " +
91-
"$dispatching. Nested action: $action ")
92-
}
93-
dispatching = action
94-
interceptorChain.proceed(action)
95-
dispatching = null
94+
if (isAndroid) {
95+
onUiSync { doDispatch(action) }
96+
} else {
97+
doDispatch(action)
98+
}
99+
}
100+
101+
@Suppress("NOTHING_TO_INLINE") //Inlined so it doesn't appear in stack trace
102+
private inline fun doDispatch(action: Any) {
103+
if (dispatching != null) {
104+
throw IllegalStateException("Nested dispatch calls. Currently dispatching: " +
105+
"$dispatching. Nested action: $action ")
96106
}
107+
dispatching = action
108+
interceptorChain.proceed(action)
109+
dispatching = null
97110
}
98111

99112
/**
@@ -104,15 +117,39 @@ class Dispatcher {
104117
onUi { dispatch(action) }
105118
}
106119

107-
data class Registration(val dispatcher: Dispatcher,
108-
val type: Class<*>,
109-
val priority: Int, val fn: (Any) -> Unit) : Disposable {
120+
/**
121+
* Handle for a dispatcher subscription.
122+
*/
123+
class Registration(val dispatcher: Dispatcher,
124+
val type: Class<*>,
125+
val priority: Int, val fn: (Any) -> Unit) : Disposable {
126+
127+
companion object {
128+
private val idCounter = AtomicInteger()
129+
}
130+
131+
val id = idCounter.getAndIncrement()
110132
var disposed = false
133+
134+
@Synchronized
111135
override fun isDisposed(): Boolean = disposed
112136

137+
@Synchronized
113138
override fun dispose() {
114139
dispatcher.unregister(this)
115140
disposed = true
116141
}
142+
143+
override fun equals(other: Any?): Boolean {
144+
if (this === other) return true
145+
if (javaClass != other?.javaClass) return false
146+
other as Registration
147+
if (id != other.id) return false
148+
return true
149+
}
150+
151+
override fun hashCode(): Int {
152+
return id
153+
}
117154
}
118155
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package mini
2+
3+
import org.amshove.kluent.`should be equal to`
4+
import org.junit.Test
5+
6+
class DispatcherTest {
7+
8+
class TestAction : BaseAction()
9+
10+
@Test
11+
fun subscriptions_are_added() {
12+
val dispatcher = Dispatcher()
13+
var called = 0
14+
dispatcher.register<TestAction> {
15+
called++
16+
}
17+
dispatcher.dispatch(TestAction())
18+
called `should be equal to` 1
19+
}
20+
21+
@Test
22+
fun order_is_respected_for_same_priority() {
23+
val dispatcher = Dispatcher()
24+
val calls = ArrayList<Int>()
25+
dispatcher.register<TestAction> {
26+
calls.add(0)
27+
}
28+
dispatcher.register<TestAction> {
29+
calls.add(1)
30+
}
31+
dispatcher.dispatch(TestAction())
32+
calls[0] `should be equal to` 0
33+
calls[1] `should be equal to` 1
34+
}
35+
36+
@Test
37+
fun order_is_respected_for_different_priority() {
38+
val dispatcher = Dispatcher()
39+
val calls = ArrayList<Int>()
40+
dispatcher.register<TestAction>(priority = 10) {
41+
calls.add(0)
42+
}
43+
dispatcher.register<TestAction>(priority = 0) {
44+
calls.add(1)
45+
}
46+
dispatcher.dispatch(TestAction())
47+
calls[0] `should be equal to` 1
48+
calls[1] `should be equal to` 0
49+
}
50+
51+
@Test
52+
fun disposing_registration_removes_subscription() {
53+
val dispatcher = Dispatcher()
54+
var called = 0
55+
dispatcher.register<TestAction> {
56+
called++
57+
}.dispose()
58+
dispatcher.dispatch(TestAction())
59+
called `should be equal to` 0
60+
}
61+
}

mini-runtime/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ dependencies {
99
compile 'io.github.classgraph:classgraph:4.6.32'
1010

1111
testCompile 'junit:junit:4.12'
12-
testCompile 'com.google.testing.compile:compile-testing:0.15'
12+
testCompile "org.amshove.kluent:kluent:1.44"
13+
1314
}

mini-runtime/src/main/java/mini/MiniRuntime.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package mini
33
import kotlin.reflect.KClass
44
import kotlin.reflect.full.findAnnotation
55
import kotlin.reflect.full.functions
6+
import kotlin.reflect.jvm.javaType
67
import kotlin.reflect.jvm.jvmErasure
78

89
object MiniRuntime : MiniInitializer {
@@ -16,6 +17,11 @@ object MiniRuntime : MiniInitializer {
1617
if (fn.parameters.size != 2) { //param 0 is `this`
1718
throw IllegalArgumentException("Function should have 1 argument for action")
1819
}
20+
21+
if (fn.returnType.javaType != Void.TYPE) {
22+
throw IllegalArgumentException("Reducers shouldn't have return value")
23+
}
24+
1925
val actionType = fn.parameters[1].type.jvmErasure
2026
val priority = annotation!!.priority
2127
dispatcher.register(clazz = actionType, priority = priority, callback = {
@@ -43,11 +49,9 @@ object MiniRuntime : MiniInitializer {
4349
private class ReflectedType(val clazz: KClass<*>, val depth: Int)
4450

4551
class ReflectiveActionTypesMap : Map<Class<*>, List<Class<*>>> {
46-
4752
private val genericTypes = listOf(
4853
Object::class.java
4954
)
50-
5155
private val map = HashMap<Class<*>, List<Class<*>>>()
5256
override val entries: Set<Map.Entry<Class<*>, List<Class<*>>>> = map.entries
5357
override val keys: Set<Class<*>> = map.keys
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package mini
2+
3+
import org.amshove.kluent.`should be equal to`
4+
import org.amshove.kluent.`should contain`
5+
import org.junit.Test
6+
7+
class MiniRuntimeTest {
8+
9+
class SampleAction : BaseAction()
10+
11+
class ValidStore : Store<String>() {
12+
@Reducer
13+
fun reducer(action: SampleAction) {
14+
}
15+
}
16+
17+
class InvalidStore1 : Store<String>() {
18+
@Reducer
19+
fun reducer(action: SampleAction): String {
20+
return "Invalid return"
21+
}
22+
}
23+
24+
class InvalidStore2 : Store<String>() {
25+
@Reducer
26+
fun reducer(action: SampleAction, badParam: String): String {
27+
return "Invalid return"
28+
}
29+
}
30+
31+
@Test
32+
fun types_are_reflected() {
33+
val map = MiniRuntime.ReflectiveActionTypesMap()
34+
map[SampleAction::class.java]!!.`should contain`(SampleAction::class.java)
35+
map[SampleAction::class.java]!!.`should contain`(BaseAction::class.java)
36+
}
37+
38+
@Test
39+
fun reducers_are_found() {
40+
val dispatcher = Dispatcher()
41+
MiniRuntime.initialize(dispatcher, listOf(ValidStore()))
42+
dispatcher.subscriptions.size `should be equal to` 1
43+
}
44+
45+
@Test(expected = IllegalArgumentException::class)
46+
fun functions_should_be_void() {
47+
val dispatcher = Dispatcher()
48+
MiniRuntime.initialize(dispatcher, listOf(InvalidStore1()))
49+
}
50+
51+
@Test(expected = IllegalArgumentException::class)
52+
fun functions_should_have_one_parameter_void() {
53+
val dispatcher = Dispatcher()
54+
MiniRuntime.initialize(dispatcher, listOf(InvalidStore2()))
55+
}
56+
}

0 commit comments

Comments
 (0)