POC Pluggable Crystal::System backends

\o/ it works!

I’ve made a POC to change (on demand!) the implementation used by the stdlib when manipulating the system (OS) environment (through ENV):

Sorry in advance, it’s midnight here, and I need to sleeep!!..
(Also, even though there are a lot of code that can be generated, I didn’t do it yet to keep it clear)

The basic idea is to override the methods in Crystal::System::Env (used by the stdlib) and use a different implementation (impl) for these methods on demand, using PluggableSystem::Env.use(some_impl).

The exemple starts with the OS backend (the normal impl), then change the impl used to an in-memory env (you can set/unset/get without changing the OS env), use it then change back the the OS impl.

The PluggableSystem::Env is the controller, that you use to choose which implementation to use for Crystal::System::Env. It should be quite generic and could be done for all Crystal::System::* system modules…

The applications are pretty cool I think, like in-memory / overlays file system, env, time, whatever…
I think it should be possible to control (and spec!) stdin/out/err without doing dark magic linux fd reopening…

AND if the PluggableSystem concept is shared by muliple shards, or even integrated in the stdlib (in the future), we could have an abstraction over the OS to be able to do all sort of things (middleware?)

Let me know if you have any idea on how this could be used, or if you have suggestions, anything ;)

Time to sleep! I think I’ll continue working on this, and maybe make a shard or two (to isolate PluggableSystem)

It’s nice to do some crystal again :wink: See ya!

1 Like

Writing to ENV doesn’t really change the os environment. Environment variables are already an in-memory storage scoped to the current process, it’s just managed by the operating system.

So there is actually a much simpler solution to test behaviour related on ENV values, which is already used in stdlib specs for Time, Dir, File and Path. It looks like this:

def with_env(name, value)
  old_value = ENV[name]?
    ENV[name] = value
    ENV[name] = old_value

The benefit over mocking the Crystal API is that this also works with C libraries and external tools.

So, ENV is a pretty bad example for this, because it really doesn’t need such a kind of mock implementation. You can simply work with the real one. This is much simpler and provides almost the same behaviour.
I guess the only benefit is that mocking the API doesn’t have to be global state but could be limited to a specific fiber. However, when the mocking itself needs to work across several fibers, it can’t be used in parallel anyway.

Yeah, ENV isn’t the greatest example here, I wanted a system module with CRUD (CRD actually) which wasn’t too complex to override and test.

I guess the only benefit is that mocking the API doesn’t have to be global state but could be limited to a specific fiber

Yes I was thinking about that too, and for example spawning a fiber would inherit the current fiber’ system impls’

I think this idea is useful. For example we have webmock: it replaces the default implementation of HTTP::Client with a mock one, for tests. timecop uses the same idea.

Ideally, I’d like many types in the standard library to provide mocks for tests, right in the standard library, so they stay in sync. And shards could do the same.

1 Like