Warnings as errors and tests

Suppose we have this Elixir code:

# lib/foo.ex
defmodule Foo do
  def foo() do
    a = 42
  end
end

when we compile it, we’ll see this helpful warning:

$ mix compile
Compiling 1 file (.ex)
warning: variable "a" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/foo.ex:3: Foo.foo/0

$ echo $?
0

where $? in a Unix shell contains the exit status of the last executed command in the shell, 0 is success, non-zero code is a failure.

To make sure we don’t accidentally commit code that has warnings, we can pass the --warnings-as-errors option:

$ mix compile --warnings-as-errors
Compiling 1 file (.ex)
warning: variable "a" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/foo.ex:3: Foo.foo/0

Compilation failed due to warnings while using the --warnings-as-errors option

$ echo $?
1

Notice our shell reports the failure, exit status 1. This is very helpful because many CI systems will automatically fail the build when encountering non-zero exit code from a command.

Warnings as errors during compilation for CIs

Let’s see how to enable warnings as errors on CIs. Here’s a typical GitHub Actions setup for an Elixir project:

# .github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Install OTP and Elixir
        uses: actions/setup-elixir@v1
        with:
          otp-version: 23.1.1
          elixir-version: 1.11.1

      - run: mix deps.get
      - run: mix test

mix test will compile the code by calling mix compile and then run tests. To enable warnings as errors, all we need to do is to call mix compile --warnings-as-errors explicitly: (remember to use the right MIX_ENV!)

# (...)
- run: mix deps.get
- run: MIX_ENV=test mix compile --warnings-as-errors
- run: mix test

we could even combine it using mix do task:

# (...)
- run: mix deps.get
- run: MIX_ENV=test mix do compile --warnings-as-errors, test

Warnings as errors during tests for CIs

We have enabled warnings as errors for compiled code but suppose we have warnings in our tests:

defmodule FooTest do
  use ExUnit.Case

  test "foo" do
    a = 42
  end
end

We run our CI command again:

$ MIX_ENV=test mix do compile --warnings-as-errors, test
Compiling 1 file (.ex)
Generated foo app
warning: variable "a" is unused (if the variable is not meant to be used, prefix it with an underscore)
  test/foo_test.exs:5: FooTest."test foo"/1

.

Finished in 0.01 seconds
1 test, 0 failures

Randomized with seed 834982

$ echo $?
0

However, our command was successful, why?

In short, mix compile by default wouldn’t see files in test/. While the test files are of course compiled too, the compilation happens inside mix test and starts with the default compilation options.

We can alleviate it by setting compiler options in test/test_helper.exs, the first file that is loaded before we load any tests:

# test/test_helper.exs
Code.put_compiler_option(:warnings_as_errors, true)

ExUnit.start()

See Code.put_compiler_option/2 for the list of all available options.

Now, if we re-run the command it will fail:

$ MIX_ENV=test mix do compile --warnings-as-errors, test
warning: variable "a" is unused (if the variable is not meant to be used, prefix it with an underscore)
  test/foo_test.exs:5: FooTest."test foo"/1

Compilation failed due to warnings while using the --warnings-as-errors option

$ echo $?
1

Finally, on Elixir v1.12+ instead of changing test_helper.exs, we can simply do mix test --warnings-as-errors. Note we still need to pass --warnings-as-errors to mix compile, see docs!

Summary

We are big fans of keeping projects free of warnings and we usually configure our CIs to ensure that. Here’s an excerpt from GitHub Actions configuration:

# .github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Install OTP and Elixir
        uses: actions/setup-elixir@v1
        with:
          otp-version: 23.1.1
          elixir-version: 1.11.1

      - run: mix deps.get
      - run: MIX_ENV=test mix do compile --warnings-as-errors, test

And from Elixir v1.12+, you can do:

- run: MIX_ENV=test mix do compile --warnings-as-errors, test --warnings-as-errors

On large projects there’s usually a lot of compilation output in which case breaking it up might be helpful to be able to inspect each step’s output separately:

- run: MIX_ENV=test mix deps.compile
- run: MIX_ENV=test mix compile --warnings-as-errors
- run: mix test --warnings-as-errors

Finally, we also like to add mix format --check-formatted and mix deps.unlock --check-unused to our CI pipeline to catch even more things before code gets committed.

Happy hacking!