exercism:main β exercism:single-sbt-session
opened 07:03PM - 31 Aug 26 UTC
Closes #842.
`bin/test` started a new sbt shell for every exercise, which is whβ¦at makes the CI run take around 20 minutes. It also only ran on Linux (`mktemp --suffix`, `sed -i -e`), and it mutated the exercises while it ran: it copied the example solution over the stub and stripped the `pending` markers out of the tests with `sed`, restoring both afterwards.
## A subproject per exercise
Every exercise is now a subproject of this build, contributed by [`project/Exercises.scala`](https://github.com/exercism/scala/blob/single-sbt-session/project/Exercises.scala), so a single sbt session verifies all of them. Each subproject's base directory is the exercise itself, which means sbt also picks up the exercise's own `build.sbt` β no need to restate `monocle`, `scala-parser-combinators` or `scalacheck` anywhere.
```
bin/test # every exercise
bin/test two-fer # a single exercise
bin/test two-fer bob # a few of them
```
β¦and from the sbt shell, `exercises/test`, `two-fer/test`, `~two-fer/test`.
| | before | after |
| --- | --- | --- |
| CI, `Run tests for all exercises` | ~20 min | **3 min 26 s** |
| locally, full run | | 49 s cold, 28 s incremental |
| exercises / tests | | 102 / 1312, `pending 0` everywhere |
The CI number is [this PR's own run](https://github.com/exercism/scala/actions/runs/33428409846/job/99607550803) on a cold cache: sbt reports 167 s for `exercises/test` itself, the remaining ~35 s is the launcher plus loading the build. The local numbers are from a 10-core M-series Mac.
1312 is exactly 1210 `pending` lines + the 102 tests that are enabled to begin with, i.e. the same coverage the old `sed` gave. It all fits in sbt's default 1 GB heap, so no `.jvmopts` is needed. `.github/workflows/test.yml` is unchanged β it still just runs `bin/test`.
## The exercises are no longer touched
Instead of copying files around, each subproject is pointed at different sources:
- `Compile / unmanagedSources` is the exemplar/example solution listed in `.meta/config.json`, plus whatever else accompanies the stub. That last part matters for `forth` and `robot-simulator`, which ship a support file next to the stub, and reading `config.json` rather than guessing matters for `pangram`, whose stub is `Pangrams.scala`.
- `Test / sourceGenerators` writes a copy of the tests with the `pending` lines **blanked out** rather than deleted, so line numbers still match the original file: a failure reports `(BobTest.scala:83)` and that is genuinely line 83 of the exercise's test file.
Everything lands in the top-level `target/`, so the exercise directories stay free of build output β which also keeps `bin/verify-exercises-in-docker` from copying it.
## Reporting
A failing run no longer stops at the first exercise; results are collected with `.result`, so every failure is reported:
```
[error] darts failed, re-run it with: bin/test darts
[error] 1 of 102 exercises failed
```
Compile errors point straight at the real file (`exercises/practice/darts/.meta/Example.scala:13:29`) instead of at the stub the example used to be copied onto.
## Also
`-source:future` moves from `ThisBuild` into `testgen`, so the exercises compile with the options students actually get. `README.md` gains a short "Verifying the Exercises" section.
## Deliberately not in this PR
Replacing `pending` with ScalaTest tags, as discussed in [the forum thread](https://forum.exercism.org/t/running-pending-scala-tests/12613/2). That touches all 102 test files, the generator templates and the test runner, and the thread hasn't settled on an approach β it seems better done together with #843. The concrete complaint from #842, using `sed` to strip `pending`, is gone: it now happens on the sbt/Scala side, without modifying anything.
π€ Generated with [Claude Code](https://claude.com/claude-code)