Pseudo-cyclic dependencies in SBT

Excuse my recent garrulousness, it seems that experimenting with pair-programming with Claude has increased my rate of encountering tricky problems. Going faster gets you to more roadworks per unit time, it appears.

Anyway … I’m trying to split a monolithic SBT build of Americium into a core module and an integration module - the core supplies Java and Scala APIs for property-based testing, and is of general use without making assumptions about the end-users test framework choice. The integration module is specific to JUnit5; having two modules means folk who aren’t using JUnit5 don’t have to pull in a raft of dependencies, and may potentially open up a path to target multiple architectures (JVM, JS, native) for the core module.

Where I’m coming unstuck is that I have a compile dependency of the JUnit5 integration module on the core, which is fine in itself, but would really like to use the JUnit5 integration for some of the tests of the core’s codebase.

This was in place prior to the module split, as everything sat in one big module, but now I have a pseudo-cycle where JUnit5 integration’s compile depends on the core’s compile, and the core’s test depends on the JUnit5 integration’s compile.

Trying to define recursive definitions by mutually referencing lazy vals (and judicious use of dependsOn with the fine-grained dependency syntax) fails for me - I just see infinite recursion as SBT starts up.

Making the recursive definitions into defs is rejected by SBT as it wants vals.

Oddly enough, if I compromise and move the problematic core tests out into the integration module, I see that SBT can run both modules’ tests concurrently, even though the integration depends on the core; so in some sense it seems SBT does allow the compile and test dependencies to be teased apart from each other.

Is there a way of achieving this?

I believe sbt should support what you want, because you can actually consider the test part of a module like another one, and in that way, you do not have any cycles.

However, precisely because of that, my personal recommendation and the approach I follow lately, is to move all tests to separate modules.

@BalmungSan I had considered that approach - are you adding a dependency of the publishing task for the ‘deliverable-module’ on the ‘test-module’ then to ensure that a bad build doesn’t get published?

I just run sbt test before sbt publish.

Ha, I was going to say that I’d prefer to have a one-stop shop in the SBT build itself, so sbt publish takes care of the test step as a prerequisite.

However, given I use GitHub - guardian/gha-scala-library-release-workflow: Publishing Scala libraries to Maven Central using GitHub Actions to do the actual publishing steps in CI, your approach would probably work fine, @BalmungSan.

I’ll try mucking around with the publishing task dependencies first (with the problematic tests in their own module), but it’s nice to know there is brute force solution to fall back on.

1 Like