United Kingdom: +44 (0)208 088 8978

Peril in the Pipeline: the Mystery of the Passing Tests

Fable caching behaviour masked our passing-but-broken local build until CI exposed it -- how we diagnosed and fixed it

We're hiring Software Developers

Click here to find out more

I recently was working on extracting the utilities from the SAFE.Meta packages into their new home at SAFE.Utils. Once I set up the repository and a pipeline for the tests, I ran into classic developer frustration: my tests ran perfectly fine locally, but consistently failed in the CI pipeline.

This post explores how we diagnosed the issue, discovered some subtle caching behavior in Fable, and ultimately fixed our build.

Finding a fix

The output made it obvious that the problem was in the compilation step, in particular when compiling some dependencies:

.\bld\fable_modules\Fable.SimpleJson.3.24.0\SimpleJson.fs(6,12): (6,18) error FSHARP: The namespace 'Import' is not defined. (code 39)
.\bld\fable_modules\Fable.Remoting.Client.7.32.0\Extensions.fs(13,30): (13,40) error FSHARP: The type 'FileReader' is not defined. (code 39)
...

To check my sanity, I re-ran the tests locally, and sure enough, there were passing tests:

> pretest
> dotnet fable -o bld

Fable 4.23.0: F# to JavaScript compiler
Minimum @fable-org/fable-library-js version (when installed from npm): 1.6.0

Thanks to the contributor! @battermann
Stand with Ukraine! https://standwithukraine.com.ua/

Parsing SAFE.Client.Tests.fsproj...
Retrieving project options from cache, in case of issues run `dotnet fable clean` or try `--noCache` option.
Project and references (21 source files) parsed in 146ms

Skipped compilation because all generated files are up-to-date!

> test
> mocha bld

  RemoteData
    defaultValue
      ✔ Not started
...

I called in reinforcement from John, who suggested re-creating the issue by reproducing what happened in the pipeline as closely as possible. We put all the steps in a .cmd file, preceded by git clean -xfd .. -e pipeline.cmd, and managed to reproduce our issue.

After a good amount of head scratching, we decided to run the tests locally again, and lo and behold, they were passing again! On reading Fable's output more carefully, it became evident why: Fable skipped compilation because all generated files are up-to-date!

This was the key insight: While the first run of the Fable command failed, it had still managed to produce some output. On the second run, compilation was skipped because Fable considered everything up to date, and the tests ran using whatever partial output had been generated.

The Solution

We found two important pieces to solving our problem:

  1. Using the --noCache flag: Fable's output itself mentioned this option. By adding --noCache to the Fable command, we could force it to disregard previous outputs, making consecutive runs consistently fail until we fixed the real issue.

  2. Adding the missing dependency: The actual fix was as simple as adding the missing DOM dependency to our Client project:

dotnet paket add Fable.Browser.Dom -p "src/Client/SAFE.Client.Utils"

GitHub issues

To help make the Fable ecosystem a bit better, I reported issues on the following repositories:

  • Fable; reported that, without changes, Fable will happily skip compilation on a broken build, without reporting an error.
  • Fable.Remoting; reported the missing dependencies

Lessons learned

The Fable compiler will not recompile if it does not need to, which can sometimes mask underlying issues. If you want to ensure you get a fresh compilation (and see any errors that might emerge), use the --noCache flag to force Fable to recompile everything.

The Fable compiler actually gives very helpful output with suggestions for troubleshooting - so take the time to read it carefully rather than just skimming over the results!