Fast Haskell rebuild+test with file watch using cabal + GHCID?

Viewed 845

my question in short is "how to get a fast save-retest workflow in a cabal-managed multi-library haskell project repository?"

I already tried a few things and did some research. Before getting into more details please have a look at the typical project repo structure and then have the question broken down into more details:

Repository Development Structure

i work on multiple Haskell projects that usually have the following form:

.
├── foo
│   ├── foo.cabal
│   ├── src
│   ├── unit-test
│   └── ...
├── bar
│   ├── bar.cabal
│   ├── src
│   ├── unit-test
│   └── ...
├── baz
│   ├── baz.cabal
│   ├── src
│   ├── unit-test
│   └── ...
├── stack.yaml
├── cabal.project
├── nix
│   └── ...
└── ...

The cabal.project file looks like this:

packages: 
  foo
  bar
  baz
  ...
tests: True
run-tests: True

The stack file contains basically the same project list and an LTS ID, so i can just just the stackProject nix function from IOHK's haskell.nix to provide myself a nix shell which has cabal etc. in place. (This question is more about cabal handling so i consider this text paragraph here only a background note that i think is not relevant for this stack overflow question.)

This setup allows me to just run cabal test all anywhere in the project, which is great. This is my simple method to see if i broke anything before closing the next git commit.

Rapid Save-Retest Workflow

Before i got to nix, i used stack build/test --watch which was nice, because i could now have a shell open which always retests and rebuilds the whole project after i changed anything anywhere.

This can be simulated with inotify:

while true; do 
  inotifywait -e modify -r ./; 
  cabal test all
done

This is not really fast but it also does the job.

After i got to know about GHCID i was amazed by how blazingly fast it is. It is also easy to use with cabal repl.

Unfortunately (this problem was also mentioned but left unanswered in a comment here How to run test suite in ghcid with cabal?), GHCID can be run on one specific unit test suite and will not detect changes on the library that the unit tests are supposed to check. (Putting all the library modules into the unit test description in the cabal file is something that i consider an ugly hack and i woul rather like to avoid that)

Also, it seems i can't run GHCID on the whole repository like cabal test all or stack test --watch do. The extreme speed of GHCID is something that i really want in my workflow.

cabal is a tool that has existed long time before stack, how do people work on their multi-lib repositories to have a fast overview of all the test cases they broke after they edited multiple files in multiple libs? If the GHCID way does not work well, what is the way to do it?

2 Answers

I use a script with stack as follows:

ghcid -c="stack ghci <test-suite>.hs" -T="main" --warnings $@

This means:

  • Run ghcid
  • Use stack ghci instead of vanilla ghci
  • Also load the test suite module into ghci
  • Run main (from the test suite) upon loading ghci
  • Run the tests even if the compile generates GHC warnings

You could easily adapt this to use, for example, cabal repl instead of stack ghci.

This has the following drawbacks:

  • The name of the test module needs to be hard-coded, and is not extracted from package.yaml/the cabal file.
  • It does not support multiple test suites. These could all be passed to the script, but you'd need a custom main to call them all.

I previously got around these problems by using :!stack test as the command, which runs the all test suites, but since this is issued via the command line rather than loaded into ghci, The tests ran much slower. Additionally, It did not hot-reload on modification to the tests.

The bottom line is: to get the benefits of ghcid, ghcid needs to be informed of which source files to look at. Any reloading of ghcid via a shell command will require a full reload and recompile by ghci, not expoloiting ghci's fast hot reload capability. If this information is stored in a build system config file, your build system needs to integrate with ghcid (absent a custom script.) My guess is this is too low-level for cabal, but I have opened a feature request for stack. Add a comment or reaction there if you want to see this happen!

There is ongoing work to support multiple home units in GHC, but as of March 2022, not enough has been implemented yet to support them in ghcid:

  • Barely any normal GHCi features are supported (#20889). It would be good to support enough for ghcid to work correctly.

In the meantime the ugly hack mentioned by the OP is the best workaround: during dev time add some settings of the library stanzas to the settings of the test-suite stanzas, usually hs-source-dirs, build-depends, and default-extensions, and remove those local libraries from the build-depends of the test-suite stanzas. import-ing common stanzas can help to reduce this boilerplate.

Related