jjh42 / mock

Mocking library for Elixir language
MIT License
639 stars 81 forks source link

ExUnit runner gets killed when mock is used inside an umbrella project AND processes #98

Closed XelaRellum closed 5 years ago

XelaRellum commented 5 years ago

I have an umbrella project which contains a subproject that uses Mock and Agent to mock out File functions. The same code works fine in a project that is not part of an umbrella project.

But when I run mix test for the umbrella project, the ExUnit runner gets killed with an output like this: ** (EXIT from #PID<0.92.0>) killed.

I have created a repository to reproduce this behaviour here: https://github.com/XelaRellum/mock_bug_with_umbrella

Thanks a lot for you help and for creating Mock!

Olshansk commented 5 years ago

You are mocking the Filemodule in your tests, which is a core elixir module.

If you try mocking something else (e.g. FileEx), the error you quoted does not appear to reproduce.

I don't know the inner implementation details of umbrella projects, but I suspect it may be using the FIle module. Your mock interferes with the expected behaviour of core modules causing it to crash.

There's no real workaround other than re-thinking the test. I would create a wrapper module for File and mock that instead.

XelaRellum commented 5 years ago

Tried this, but not the File module seems to be the problem, but the Agent that is used to carry state (and by the way the File mock should not escape the with_mock statement, anyway).

I have updated the project and included a subproject with_umbrella_and_file_wrapper.

Olshansk commented 5 years ago

I think I'm misunderstanding something. The with_umbrella_and_file_wrapper project has the following mock:

test "unsuccessful read", mocks do
  with_mock(File, [
    write!: mocks.write!,
    read!: fn _path -> "" end
  ]) do
    assert FileEx.write_and_verify("some path", "som content") == false
  end
end

I do not suggest you mock core Elixir libraries unless you're 100% it won't have any side effects on the rest of the system, which in this case, it seems to have.

XelaRellum commented 5 years ago

You are right. When replacing File with FileWrapper I left this one location unchanged and this caused the bug.

For me, the lesson learned is not to mock core Elixir libraries.... Thanks for the help!

liskin commented 5 years ago

If anyone's interested what is happening under the hood, I dig a bit into it and here's a short explanation:

To mock a module, meck loads some synthetic code instead of the real module, and to unmock it, the real module is loaded again. Erlang can only keep two versions of any module in memory, so the second reload kills all processes that were running the original code. And mix happens to run old File code for longer periods of time: https://github.com/elixir-lang/elixir/blob/98485daab0a9f3ac2d7809d38f5e57cd73cb22ac/lib/mix/lib/mix/project.ex#L355

The crash can be reproduced by this short iex snippet:

iex(1)> File.cd!(".", fn -> :code.purge(File); :code.load_file(File); :code.purge(File); :code.load_file(File) end)
** (EXIT from #PID<0.127.0>) shell process exited with reason: killed

(the second :code.load_file is redundant, it's the second :code.purge what kills it, it's just there for readers to understand what two reloads look like)