Skip to content

Commit 196822f

Browse files
committed
Add Stack tests
1 parent d95a4f8 commit 196822f

1 file changed

Lines changed: 227 additions & 0 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Copyright 2020-2024 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package cats
18+
package effect
19+
package std
20+
21+
import cats.arrow.FunctionK
22+
import cats.syntax.all._
23+
24+
import org.specs2.specification.core.Fragments
25+
26+
import scala.concurrent.duration._
27+
28+
final class StackSpec extends BaseSpec with DetectPlatform {
29+
30+
final override def executionTimeout = 2.minutes
31+
32+
"ConcurrentStack" should {
33+
tests(Stack.apply[IO, Int])
34+
}
35+
36+
"Stack with dual constructors" should {
37+
tests(Stack.in[IO, IO, Int])
38+
}
39+
40+
"MapK'd Stack" should {
41+
tests(Stack[IO, Int].map(_.mapK[IO](FunctionK.id)))
42+
}
43+
44+
def tests(stack: IO[Stack[IO, Int]]): Fragments = {
45+
"push and retrieve elements in LIFO order" in real {
46+
val p = for {
47+
s <- stack
48+
_ <- s.push(0)
49+
_ <- s.push(1)
50+
_ <- List.range(start = 2, end = 7).traverse_(n => s.push(n))
51+
_ <- s.pushN(7, 8, 9, 10)
52+
_ <- s.pushN(List.range(start = 11, end = 15): _*)
53+
result <- s.pop.replicateA(15)
54+
} yield result
55+
56+
p mustEqual List.range(start = 0, end = 15)
57+
}
58+
59+
"allow intercalate pushes and pops" in real {
60+
val p = for {
61+
s <- stack
62+
_ <- s.push(0)
63+
r1 <- s.pop
64+
_ <- s.push(1)
65+
_ <- s.push(3)
66+
_ <- s.push(5)
67+
r2 <- s.pop
68+
r3 <- s.pop
69+
_ <- s.push(10)
70+
r4 <- s.pop
71+
r5 <- s.pop
72+
} yield List(r1, r2, r3, r4, r5)
73+
74+
p mustEqual List(0, 1, 3, 5, 10)
75+
}
76+
77+
"block pop if empty" in ticked { implicit ticker =>
78+
val p = stack.flatMap(s => s.pop.void)
79+
80+
p must nonTerminate
81+
}
82+
83+
"unblock pop with a push" in ticked { implicit ticker =>
84+
val p = stack.flatMap { s =>
85+
(
86+
s.pop,
87+
(IO.sleep(1.millis) >> s.push(1))
88+
).parTupled
89+
}
90+
91+
p must completeAs((1, ()))
92+
}
93+
94+
"blocked fibers must be released in FIFO order (multiple pushes)" in ticked {
95+
implicit ticker =>
96+
val numbers = List.range(start = 1, end = 10)
97+
val p = stack.flatMap { s =>
98+
val popAll = numbers.parTraverse { i => IO.sleep(i.millis) >> s.pop }
99+
100+
val pushAll = IO.sleep(100.millis) >> numbers.traverse_(s.push)
101+
102+
(popAll, pushAll).parTupled
103+
}
104+
105+
p must completeAs((numbers, ()))
106+
}
107+
108+
"blocked fibers must be released in FIFO order (pushN)" in ticked { implicit ticker =>
109+
val numbers = List.range(start = 1, end = 10)
110+
val p = stack.flatMap { s =>
111+
val popAll = numbers.parTraverse { i => IO.sleep(i.millis) >> s.pop }
112+
113+
val pushAll = IO.sleep(100.millis) >> s.pushN(numbers: _*)
114+
115+
(popAll, pushAll).parTupled
116+
}
117+
118+
p must completeAs((numbers, ()))
119+
}
120+
121+
"cancelling a blocked pop must remove it from waiting queue" in ticked { implicit ticker =>
122+
val p = for {
123+
s <- stack
124+
ref <- IO.ref(List.empty[Int])
125+
fiber = s.pop.flatMap(n => ref.update(n :: _)).start
126+
f1 <- fiber
127+
_ <- f1.cancel
128+
f2 <- fiber
129+
f3 <- fiber
130+
f4 <- fiber
131+
f5 <- fiber
132+
f6 <- fiber
133+
f7 <- fiber
134+
_ <- f2.cancel
135+
_ <- s.push(1)
136+
_ <- f3.join
137+
_ <- s.push(3)
138+
_ <- f4.join
139+
_ <- f6.cancel
140+
_ <- s.push(5)
141+
_ <- s.push(10)
142+
_ <- f5.join
143+
_ <- f7.join
144+
result <- ref.get
145+
} yield result
146+
147+
p must completeAs(List(1, 3, 5, 10))
148+
}
149+
150+
"tryPop must not block if empty" in real {
151+
val p = for {
152+
s <- stack
153+
r1 <- s.tryPop
154+
_ <- s.push(3)
155+
r2 <- s.tryPop
156+
} yield List(r1, r2)
157+
158+
p mustEqual List(None, Some(3))
159+
}
160+
161+
"peek must not block and must not remove the element" in real {
162+
val p = for {
163+
s <- stack
164+
r1 <- s.peek
165+
_ <- s.push(1)
166+
_ <- s.push(3)
167+
r2 <- s.peek
168+
r3 <- s.peek
169+
r4 <- s.tryPop
170+
r5 <- s.peek
171+
} yield List(r1, r2, r3, r4, r5)
172+
173+
p mustEqual List(None, Some(3), Some(3), Some(3), Some(1))
174+
}
175+
176+
"size must be consistent in a non concurrent scenario" in real {
177+
val p = for {
178+
s <- stack
179+
r1 <- s.size
180+
_ <- s.push(1)
181+
r2 <- s.size
182+
_ <- s.pushN(2, 3, 4, 5)
183+
r3 <- s.size
184+
_ <- s.pop
185+
_ <- s.pop
186+
r4 <- s.size
187+
} yield List(r1, r2, r3, r4)
188+
189+
p mustEqual List(0, 1, 5, 3)
190+
}
191+
192+
"used concurrently" in ticked { implicit ticker =>
193+
val numbers = List.range(start = 0, end = 10)
194+
val p = stack.flatMap { s =>
195+
(
196+
s.pop.parReplicateA(numbers.size).map(_.sorted),
197+
numbers.parTraverse_(s.push)
198+
).parTupled
199+
}
200+
201+
p must completeAs((numbers, ()))
202+
}
203+
204+
"not lost elements when concurrently canceling a pop with a push" in real {
205+
val p = (stack, IO.deferred[Either[Int, Option[Int]]]).flatMapN {
206+
case (s, df) =>
207+
val left = IO.uncancelable(poll => poll(s.pop).flatMap(n => df.complete(Left(n))))
208+
val right = s.tryPop.flatMap(on => df.complete(Right(on)))
209+
210+
left.start.flatMap { f =>
211+
(
212+
IO.sleep(10.millis) >> f.cancel,
213+
IO.sleep(10.millis) >> s.push(3)
214+
).parTupled >> f.join
215+
} >> right >> df.get
216+
}
217+
218+
List.fill(10000)(p).forallM { result =>
219+
result.map {
220+
case Left(5) => true
221+
case Right(Some(5)) => true
222+
case _ => false
223+
}
224+
}
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)