on
Erlang
Intro
Erlang releases are a bit like magnets. Everyone who thinks about them shares the same thought: f**king releases, how do they work?
Fortunately, since the years of Emakefiles, reltool and systools, the Erlang community has stood up and improved its tooling continuously.
Rebar has been improving non-stop and keeps getting better for many functions. The newest generation, Rebar3, tries to provide an end-to-end experience to building Erlang projects.
Along with installing Erlang (version R16B03-1 at least), getting a hold of Rebar3 is all you’re gonna need.
For rebar, just download it and follow the
instructions. Rebar3 will
basically generate a self-executable that you can store in your repository, or
install globally on your computer. This tutorial expects that you have
installed it in your system and made it available in your $PATH
.
Once they’re all installed somewhere in your system, arm yourself with the text editor or IDE of your choice (mine is Vim, because I’m a terrible person) and get ready to write a few things.
My Environment
Despite you being free to develop on whatever you want, I’m gonna go through whatever my setup is.
I use zsh with oh-my-zsh, using a custom theme (depends on hg-prompt as a script), and stuck in vi-mode, because vim vim vim.
For vim itself, to work with Erlang, I use these two lines in my .vimrc file:
autocmd BufRead,BufNewFile *.erl,*.es.*.hrl,*.yaws,*.xrl set expandtab
au BufNewFile,BufRead *.erl,*.es,*.hrl,*.yaws,*.xrl setf erlang
And I depend on these two plugins:
I don’t use a lot of material outside of that, and the OS will tend to be my IDE – for projects that I tend to work on a lot, I will use tmux scripts (see this blog post for an example) – to get everything going early.
The Project
To avoid the usual Hello World stuff, this tutorial will use a somewhat more fun application to get up and running from a basic Erlang app that can be run within a module, to a proper OTP library that can be included by other projects, to a release than can be self-executing and distributed to client’s computer, or on a server.
Our project will be the replication of one of the most well-known software programs in popular history, used in life-critical situations: Homer Simpson’s console in the episode where he’s so fat he can work at home.
From this episode we can infer the following about the software:
- When the program boots, it asks you to press any key.
- The program will ask you questions that can be answered by
yes/no
, but alsoY/N
ory/n
- Most questions can be turned into commands. Each assertion is equivalent to
answering a given question positively. For example,
Vent radioactive gas? Yes/No
can be turned into theVent gas
command. - Nothing should go wrong if you keep pressing
Y
all the time - After a given delay, a new question is asked
- Too many times without venting radioactive gas risks exploding everything
- Some behaviours aren’t defined by the TV show, so we go somewhat anyway we feel like
Out of this, a finite-state machine can be created. The one that follows explains what I understood as possible, but you’ll notice I’m not really good at having a consistent notation for events, states, and so on:
[press any key]
|
(key pressed)
|
[check core temperature (first)]
\________,________/
|
(yes/no)
|
[venting radioactive gases (first)]
| |
(yes) ,-<-, (no)
| | | |
[gas blows away crop] | [venting prevents explosions]
| | | |
| '--<-(yes) (no)
\ /
\______________,_____________/
V
|
[wait for command]<--------,
/ \ |
(get data) (timeout) |
| | |
| [ask question] |
| / \ |
| (Yes) (No) |
| / | |
+----' '-----+
| |
[show result] -->-------'
Based on this, we’ll be able to draw up a first prototype with all the required state transitions. I’ve also looked for transcripts of the show and extracted the following questions and consequences:
Check core temperature. yes/no:
yes
: Core temperature normal.no
: –
Vent radioactive gas?
yes
: *gas blows away corn crop*no
: venting prevents explosion (allowyes
, show only the first time?)
Sound alertness horn?
yes
: *horn sounds in the distance*no
: –
Decalcify calcium ducts?
yes
: –no
: –
Special case: after denying venting too many times, the valve must be disabled manually.
The simplest way to write a basic FSM for this one is to use a bunch of function calls. Given Erlang has last call optimization (a call that happens as a return value does not leave a stack trace, and therefore can happen infinitely many times), this is more than adequate.
The sequence of states a -> b -> c
can be programmed as:
a() ->
b().
b() ->
c().
c() ->
done.
Of course, there’s going to be more data in our case.
The Prototype
Our glorious application will be called ‘muumuu’. Whenever I don’t exactly know where I’m going, I decide to prototype stuff. And here I stress the importance of prototype. Despite this fact, it will often end up being in production, but yeah – that’s to be avoided.
I decide to start with the basic stuff to prototype, state transitions. I go for them in a fairly simple manner, top-down:
-module(muumuu_fsm).
-export([start/0]).
-define(MAX_NO_VENT, 5).
start() ->
%% Seed PRNG
<<A:32, B:32, C:32>> = crypto:rand_bytes(12),
random:seed(A,B,C),
wait_any_key().
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% States and Transitions %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
wait_any_key() ->
io:get_line("To Start, Press Any Key.\n> "),
first_core_check().
first_core_check() ->
case option("Check core temperature?") of
yes -> core_temperature();
no -> noop()
end,
first_gas_vent().
first_gas_vent() ->
case option("Vent radioactive gas?") of
yes -> blow_crops_away();
no -> venting_prevents_explosions()
end,
wait_for_command().
wait_for_command() ->
case wait_cmd(10000) of
timeout ->
{Opt, Yes, No} = random_option(),
case option(Opt) of
yes -> Yes();
no -> No()
end;
Cmd ->
case match_option(Cmd) of
{_, Yes, _} -> Yes();
_ -> noop
end
end,
wait_for_command().
In this bit of code, we can see our 4 main states:
wait_any_key
first_core_check
first_gas_event
wait_for_command
The rest of the code is more or less going to be events and input management to check the transitions:
- printing questions and getting responses (
option/1
) - eventually waiting for a command (
wait_cmd/1
andmatch_option/1
) - or, if it takes too long, generate an option randomly (
random_option/1
)
You can look at the code, find whatever you want about it disgusting. So that’s the general idea I want in the code. Time to add all that option management stuff:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Options and Response Handling %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
option(Prompt) ->
show_option(Prompt),
Data = io:get_line(""),
case iolist_to_binary(Data) of
<<"Y", _/binary>> -> yes;
<<"y", _/binary>> -> yes;
<<"N", _/binary>> -> no;
<<"n", _/binary>> -> no;
_ -> ambiguous
end.
show_option(Str) -> io:format("~s (Y/N)~n> ", [Str]).
wait_cmd(Timeout) ->
Parent = self(),
Pid = spawn(fun() -> Parent ! io:get_line("") end),
receive
Data -> Data
after Timeout ->
exit(Pid, kill),
timeout
end.
random_option() ->
Pos = random:uniform(tuple_size(opts())),
{_, Val} = element(Pos, opts()),
Val.
match_option(Data) ->
case [Vals || {Pattern, Vals} <- tuple_to_list(opts()),
nomatch =/= re:run(Data, Pattern, [caseless])] of
[Opt|_] -> Opt;
[] -> invalid_opt
end.
Cool. Not fantastic looking yet. Basically, an option will only fetch a line of text entered by the user, look at the first response, and return what it is. Showing the options just wraps things up so they look like a prompt.
Interestingly enough, the command has to be waited for in a different process.
The problem with this it that Erlang’s standard library doesn’t support a
timeout mode for io
operations, which would tell us “wait 10 seconds for
input or quit”. Therefore, there is a need to move this to a process.
The rest relies on an elusive opts()
function that apparently returns all
questions and options offered to the user:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Defining Options/Events %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
opts() ->
{{"(check|core|temp)",
{"Check core temperature?",
fun core_temperature/0,
fun noop/0}},
{"(vent|rad|gas)",
{"Vent radioactive gas?",
fun vent_gas/0,
fun no_venting/0}},
{"(sound|alert|horn)",
{"Sound alertness horn?",
fun sound_horn/0,
fun noop/0}},
{"(calc|duct)",
{"Decalcify calcium ducts?",
fun noop/0,
fun noop/0}}}.
This basically is a tuple (I use a tuple because it makes random selection with
a fixed position more efficient) of all questions, positive and negative
response and consequences, paired up with a regular expression that represents
fuzzy matching – for example, someone typing it check temperature
should
match Check core temperature?
as a question, and return both options. The
code back in wait_for_command/0
will only execute the core_temperature/0
function.
Finally, all actions and consequences can be implemented:
noop() -> ok.
venting_prevents_explosions() ->
case option("Venting prevents explosion.") of
yes -> blow_crops_away();
no -> noop()
end.
core_temperature() -> io:format("Core temperature normal.~n").
blow_crops_away() -> io:format("*Gas blows away corn crop*~n").
sound_horn() -> io:format("*horn sounds in the distance*~n").
pressure_too_high() -> io:format("Pressure too high. Tank must be shut down manually.~n").
vent_gas() ->
%% After ?MAX_NO_VENT, pressure has to be shut down
%% manually -- unsupported in this here program!
case get(missed) of
?MAX_NO_VENT ->
pressure_too_high();
_ ->
put(missed, 0),
blow_crops_away()
end.
no_venting() ->
case get(missed) of
undefined -> put(missed, 1);
N -> put(missed, N+1)
end.
Here the two last functions implement the special last requirement: after denying venting too many times, the valve must be disabled manually.
Here we use a dirty ugly counter for prototyping’s sake. In fact I had forgotten about that requirement at the time and just bolted it on that way. The prototype helped figure that requirement out, and the final version can now be designed with this in mind.
You can run the code and try it from a shell:
λ → erlc src/muumuu_fsm.erl && erl -s muumuu_fsm -noshell
To Start, Press Any Key.
> .
Check core temperature? (Y/N)
> N
Vent radioactive gas? (Y/N)
> No
Venting prevents explosion. (Y/N)
> yes
*Gas blows away corn crop*
Sound alertness horn? (Y/N)
> Y
*horn sounds in the distance*
That works. Using -s <module>
runs the start/0
function from that module,
and using -noshell
makes it so that the Erlang VM won’t fight with all the
io
calls I’m doing for user input ownership.
Sadly, the implementation is kind of ugly and shouldn’t go in production.
Making it a library
There are two ways to make something reach production: distributing yourself, or distributing it as a library other Erlang developers can use.
The latter can be a prerequisite for the former, so we’re going to start there. By default, everyone using Erlang in the open source community uses OTP applications.
OTP is kind of often treated as a super advanced topic, so what I’m gonna show here is how to take any non-OTP compliant code and turn it into an OTP application. Fun fun.
First, the directory structure:
src/
- muumuu_fsm.erl
That’s all you need in terms of structure if you have rebar3 installed in your system.
Add a file in src/
called muumuu.app.src
. This file is basically
telling Erlang (and rebar3) what the library is:
{application, muumuu, [
{description, "Too fat to go to the power plant app"},
{vsn, "0.1.0"},
{registered, []},
{applications, [kernel, stdlib, crypto]},
{mod, {muumuu_app, []}},
{env, []}
]}.
The registered
entry specifies what processes are going to be globally
registered on the node. In this case, none. The applications
tuple is a list
of all applications we depend on. All applications depend on both kernel
and
stdlib
. These entries have to always be in there. On the other hand, crypto
is optional to most apps, but we need it because we use it to seed our
pseudo-random number generator in start/0
.
The env
tuple can contain configuration
values, but we need none right now.
The other option considered here is mod
. If your library requires no process
to be started and you’re just shipping code around, you’re done. In our case
however, we’re starting a process (or we want to), and therefore we specify an
application module named muumuu_app
. This module is also in src/
:
-module(muumuu_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_Type, _Args) ->
muumuu_sup:start_link().
stop(_) ->
ok.
That module is basically giving callbacks to the Erlang VM. See it a bit as the
main
function in C, except you also have to provide a stop
function that
will clean up once the process exits. In this case we need nothing.
What’s the muumuu_sup
module? That’s the final step to be glued in OTP. OTP
has a concept called
supervisors
. Supervisors are in
charge of checking OTP-compliant processes, to start them, stop them, and
provide guarantees regarding their
state.
Unfortunately, our process isn’t OTP-compliant. The guys at Ericsson have long ago hit that problem and developed a supervisor bridge, which basically acts as a wrapper. This is what we could use if I were not the kind of person to want my OTP processes done correctly everywhere.
For the time being, I’ll stick with a regular supervisor and will rewrite the FSM right after:
-module(muumuu_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link(?MODULE, []).
init([]) ->
{ok, {{one_for_one, 1, 5},
[{console,
{muumuu_fsm, start_link, []},
permanent, 5000, worker, [muumuu_fsm]}]}}.
This will start muumuu_fsm
as a permanent worker that can die once every 5
seconds before the entire system crashes. I don’t have a good way to pick
frequencies, but 1 in 5 seconds sounds like something reasonable for someone to
mash keys in ways bad enough it causes errors.
So then comes the rewrite from prototype to
gen_fsm
. This is stuff
that has been covered in multiple tutorials before, so I’m going to skip most
of it. You can instead look at books and docs for gen_fsm
, follow along the
final module,
muumuu_fsm.erl,
and see for yourself.
The biggest changes there, outside of providing the gen_fsm
callbacks
required by the OTP behavior, are related to the general information flow.
Rather than being really direct sequences of functions doing whatever they
want, the OTP version of the module becomes a lot more declarative.
We no longer enter a state function, ask a question, and wait for the response
within the same context. The logic has moved so that an event in a state (say
first_gas_vent
) causes a question to be asked before transitioning to the
state that will handle that response.
This doesn’t make the code particulalry harder to read, just different:
init([]) ->
<<A:32, B:32, C:32>> = crypto:rand_bytes(12),
random:seed(A,B,C),
{ok, wait_any_key, prompt(wait_any_key, #state{})}.
%% [...]
wait_any_key(_, State) ->
{next_state, first_core_check, prompt(first_core_check, State)}.
first_core_check(no, State) ->
{next_state, first_gas_vent, prompt(first_gas_vent, State)};
first_core_check(yes, State) ->
show_core_temperature(),
{next_state, first_gas_vent, prompt(first_gas_vent, State)}.
first_gas_vent(no, State) ->
StateName = venting_prevents_explosions,
{next_state, StateName, prompt(StateName, State)};
first_gas_vent(yes, State) ->
show_blow_crops_away(),
{next_state, wait_for_command, prompt(wait_for_command, State), 10000}.
This form, along with the experience gained in the prototype, allows for
simpler state management via the State
variable, which allows us to be more
transparent about our usage of venting limits, for example. We also instantly
benefit from everything OTP gives us in terms of transparency: tracing,
logging, statistics, and so on (see the sys
module)
With that code in place, we can compile and run the entire application:
λ → rebar3 compile
===> Verifying dependencies...
===> Compiling muumuu
With this compiled we can run it, with a funky command:
λ → erl -env ERL_LIBS _build/default/lib -eval 'application:ensure_all_started(muumuu).' -noshell
To Start, Press Any Key.
> any
Check core temperature? (Y/N)
> y
Core temperature normal.
Vent radioactive gas? (Y/N)
> y
*Gas blows away corn crop*
That’s kind of an ugly command to run the app, but the app is now something other people can use to pull it within their own systems.
In order to run it ourselves and actually ship it to customers, we will need to build a release. In any other case, though, you may want to publish your library as a Hex package with the help of the proper rebar3 plugin.
Releases
The directory structure we’ve been using was for an application and turns out looking like:
src/
ebin/
At the simplest level. A release is basically a group of applications put together. For this reason, we’ll change the directory structure a bit:
apps/
- muumuu/
- src/
- ebin/
rebar.config
All applications you need will go into apps/
. Here I just moved src/
to
apps/muumuu/
.
The rebar.config file looks like this:
{relx, [
{release, {muumuu, "1.0.0"},
%% list of apps to include
[muumuu]},
%% Don't ship an Erlang VM by default
{include_erts, false}
]}.
{profiles, [
%% called as `rebar3 as prod <command>`
{prod, [
{relx, [ % override relx specifically
{include_src, false}, % don't include source code
{include_erts, true} % include the VM in the release
]}
]}
]}.
This basically just tells rebar3 what the release-building tool it includes (relx) should do to give us our release. The release will only include our custom Erlang code, and use the currently installed Erlang VM to run things rather than installing a fully self-contianed program. Then the magic happens:
λ → rebar3 release
===> Verifying dependencies...
===> Compiling muumuu
===> Starting relx build process ...
===> Resolving OTP Applications from directories:
/Users/ferd/code/self/howistart-erlang1-code/release/_build/default/lib
/Users/ferd/code/self/howistart-erlang1-code/release/apps
/Users/ferd/.kerl/builds/17.4/release_17.4/lib
===> Resolved muumuu-1.0.0
===> release successfully created!
And a release is born! To run it:
λ → ./_build/default/rel/muumuu/bin/muumuu -noshell
To Start, Press Any Key.
>
Pretty cool. This can now be shipped and distributed to people.
I want to make the release a bit fancier though. As you’ve just seen, we still
need to put the -noshell
by hand, which is totally unacceptable.
To fix this, add a config/
repository, and I open the vm.args
file in vim in
there:
# only show the programmed prompt
-noshell
# for remote access & debugging
-name [email protected]
# not needed
-smp disable
+A 1
Arguments in there I merged into one. A good practice for any Erlang system is to give it a name, which will let you connect to it while it’s running. In this case I could go in and debug the console as the user is maintaining the powerplant.
The last arguments (-smp disable +A 1
) are basically optimizations for this
very app: they remove Erlang parallelism (I’m running a single active process
for the thing, so why bother?) and removes the number of asynchronous threads
for IO to a single one (for the same reason – one active process, why
bother?).
In more serious apps, tweaking your VM options can be worthwhile, but outside of this text’s scope.
The rebar3 config file needs an update too:
{relx, [
{release, {muumuu, "1.0.0"},
%% list of apps to include
[muumuu]},
%% Don't ship an Erlang VM by default
{include_erts, false},
{vm_args, "./config/vm.args"}
]}.
{profiles, [
%% called as `rebar3 as prod <command>`
{prod, [
{relx, [ % override relx specifically
{include_src, false}, % don't include source code
{include_erts, true} % include the VM in the release
]}
]}
]}.
The last line above the profiles is the new one. Compile again and the arguments should implicitly be passed to the node:
λ → rebar3 release
===> Verifying dependencies...
===> Compiling muumuu
===> Starting relx build process ...
===> Resolving OTP Applications from directories:
/Users/ferd/code/self/howistart-erlang1-code/release/_build/default/lib
/Users/ferd/code/self/howistart-erlang1-code/release/apps
/Users/ferd/.kerl/builds/17.4/release_17.4/lib
/Users/ferd/code/self/howistart-erlang1-code/release/_build/default/rel
===> Resolved muumuu-1.0.0
===> release successfully created!
λ → ./_build/default/rel/muumuu/bin/muumuu
To Start, Press Any Key.
> <Tab>
Check core temperature? (Y/N)
>
Cool, everything works. I now have a binary executable I can link to from anywhere in the system and will require no magical arguments to work!
Tests
As much as I like to try and get testing done ahead of time – it’s the only time it’s not super terrible and crappy – I often end up adding it after the fact when I know I’ll have to maintain it.
For this, each app should have its tests, so I’ll have to add a test/
directory in apps/muumuu/
.
My tool of choice is Common Test, which while it is kind of full of annoying overheads for unit testing and is mostly useless for shell output (you gotta deal with HTML files), it scales fairly well for integration and system tests.
The test suite in there is going to be muumuu_SUITE.erl
:
-module(muumuu_SUITE).
-include_lib("common_test/include/ct.hrl").
-compile(export_all).
%% Copy/pasting from the suite
-record(state, {no_vent_count=0,
pid,
yes,
no}).
all() ->
[demo_session].
So at first I’m just gonna make one run-through test. Testing muumuu
is going
to be hard because it’s purely a side-effectful application.
Before going further, I’ll say that the trick to getting this working is to use
meck
, which is pretty much the best code-mocking application around.
Adding meck
can be done by declaring rebar.config
dependencies:
{profiles, [
{test, [
{deps, [
{meck, "0.8.2"}
]}
]},
%% called as `rebar3 as prod <command>`
{prod, [
...
]}
]}
]}.
Note that rather than having a top-level deps
entry as we usually would, we
define this one to be into the test
profile. This will allow the dependency
to only be fetched and used when running tests, and to avoid bundling it when
shipping the application.
Rebar3 pulls stuff from a package repository for this one (github dependencies are also an option). Rebar3 will add it to a lock file when it fetches and compiles it later.
Now back to muumuu_SUITE
. Time to set up the state:
init_per_testcase(demo_session, Config) ->
mock_io(),
{ok, Pid} = muumuu_fsm:start_link(),
[{pid, Pid} | Config].
end_per_testcase(_, Config) ->
meck:unload(io),
Pid = ?config(pid, Config),
unlink(Pid),
exit(Pid, shutdown),
wait_for_death(Pid).
Mocking the io
system is a fun way to basically take it and make it return
messages we can look at. That all takes place in mock_io()
, and after that’s
in place, we start a muumuu
instance directly (no application needed):
mock_io() ->
%% For this one we mock the IO system so that instead of
%% printing messages and getting input to and from the user,
%% we instead have a message-passing interface that will
%% be inspectable.
%%
%% Note that because the `io` module is pre-compiled by the
%% VM, we have to 'unstick' it first, and be careful to keep
%% it mocked as little as possible.
Parent = self(),
code:unstick_dir(filename:dirname(code:where_is_file("io.beam"))),
meck:new(io, [passthrough, no_link]),
meck:expect(io, format, fun(Str) ->
Parent ! {out, Str},
ok
end),
meck:expect(io, format, fun(Str, Args) ->
Parent ! {out, io_lib:format(Str,Args)},
ok
end),
meck:expect(io, get_line, fun(_Prompt) ->
Parent ! {in, self()},
receive {Parent, In} -> In end
end).
Ugly. The first step is unstickying the directory for Erlang code. Most modules
don’t require that, only those in Erlang’s standard library. Unstickying allows
to load new versions of code at run time, which meck
dynamically does.
Here what I’m doing is mocking the functions io:format/1
, io:format/2
and
io:get_line/1
to send messages of the form {in, Msg}
and {out, Msg}
from
input and output, respectively. meck:unload(io)
will undo that.
We also had the wait_for_death/1
call. I’m using these everywhere in tests.
Timers are the enemy of good concurrent testing, and if you rely on a
timer:sleep(1000)
of some sort to make sure everything is clean, you’re doing
it wrong.
Here the function polls to return ASAP, with a tiny sleep to not heat up your room too much via the CPU:
wait_for_death(Pid) ->
case is_process_alive(Pid) of
true ->
timer:sleep(10),
wait_for_death(Pid);
false ->
ok
end.
With this done, I can start planning more for the test. This here is something I always want to write a library for, and maybe some day I will, but right now I re-do that crap by hand every time:
%%%%%%%%%%%%%%%%%%
%%% TEST CASES %%%
%%%%%%%%%%%%%%%%%%
%% Pressing a given key through the message-passing interface
%% will yield expected output. There should be a prompt waiting
%% for a key.
%% All states can be cycled through using only Y/N answers.
demo_session(Config) ->
Pid = ?config(pid, Config),
out("press.*any.*key.*>"),
in("<tab>"), % the characters shouldn't matter
out("check.*core.*temp.*>"),
in("Y"),
out("temperature.*normal"),
out("vent.*radioactive.*gas.*>"),
in("no"),
out("venting.*prevents.*explosion.*>"),
in("yES"),
out("gas.*blows.*crop.*"),
gen_fsm:send_event(Pid, timeout), % force a timeout faster
out(".*Y/N.*>"), % some question
in("No"), % who cares
in("vent gAs"), % force a command
out("gas.*blows.*crop.*").
I basically just write the test the way I want it to look like. I will start
expecting messages that will match the regex "press.*any.*key.*>"
being
output, after which I’ll insert <tab>
. Rinse and repeat.
Here, my desire is pretty much to turn the interactions I’d write in the shell into a bunch of function calls and matches.
That’s why I planned having a message-passing interface. I can now write functions to wrap that functionality:
%%%%%%%%%%%%%%%
%%% HELPERS %%%
%%%%%%%%%%%%%%%
in(Input) ->
receive
{in, Pid} -> Pid ! {self(), Input}
after 1000 ->
ct:pal("MBOX: ~p", [process_info(self(), messages)]),
error({too_long, {in, Input}})
end.
If we look back into the mocked function, the mocked function sends us {in, ProcessThatWaitsForInput}
. We take the Input
argument, and send it back to
the mocked function (which runs in its own process).
If we never receive the in
message, we crash, but printing the debugging
information. Interestingly here the function I use is ct:pal
. It works exactly
like io:format
, except:
- It outputs to both the shell and HTML logs for Common Test
- It’s not gonna be used in production systems and it’s surely never going to
be mocked (unlike
io
).
The out/1
helper is slightly more complex:
%% fuzzily match the input string, waiting 1s at most
out(Expected) ->
receive
{out, Prompt} ->
ct:pal("Expected: ~p~nPrompt: ~p", [Expected, Prompt]),
{match, _} = re:run(Prompt, Expected, [dotall, caseless, global])
after 1000 ->
ct:pal("MBOX: ~p", [process_info(self(), messages)]),
error({too_long, {out, Expected}})
end.
That one makes an assertion on a regular expression with re:run/3
, and the
rest is similar to what we did in in/1
. We receive the output, match it, and
that’s it.
And there we go, we can run the tests:
λ → rebar3 ct
→ rebar3 ct
===> Verifying dependencies...
===> Fetching meck ({pkg,<<"meck">>,<<"0.8.2">>})
===> Compiling meck
===> Compiling muumuu
===> Running Common Test suites...
<test output omitted>
All 1 tests passed.
After this, I check in the rebar lock files into version control, and I go do something else because I’m pretty much done. You can see all the code here.