Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace specs2 with munit #4216

Open
wants to merge 15 commits into
base: series/3.x
Choose a base branch
from

Conversation

iRevive
Copy link
Contributor

@iRevive iRevive commented Dec 27, 2024

Follow up to #3787.

The whole PR is purely focused on the migration. There are options to build more usable test abstractions and utilities, but I don't want to do it without proper thinking.

@iRevive iRevive force-pushed the topic/munit-kernel-laws branch from e671306 to d2b73e9 Compare December 27, 2024 19:26
// we try iterating a few times to get canceled in the middle
if (i > Bound) {
IO.pure(skipped(s"attempted $i times and could not reproduce scenario"))
IO.println(s"attempted $i times and could not reproduce scenario")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it's not a direct equivalent. But I cannot think of anything better.

@iRevive iRevive force-pushed the topic/munit-kernel-laws branch 2 times, most recently from 4c319c2 to 7bc5906 Compare December 27, 2024 20:00
@djspiewak
Copy link
Member

…holy crap

@iRevive
Copy link
Contributor Author

iRevive commented Dec 27, 2024

General 20k LOC changeset. Nothing to worry about 😅

@iRevive
Copy link
Contributor Author

iRevive commented Dec 27, 2024

Specs2 model feels more strict. With munit the following code compiles and the test is green, but it's never being executed:

test("leaky io") {
  IO(sys.error("boom"))
}

The compiler doesn't throw any warning, either.


I hope I didn't miss such cases.

Copy link
Member

@djspiewak djspiewak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NGL, I didn't read through all 11k lines. Did some spot checking around though and left some comments on things that I think probably apply more broadly. Agree we should review carefully. Overall looks good though.


if (!isWindows) { // these tests have all been emperically flaky on Windows CI builds, so they're disabled

"evaluate and print hello world" in {
test(s"IOApp (${platform.id}) - evaluate and print hello world") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of bikeshedding: I think on this one (and a bunch of similars), we can simply discard the should. With IOAppSpec it just doesn't add any value.

build.sbt Show resolved Hide resolved
tests/shared/src/test/scala/cats/effect/IOSuite.scala Outdated Show resolved Hide resolved
ioapp-tests/src/test/scala/IOAppSpec.scala Show resolved Hide resolved
@djspiewak
Copy link
Member

Specs2 model feels more strict. With munit the following code compiles and the test is green, but it's never being executed:

This one is concerning, to be sure. We've historically had a bunch of problems with this, and we do have a ton of tests which throw exceptions in various cases. We should explore our own variant of test probably which is stricter.


override def munitValueTransforms: List[ValueTransform] =
super.munitValueTransforms ++ List(
new ValueTransform("IO", { case _: IO[_] => sys.error("Non-evaluated IO") }),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These transformers prevent us from misuse like:

test("leaky io") {
  IO(sys.error("boom"))
}

@iRevive iRevive force-pushed the topic/munit-kernel-laws branch from 3d60f3f to 9f68a30 Compare December 30, 2024 13:59
@iRevive
Copy link
Contributor Author

iRevive commented Dec 30, 2024

specs2 CI time: 5h 22m
munit CI time: 5h 6m

There's not much difference, assuming we lost a spec parallelism.

@djspiewak
Copy link
Member

I think the next step on this is probably to go through and surgically break some subtle things one at a time and make sure the test suite still catches it.

Comment on lines 25 to 31
@deprecated("Please use a type safe alternative, such as 'real' or 'ticked'", "3.6.0")
override def test(name: String)(body: => Any)(implicit loc: Location): Unit =
super.test(name)(body)

@deprecated("Please use a type safe alternative, such as 'real' or 'ticked'", "3.6.0")
override def test(options: TestOptions)(body: => Any)(implicit loc: Location): Unit =
super.test(options)(body)
Copy link
Contributor Author

@iRevive iRevive Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deprecated the built-in options. Now, we are forced to use a type-safe alternative.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just boink them entirely since it's internal, rather than deprecating.

override def test(name: String)(body: => Any)(implicit loc: Location): Unit =
super.test(name)(body)

@deprecated("Please use a type safe alternative, such as 'real' or 'ticked'", "3.6.0")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it type safe, type-safe, typsafe?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen all three. 😃 Which is your favorite?

@armanbilge
Copy link
Member

Sorry, I snuck a new test in here.

Comment on lines 52 to 58
new ValueTransform(
"Other",
{
case r if !r.isInstanceOf[Unit] && !r.isInstanceOf[Prop] =>
sys.error(s"Unexpected value of type ${r.getClass.getName}: $r")
}
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This transformer ensures the result type is Unit or Prop.
From the munit perspective, that's a valid test:

test("some test") {
  1 == 2
}

However, it should use assert:

test("some test") {
  assert(1 == 2)
}

Here are all the fixed tests that were exposed by this transformer: 9186d4b

def real[A: AsResult](test: => IO[A]): Execution =
Execution.withEnvAsync { _ =>
val (fut, cancel) = test.unsafeToFutureCancelable()(runtime())
def ticked(options: TestOptions)(body: Ticker => Unit)(implicit loc: Location): Unit =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tightened the body type from Ticker => A to Ticker => Unit. Spotted a few non-evaluated assertions that way.

@iRevive
Copy link
Contributor Author

iRevive commented Jan 3, 2025

Sorry, I snuck a new test in here.

No worries. I've backported the test.

@djspiewak
Copy link
Member

djspiewak commented Jan 3, 2025

So for some reason, IOAppSpec just hung for me locally. I ran root/testQuick and I think it was the native platform which hung:

[warn] Canceling execution...
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/example/native/target/scala-2.13/cats-effect-example-test-out finished with non-zero value 130 (0x82)
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/laws/native/target/scala-2.13/cats-effect-laws-test-out finished with non-zero value 130 (0x82)
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/testkit/native/target/scala-2.13/cats-effect-testkit-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/kernel/native/target/scala-2.13/cats-effect-kernel-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/kernel-testkit/native/target/scala-2.13/cats-effect-kernel-testkit-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/std/native/target/scala-2.13/cats-effect-std-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[error] Test runner interrupted by fatal signal 2
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/tests/native/target/scala-2.13/cats-effect-tests-test-out finished with non-zero value 130 (0x82)
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect/series-3.x/core/native/target/scala-2.13/cats-effect-test-out finished with non-zero value 130 (0x82)
[info] cats.effect.IOAppSpec:
[error] ==> X cats.effect.IOAppSpec.evaluate and print hello world  0.159s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:154
[error] 153:        val h = platform("HelloWorld", Nil)
[error] 154:        assertEquals(h.awaitStatus(), 0)
[error] 155:        assertEquals(h.stdout(), s"Hello, World!${System.lineSeparator()}")
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[error] ==> X cats.effect.IOAppSpec.pass all arguments to child  0.055s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:161
[error] 160:        val h = platform("Arguments", expected)
[error] 161:        assertEquals(h.awaitStatus(), 0)
[error] 162:        assertEquals(
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[error] ==> X cats.effect.IOAppSpec.exit on non-fatal error  0.053s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:171 assertion failed
[error] 170:        assertEquals(h.awaitStatus(), 1)
[error] 171:        assert(h.stderr().contains("Boom!"))
[error] 172:      }
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$7(IOAppSpec.scala:171)
[error] ==> X cats.effect.IOAppSpec.exit with leaked fibers  0.059s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:176
[error] 175:        val h = platform("LeakedFiber", List.empty)
[error] 176:        assertEquals(h.awaitStatus(), 0)
[error] 177:      }
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[error] ==> X cats.effect.IOAppSpec.exit on fatal error  0.055s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:182 assertion failed
[error] 181:        assertEquals(h.awaitStatus(), 1)
[error] 182:        assert(h.stderr().contains("Boom!"))
[error] 183:        assert(!h.stdout().contains("sadness"))
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$13(IOAppSpec.scala:182)
[error] ==> X cats.effect.IOAppSpec.exit on fatal error with other unsafe runs  0.059s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:189 assertion failed
[error] 188:        assertEquals(h.awaitStatus(), 1)
[error] 189:        assert(h.stderr().contains("Boom!"))
[error] 190:      }
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$19(IOAppSpec.scala:189)
[error] ==> X cats.effect.IOAppSpec.exit on raising a fatal error with attempt  0.045s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:195 assertion failed
[error] 194:        assertEquals(h.awaitStatus(), 1)
[error] 195:        assert(h.stderr().contains("Boom!"))
[error] 196:        assert(!h.stdout().contains("sadness"))
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$23(IOAppSpec.scala:195)
[error] ==> X cats.effect.IOAppSpec.exit on raising a fatal error with handleError  0.062s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:202 assertion failed
[error] 201:        assertEquals(h.awaitStatus(), 1)
[error] 202:        assert(h.stderr().contains("Boom!"))
[error] 203:        assert(!h.stdout().contains("sadness"))
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$29(IOAppSpec.scala:202)
[error] ==> X cats.effect.IOAppSpec.exit on raising a fatal error inside a map  0.044s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:209 assertion failed
[error] 208:        assertEquals(h.awaitStatus(), 1)
[error] 209:        assert(h.stderr().contains("Boom!"))
[error] 210:        assert(!h.stdout().contains("sadness"))
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$35(IOAppSpec.scala:209)
[error] ==> X cats.effect.IOAppSpec.exit on raising a fatal error inside a flatMap  0.056s munit.FailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:216 assertion failed
[error] 215:        assertEquals(h.awaitStatus(), 1)
[error] 216:        assert(h.stderr().contains("Boom!"))
[error] 217:        assert(!h.stdout().contains("sadness"))
[error]     at munit.FunSuite.assert(FunSuite.scala:11)
[error]     at cats.effect.IOAppSpec.$anonfun$new$41(IOAppSpec.scala:216)
[error] ==> X cats.effect.IOAppSpec.warn on global runtime collision  0.046s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:222
[error] 221:        val h = platform("GlobalRacingInit", List.empty)
[error] 222:        assertEquals(h.awaitStatus(), 0)
[error] 223:        assert(
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[error] ==> X cats.effect.IOAppSpec.reset global runtime on shutdown  0.097s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:232
[error] 231:        val h = platform("GlobalShutdown", List.empty)
[error] 232:        assertEquals(h.awaitStatus(), 0)
[error] 233:        assert(
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[warn] ==> i cats.effect.IOAppSpec.warn on cpu starvation ignored 0.0s
[error] ==> X cats.effect.IOAppSpec.custom runtime installed as global  0.058s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:258
[error] 257:        val h = platform("CustomRuntime", List.empty)
[error] 258:        assertEquals(h.awaitStatus(), 0)
[error] 259:      }
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[error] ==> X cats.effect.IOAppSpec.abort awaiting shutdown hooks  0.053s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:264
[error] 263:          val h = platform("ShutdownHookImmediateTimeout", List.empty)
[error] 264:          assertEquals(h.awaitStatus(), 0)
[error] 265:        }
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +0
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[error] ==> X cats.effect.IOAppSpec.run finalizers on TERM  10.601s munit.ComparisonFailException: /Users/daniel/Development/Scala/cats-effect/series-3.x/ioapp-tests/src/test/scala/IOAppSpec.scala:301
[error] 300:        h.term()
[error] 301:        assertEquals(h.awaitStatus(), 143)
[error] 302:
[error] values are not the same
[error] => Obtained
[error] 1
[error] => Diff (- obtained, + expected)
[error] -1
[error] +143
[error]     at munit.Assertions.failComparison(Assertions.scala:274)
[info]   + exit on fatal error without IOApp 0.044s
[info]   + exit on canceled 0.055s
[error] ==> X cats.effect.IOAppSpec.live fiber snapshot  639.814s java.lang.InterruptedException: sleep interrupted
[error]     at java.lang.Thread.sleepNanos0(Native Method)
[error]     at java.lang.Thread.sleepNanos(Thread.java:496)
[error]     at java.lang.Thread.sleep(Thread.java:527)
[error]     at cats.effect.IOAppSpec.$anonfun$new$81(IOAppSpec.scala:331)
[error] ==> X cats.effect.IOAppSpec.gracefully ignore undefined process.exit  0.005s java.lang.InterruptedException: null
[error]     at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1696)
[error]     at java.lang.ProcessImpl.waitFor(ProcessImpl.java:425)
[error]     at scala.sys.process.ProcessImpl$SimpleProcess.exitValue(ProcessImpl.scala:266)
[error]     at cats.effect.IOAppSpec$Platform$$anon$1.awaitStatus(IOAppSpec.scala:49)
[error]     at cats.effect.IOAppSpec.$anonfun$new$105(IOAppSpec.scala:386)
Cancelled: testQuick

@iRevive
Copy link
Contributor Author

iRevive commented Jan 3, 2025

My best guess is that testQuick relies on the cached outputs. For example, testQuick doesn't even work on a fresh run:

...
[error] ==> X cats.effect.IOAppSpec.exit on canceled  0.002s java.io.IOException: Cannot run program "/Users/maksym/projects/oss/public/cats-effect/tests/native/target/scala-2.13/cats-effect-tests-out": error=2, No such file or directory
[error]     at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
[error]     at scala.sys.process.ProcessBuilderImpl$Simple.run(ProcessBuilderImpl.scala:85)
[error]     at cats.effect.IOAppSpec$Platform.apply(IOAppSpec.scala:42)
[error]     at cats.effect.IOAppSpec.$anonfun$new$79(IOAppSpec.scala:319)
[error] Caused by: java.io.IOException: error=2, No such file or directory
[error]     at java.lang.UNIXProcess.forkAndExec(Native Method)
[error]     at java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
[error]     at java.lang.ProcessImpl.start(ProcessImpl.java:134)
[error]     at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
[error]     ... 4 more
[error] Failed: Total 16, Failed 16, Errors 0, Passed 0, Ignored 1

If you didn't clean the project, it may use the old cached native artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants