take_a_number_deluxe
This commit is contained in:
parent
5203753bd5
commit
def44c9d46
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"angelikatyborska"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"jiegillet"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/take_a_number_deluxe.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/take_a_number_deluxe_test.exs"
|
||||||
|
],
|
||||||
|
"exemplar": [
|
||||||
|
".meta/exemplar.ex"
|
||||||
|
],
|
||||||
|
"editor": [
|
||||||
|
"lib/take_a_number_deluxe/state.ex",
|
||||||
|
"lib/take_a_number_deluxe/queue.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language_versions": ">=1.10",
|
||||||
|
"icon": "take-a-number",
|
||||||
|
"blurb": "Learn about GenServers by writing an embedded system for a Take-A-Number machine."
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"take-a-number-deluxe","id":"0c30c0b2b5744d3ca17d64d02cb34d6d","url":"https://exercism.org/tracks/elixir/exercises/take-a-number-deluxe","handle":"negrienko","is_requester":true,"auto_approve":false}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
nil-*.tar
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Help
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
From the terminal, change to the base directory of the exercise then execute the tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix test
|
||||||
|
```
|
||||||
|
|
||||||
|
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
|
||||||
|
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
|
||||||
|
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
|
||||||
|
|
||||||
|
## Pending tests
|
||||||
|
|
||||||
|
In test suites of practice exercises, all but the first test have been tagged to be skipped.
|
||||||
|
|
||||||
|
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# @tag :pending
|
||||||
|
test "shouting" do
|
||||||
|
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix test --include pending
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# ExUnit.configure(exclude: :pending, trace: true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful `mix test` options
|
||||||
|
|
||||||
|
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
|
||||||
|
* `--failed` - runs only tests that failed the last time they ran
|
||||||
|
* `--max-failures` - the suite stops evaluating tests when this number of test failures
|
||||||
|
is reached
|
||||||
|
* `--seed 0` - disables randomization so the tests in a single file will always be ran
|
||||||
|
in the same order they were defined in
|
||||||
|
|
||||||
|
## Submitting your solution
|
||||||
|
|
||||||
|
You can submit your solution using the `exercism submit lib/take_a_number_deluxe.ex` command.
|
||||||
|
This command will upload your solution to the Exercism website and print the solution page's URL.
|
||||||
|
|
||||||
|
It's possible to submit an incomplete solution which allows you to:
|
||||||
|
|
||||||
|
- See how others have completed the exercise
|
||||||
|
- Request help from a mentor
|
||||||
|
|
||||||
|
## Need to get help?
|
||||||
|
|
||||||
|
If you'd like help solving the exercise, check the following pages:
|
||||||
|
|
||||||
|
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
|
||||||
|
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
|
||||||
|
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
|
||||||
|
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
|
||||||
|
|
||||||
|
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
|
||||||
|
|
||||||
|
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Hints
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Read about `GenServer` in the official [Getting Started guide][getting-started-genserver].
|
||||||
|
- Read about `GenServer` on [elixirschool.com][elixir-school-genserver].
|
||||||
|
- Read about the `GenServer` behaviour [in the documentation][genserver].
|
||||||
|
|
||||||
|
## 1. Start the machine
|
||||||
|
|
||||||
|
- Remember to [use][use] the [`GenServer` behaviour][genserver].
|
||||||
|
- There is [a built-in function][start-link] that starts a linked `GenServer` process. The only thing that `TakeANumberDeluxe.start_link/2` needs to do is call that function with the right arguments.
|
||||||
|
- `__MODULE__` is a special variable that holds the name of the current module.
|
||||||
|
- Implement the [`GenServer` callback used when starting the process][init].
|
||||||
|
- The callback should return either `{:ok, state}` or `{:stop, reason}`.
|
||||||
|
- Read the options from the `init_arg` keyword list.
|
||||||
|
- There is [a built-in function][keyword-get] to get a value from a keyword list.
|
||||||
|
- Use `TakeANumberDeluxe.State.new/2` to get the initial state.
|
||||||
|
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from.
|
||||||
|
|
||||||
|
## 2. Report machine state
|
||||||
|
|
||||||
|
- There is [a built-in function][call] that sends a message to a `GenServer` process and receives a reply. The only thing that `TakeANumberDeluxe.report_state/1` needs to do is call that function with the right arguments.
|
||||||
|
- The messages sent to a server can be anything, but atoms are best.
|
||||||
|
- Implement the [`GenServer` callback used when handling messages that need a reply][handle-call].
|
||||||
|
- The callback should return `{:reply, reply, state}`.
|
||||||
|
- Pass the state as the reply.
|
||||||
|
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from.
|
||||||
|
|
||||||
|
## 3. Queue new numbers
|
||||||
|
|
||||||
|
- There is [a built-in function][call] that sends a message to a `GenServer` process and receives a reply. The only thing that `TakeANumberDeluxe.queue_new_number/1` needs to do is call that function with the right arguments.
|
||||||
|
- The messages sent to a server can be anything, but atoms are best.
|
||||||
|
- Implement the [`GenServer` callback used when handling messages that need a reply][handle-call].
|
||||||
|
- The callback should return `{:reply, reply, state}`.
|
||||||
|
- Get the reply and the new state by calling `TakeANumberDeluxe.State.queue_new_number/1`. Use a [`case`][case] expression to pattern match the return value.
|
||||||
|
- The reply should be either `{:ok, new_number}` or `{:error, error}`.
|
||||||
|
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from.
|
||||||
|
|
||||||
|
## 4. Serve next queued number
|
||||||
|
|
||||||
|
- There is [a built-in function][call] that sends a message to a `GenServer` process and receives a reply. The only thing that `TakeANumberDeluxe.serve_next_queued_number/2` needs to do is call that function with the right arguments.
|
||||||
|
- The messages sent to a server can be anything, but tuples are best if an argument needs to be sent in the message. Use a message like this: `{:my_message_name, some_argument}`.
|
||||||
|
- Implement the [`GenServer` callback used when handling messages that need a reply][handle-call].
|
||||||
|
- The callback should return `{:reply, reply, state}`.
|
||||||
|
- Get the reply and the new state by calling `TakeANumberDeluxe.State.serve_next_queued_number/2`. Use a [`case`][case] expression to pattern match the return value.
|
||||||
|
- The reply should be either `{:ok, next_number}` or `{:error, error}`.
|
||||||
|
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from.
|
||||||
|
|
||||||
|
## 5. Reset state
|
||||||
|
|
||||||
|
- There is [a built-in function][cast] that sends a message to a `GenServer` process and does not wait for a reply. The only thing that `TakeANumberDeluxe.reset_state/1` needs to do is call that function with the right arguments.
|
||||||
|
- The messages sent to a server can be anything, but atoms are best.
|
||||||
|
- Implement the [`GenServer` callback used when handling messages that do not need a reply][handle-cast].
|
||||||
|
- The callback should return `{:noreply, state}`.
|
||||||
|
- Use `TakeANumberDeluxe.State.new/2` to get the new state, just like in `init/1`.
|
||||||
|
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from.
|
||||||
|
|
||||||
|
## 6. Implement auto shutdown
|
||||||
|
|
||||||
|
- Extend all `init/1` and `handle_*` callbacks to return one extra element in their tuples. Its value should be `state.auto_shutdown_timeout`.
|
||||||
|
- The return value `{:stop, reason}` of `init/1` does not need a timeout.
|
||||||
|
- Implement the [`GenServer` callback used when handling messages that weren't sent in the usual `GenServer` way][handle-info].
|
||||||
|
- This callback needs to handle `:timeout` messages and exit the process, but also catch and ignore any other messages.
|
||||||
|
- To exit a GenServer process, return `{:stop, reason, state}` from the callback.
|
||||||
|
- The exit reason should be `:normal`.
|
||||||
|
|
||||||
|
[getting-started-genserver]: https://hexdocs.pm/elixir/genservers.html
|
||||||
|
[elixir-school-genserver]: https://elixirschool.com/en/lessons/advanced/otp_concurrency
|
||||||
|
[genserver]: https://hexdocs.pm/elixir/GenServer.html
|
||||||
|
[use]: https://hexdocs.pm/elixir/Kernel.html#use/2
|
||||||
|
[impl]: https://hexdocs.pm/elixir/Module.html#module-impl
|
||||||
|
[start-link]: https://hexdocs.pm/elixir/GenServer.html#start_link/3
|
||||||
|
[call]: https://hexdocs.pm/elixir/GenServer.html#call/2
|
||||||
|
[cast]: https://hexdocs.pm/elixir/GenServer.html#cast/2
|
||||||
|
[init]: https://hexdocs.pm/elixir/GenServer.html#c:init/1
|
||||||
|
[handle-call]: https://hexdocs.pm/elixir/GenServer.html#c:handle_call/3
|
||||||
|
[handle-cast]: https://hexdocs.pm/elixir/GenServer.html#c:handle_cast/2
|
||||||
|
[handle-info]: https://hexdocs.pm/elixir/GenServer.html#c:handle_info/2
|
||||||
|
[keyword-get]: https://hexdocs.pm/elixir/Keyword.html#get/3
|
||||||
|
[case]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#case/2
|
|
@ -0,0 +1,293 @@
|
||||||
|
# Take-A-Number Deluxe
|
||||||
|
|
||||||
|
Welcome to Take-A-Number Deluxe on Exercism's Elixir Track.
|
||||||
|
If you need help running the tests or submitting your code, check out `HELP.md`.
|
||||||
|
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
## GenServer
|
||||||
|
|
||||||
|
`GenServer` (generic server) is a [behaviour][concept-behaviours] that abstracts common client-server interactions between Elixir processes.
|
||||||
|
|
||||||
|
Remember the receive loop from when we learned about [processes][concept-processes]? The `GenServer` behaviour provides abstractions for implementing such loops, and for exchanging messages with a process that runs such a loop. It makes it easier to keep state and execute asynchronous code.
|
||||||
|
|
||||||
|
~~~~exercism/note
|
||||||
|
Be warned that the name `GenServer` is loaded. It is also used to describe a _module_ that _uses_ the `GenServer` behaviour, as well as a _process_ that was started from a module that _uses_ the `GenServer` behaviour.
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
The `GenServer` behaviour defines one required callback, `init/1`, and a few interesting optional callbacks: `handle_call/3`, `handle_cast/2`, and `handle_info/3`. The _clients_ using a `GenServer` aren't supposed to call those callbacks directly. Instead, the `GenServer` module provides functions that clients can use to communicate with a `GenServer` process.
|
||||||
|
|
||||||
|
Often, a single module defines both a _client API_, a set of functions that other parts of your Elixir app can call to communicate with this `GenServer` process, and _server callback implementations_, which contain this `GenServer`'s logic.
|
||||||
|
|
||||||
|
Let's take a look at a simple example of a `GenServer` first, and then learn what each callback means.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
This is an example server that can respond to the repetitive inquisitions of annoying passengers during a long road trip, more exactly the question: "are we there yet?". It keeps track of how many times this question has been asked, returning increasingly more annoyed responses.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule AnnoyingPassengerAutoresponder do
|
||||||
|
use GenServer
|
||||||
|
# Client API
|
||||||
|
|
||||||
|
def start_link(init_arg) do
|
||||||
|
GenServer.start_link(__MODULE__, init_arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
def are_we_there_yet?(pid) do
|
||||||
|
GenServer.call(pid, :are_we_there_yet?)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Server callbacks
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def init(_init_arg) do
|
||||||
|
# the initial count of questions asked is always 0
|
||||||
|
state = 0
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call(:are_we_there_yet?, _from, state) do
|
||||||
|
reply =
|
||||||
|
cond do
|
||||||
|
state <= 3 -> "No."
|
||||||
|
state <= 10 -> "I told you #{state} times already. No."
|
||||||
|
true -> "..."
|
||||||
|
end
|
||||||
|
|
||||||
|
# increase the count of questions asked
|
||||||
|
new_state = state + 1
|
||||||
|
# reply to the caller
|
||||||
|
{:reply, reply, new_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Callbacks
|
||||||
|
|
||||||
|
#### `init/1`
|
||||||
|
|
||||||
|
A server can be started by calling `GenServer.start/3` or `GenServer.start_link/3`. We learned about the difference between those functions in the [links concept][concept-links].
|
||||||
|
|
||||||
|
Those two functions:
|
||||||
|
|
||||||
|
- Accept a module implementing the `GenServer` behaviour as the first argument.
|
||||||
|
- Accept anything as the second argument called `init_arg`. As the name suggest, this argument gets passed to the `init/1` callback.
|
||||||
|
- Accept an optional third argument with advanced options for running the process that we won't cover now.
|
||||||
|
|
||||||
|
Starting a server by calling `GenServer.start/3` or `GenServer.start_link/3` will invoke the `init/1` callback in a blocking way. The return value of `init/1` dictates if the server can be started successfully.
|
||||||
|
|
||||||
|
The `init/1` callback usually returns one of those values:
|
||||||
|
|
||||||
|
- `{:ok, state}`. The server will start its receive loop using `state` as its initial state. `state` can be of any type.
|
||||||
|
- `{:stop, reason}`. `reason` can be of any type. The server will not start its receive loop. The process will exit with the given reason.
|
||||||
|
|
||||||
|
There are also more advanced possibilities that we won't cover now.
|
||||||
|
|
||||||
|
If the server's receive loop starts, the functions `GenServer.start/3` and `GenServer.start_link/3` return an `{:ok, pid}` tuple. Otherwise they return `{:error, reason}`
|
||||||
|
|
||||||
|
#### `handle_call/3`
|
||||||
|
|
||||||
|
A message that requires a reply can be sent to a server process with `GenServer.call/2`. This function expects the `pid` of a running server process as the first argument, and the message as the second argument. The message can be of any type.
|
||||||
|
|
||||||
|
The `handle_call/3` callback is responsible for handling and responding to synchronous messages. It receives three arguments:
|
||||||
|
|
||||||
|
1. `message` - the value passed as the second argument to `GenServer.call/2`.
|
||||||
|
2. `from` - the `pid` of the process calling `GenServer.call/2`. Most often this argument can be ignored.
|
||||||
|
3. `state` - the current state of the server. Remember that its initial value was set in the `init/1` callback.
|
||||||
|
|
||||||
|
The `handle_call/3` callback usually returns a 3 tuple of `{:reply, reply, state}`. This means that the second element in the tuple, a `reply` that can be of any type, will be sent back to the caller. The third element in the tuple, `state`, is the new state of the server after handling this message.
|
||||||
|
|
||||||
|
There are also more advanced possibilities that we won't cover now.
|
||||||
|
|
||||||
|
~~~~exercism/note
|
||||||
|
To memorize what this callback does by its name,
|
||||||
|
think of it as "calling" somebody on the phone.
|
||||||
|
|
||||||
|
If that person is available, you'll receive a reply immediately (synchronously).
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
#### `handle_cast/2`
|
||||||
|
|
||||||
|
A message that doesn't require a reply can be sent to a server process with `GenServer.cast/2`. Its arguments are identical to those of `GenServer.call/2`.
|
||||||
|
|
||||||
|
The `handle_cast/2` callback is responsible for handling those messages. It receives two arguments, `message` and `state`, which are the same arguments as in the `handle_call/3` callback (except for `from`).
|
||||||
|
|
||||||
|
The `handle_cast/2` callback usually returns a 2 tuple of `{:noreply, state}`.
|
||||||
|
|
||||||
|
There are also more advanced possibilities that we won't cover now.
|
||||||
|
|
||||||
|
~~~~exercism/note
|
||||||
|
To memorize what this callback does by its name,
|
||||||
|
remember that "to cast" also means "to throw".
|
||||||
|
|
||||||
|
If you throw a message in a bottle into the sea,
|
||||||
|
you don't expect to receive a reply immediately,
|
||||||
|
or maybe ever.
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
#### Should I use `call` or `cast`?
|
||||||
|
|
||||||
|
Almost always use `call` even if your client code doesn't need the reply from the server.
|
||||||
|
|
||||||
|
Using `call` waits for the reply, which serves as a backpressure mechanism (to prevent clients from sending too many messages at once). Receiving a reply from the server is also the only way to be sure that the server received and handled the client's message.
|
||||||
|
|
||||||
|
#### `handle_info/2`
|
||||||
|
|
||||||
|
Messages can also end up in the server's inbox by means other than calling `GenServer.call/2` or `GenServer.cast/2`, for example calling the plain `send/2` function.
|
||||||
|
|
||||||
|
To handle such messages, use the `handle_info/2` callback. This callback works in exactly the same way as `handle_cast/2`.
|
||||||
|
|
||||||
|
The `GenServer` behaviour provides a catch-all implementation of `handle_info/2` that logs errors about unexpected messages. If you override that default implementation, make sure to always include your own catch-all implementation. If you forget, the server will crash if it receives an unexpected message.
|
||||||
|
|
||||||
|
### Timeouts
|
||||||
|
|
||||||
|
The return value of each of the four callbacks described above can be extended by one more tuple element, a timeout. E.g. instead of returning `{:ok, state}` from `init/1`, return `{:ok, state, timeout}`.
|
||||||
|
|
||||||
|
The timeout can be used to detect a lack of messages in the mailbox for a specific period. If the server returns a timeout from one of its callbacks, and the specified number of milliseconds have elapsed with no message arriving, `handle_info/2` is called with `:timeout` as the first argument.
|
||||||
|
|
||||||
|
[concept-behaviours]: https://exercism.org/tracks/elixir/concepts/behaviours
|
||||||
|
[concept-processes]: https://exercism.org/tracks/elixir/concepts/processes
|
||||||
|
[concept-links]: https://exercism.org/tracks/elixir/concepts/links
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
The basic Take-A-Number machine was selling really well, but some users were complaining about its lack of advanced features compared to other models available on the market.
|
||||||
|
|
||||||
|
The manufacturer listened to user feedback and decided to release a deluxe model with more features, and you once again were tasked with writing the software for this machine.
|
||||||
|
|
||||||
|
The new features added to the deluxe model include:
|
||||||
|
- Keeping track of currently queued numbers.
|
||||||
|
- Setting the minimum and maximum number. This will allow using multiple deluxe Take-A-Number machines for queueing customers to different departments at the same facility, and to tell apart the departments by the number range.
|
||||||
|
- Allowing certain numbers to skip the queue to provide priority service to pregnant women and the elderly.
|
||||||
|
- Auto shutdown to prevent accidentally leaving the machine on for the whole weekend and wasting energy.
|
||||||
|
|
||||||
|
The business logic of the machine was already implemented by your colleague and can be found in the module `TakeANumberDeluxe.State`. Now your task is to wrap it in a `GenServer`.
|
||||||
|
|
||||||
|
## 1. Start the machine
|
||||||
|
|
||||||
|
Use the `GenServer` behaviour in the `TakeANumberDeluxe` module.
|
||||||
|
|
||||||
|
Implement the `start_link/1` function and the necessary `GenServer` callback.
|
||||||
|
|
||||||
|
The argument passed to `start_link/1` is a keyword list. It contains the keys `:min_number` and `:max_number`. The values under those keys need to be passed to the function `TakeANumberDeluxe.State.new/2`.
|
||||||
|
|
||||||
|
If `TakeANumberDeluxe.State.new/2` returns an `{:ok, state}` tuple, the machine should start, using the returned state as its state. If it returns an `{:error, error}` tuple instead, the machine should stop, giving the returned error as the reason for stopping.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
TakeANumberDeluxe.start_link(min_number: 1, max_number: 9)
|
||||||
|
# => {:ok, #PID<0.174.0>}
|
||||||
|
|
||||||
|
TakeANumberDeluxe.start_link(min_number: 9, max_number: 1)
|
||||||
|
# => {:error, :invalid_configuration}
|
||||||
|
```
|
||||||
|
|
||||||
|
You might have noticed that the function `TakeANumberDeluxe.State.new/2` also takes an optional third argument, `auto_shutdown_timeout`. We will use it in the last step of this exercise.
|
||||||
|
|
||||||
|
## 2. Report machine state
|
||||||
|
|
||||||
|
Implement the `report_state/1` function and the necessary `GenServer` callback. The machine should reply to the caller with its current state.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
|
||||||
|
TakeANumberDeluxe.report_state(machine)
|
||||||
|
# => %TakeANumberDeluxe.State{
|
||||||
|
# max_number: 10,
|
||||||
|
# min_number: 1,
|
||||||
|
# queue: %TakeANumberDeluxe.Queue{in: [], out: []},
|
||||||
|
# auto_shutdown_timeout: :infinity,
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Queue new numbers
|
||||||
|
|
||||||
|
Implement the `queue_new_number/1` function and the necessary `GenServer` callback.
|
||||||
|
|
||||||
|
It should call the `TakeANumberDeluxe.State.queue_new_number/1` function with the current state of the machine.
|
||||||
|
|
||||||
|
If `TakeANumberDeluxe.State.queue_new_number/1` returns an `{:ok, new_number, new_state}` tuple, the machine should reply to the caller with the new number and set the new state as its state. If it returns a `{:error, error}` tuple instead, the machine should reply to the caller with the error and not change its state.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 2)
|
||||||
|
TakeANumberDeluxe.queue_new_number(machine)
|
||||||
|
# => {:ok, 1}
|
||||||
|
|
||||||
|
TakeANumberDeluxe.queue_new_number(machine)
|
||||||
|
# => {:ok, 2}
|
||||||
|
|
||||||
|
TakeANumberDeluxe.queue_new_number(machine)
|
||||||
|
# => {:error, :all_possible_numbers_are_in_use}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Serve next queued number
|
||||||
|
|
||||||
|
Implement the `serve_next_queued_number/2` function and the necessary `GenServer` callback.
|
||||||
|
|
||||||
|
It should call the `TakeANumberDeluxe.State.serve_next_queued_number/2` function with the current state of the machine and its second optional argument, `priority_number`.
|
||||||
|
|
||||||
|
If `TakeANumberDeluxe.State.serve_next_queued_number/2` returns an `{:ok, next_number, new_state}` tuple, the machine should reply to the caller with the next number and set the new state as its state. If it returns a `{:error, error}` tuple instead, the machine should reply to the caller with the error and not change its state.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
|
||||||
|
TakeANumberDeluxe.queue_new_number(machine)
|
||||||
|
# => {:ok, 1}
|
||||||
|
|
||||||
|
TakeANumberDeluxe.serve_next_queued_number(machine)
|
||||||
|
# => {:ok, 1}
|
||||||
|
|
||||||
|
TakeANumberDeluxe.serve_next_queued_number(machine)
|
||||||
|
# => {:error, :empty_queue}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Reset state
|
||||||
|
|
||||||
|
Implement the `reset_state/1` function and the necessary `GenServer` callback.
|
||||||
|
|
||||||
|
It should call the `TakeANumberDeluxe.State.new/2` function to create a new state using the current state's `min_number` and `max_number`. The machine should set the new state as its state. It should not reply to the caller.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
|
||||||
|
|
||||||
|
TakeANumberDeluxe.reset_state(machine)
|
||||||
|
# => :ok
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Implement auto shutdown
|
||||||
|
|
||||||
|
Modify starting the machine. It should read the value under the key `:auto_shutdown_timeout` in the keyword list passed as `init_arg` and pass it as the third argument to `TakeANumberDeluxe.State.new/3`. Use the default value of `:infinity` if `:auto_shutdown_timeout` was not given.
|
||||||
|
|
||||||
|
Modify resetting the machine state to also pass `auto_shutdown_timeout` to `TakeANumberDeluxe.State.new/3`.
|
||||||
|
|
||||||
|
Modify the return values of all implemented callbacks (`init/1` and all `handle_*` callbacks) to set a timeout. Use the value under the key `:auto_shutdown_timeout` in the current machine state. Do not add the timeout to the `{:stop, reason}` return value of `init/1` - timeouts only apply after the server has started its receive loop.
|
||||||
|
|
||||||
|
Implement a `GenServer` callback to handle the `:timeout` message that will be sent to the machine if it doesn't receive any other messages within the given timeout. It should exit the process with reason `:normal`.
|
||||||
|
|
||||||
|
Make sure to also handle any unexpected messages by ignoring them.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
{:ok, machine} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 10,
|
||||||
|
auto_shutdown_timeout: :timer.hours(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
# after 3 hours...
|
||||||
|
|
||||||
|
TakeANumberDeluxe.queue_new_number(machine)
|
||||||
|
# => ** (exit) exited in: GenServer.call(#PID<0.171.0>, :queue_new_number, 5000)
|
||||||
|
# ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
|
||||||
|
# (elixir 1.13.0) lib/gen_server.ex:1030: GenServer.call/3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @jiegillet
|
|
@ -0,0 +1,94 @@
|
||||||
|
defmodule TakeANumberDeluxe do
|
||||||
|
alias TakeANumberDeluxe.State
|
||||||
|
use GenServer
|
||||||
|
# Client API
|
||||||
|
|
||||||
|
@spec start_link(keyword()) :: {:ok, pid()} | {:error, atom()}
|
||||||
|
def start_link(init_arg) do
|
||||||
|
GenServer.start_link(__MODULE__, init_arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec report_state(pid()) :: State.t()
|
||||||
|
def report_state(machine) do
|
||||||
|
GenServer.call(machine, :report_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec queue_new_number(pid()) :: {:ok, integer()} | {:error, atom()}
|
||||||
|
def queue_new_number(machine) do
|
||||||
|
GenServer.call(machine, :queue_new_number)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec serve_next_queued_number(pid(), integer() | nil) :: {:ok, integer()} | {:error, atom()}
|
||||||
|
def serve_next_queued_number(machine, priority_number \\ nil) do
|
||||||
|
GenServer.call(machine, {:serve_next_queued_number, priority_number})
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec reset_state(pid()) :: :ok
|
||||||
|
def reset_state(machine) do
|
||||||
|
GenServer.cast(machine, :reset_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Server callbacks
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def init(options) do
|
||||||
|
with min <- Keyword.get(options, :min_number),
|
||||||
|
max <- Keyword.get(options, :max_number),
|
||||||
|
shutdown <- Keyword.get(options, :auto_shutdown_timeout, :infinity),
|
||||||
|
{:ok, state} <- State.new(min, max, shutdown) do
|
||||||
|
{:ok, state, shutdown}
|
||||||
|
else
|
||||||
|
_ -> {:stop, :invalid_configuration}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call(:report_state, _from, state) do
|
||||||
|
{:reply, state, state, state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call(:queue_new_number, _from, state) do
|
||||||
|
case State.queue_new_number(state) do
|
||||||
|
{:ok, new_number, new_state} -> {:reply, {:ok, new_number}, new_state, new_state.auto_shutdown_timeout}
|
||||||
|
error -> {:reply, error, state, state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call({:serve_next_queued_number, priority_number}, _from, state) do
|
||||||
|
case State.serve_next_queued_number(state, priority_number) do
|
||||||
|
{:ok, new_number, new_state} -> {:reply, {:ok, new_number}, new_state, new_state.auto_shutdown_timeout}
|
||||||
|
error -> {:reply, error, state, state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call(_request, _from, state) do
|
||||||
|
{:reply, :unknown_command, state, state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_cast(
|
||||||
|
:reset_state,
|
||||||
|
%State{min_number: min_number, max_number: max_number, auto_shutdown_timeout: auto_shutdown_timeout}
|
||||||
|
) do
|
||||||
|
{:ok, new_state} = State.new(min_number, max_number, auto_shutdown_timeout)
|
||||||
|
{:noreply, new_state, new_state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_cast(_request, state) do
|
||||||
|
{:noreply, state, state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_info(:timeout, state) do
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_info(_request, state) do
|
||||||
|
{:noreply, state, state.auto_shutdown_timeout}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,42 @@
|
||||||
|
defmodule TakeANumberDeluxe.Queue do
|
||||||
|
# You don't need to read this module to solve this exercise.
|
||||||
|
|
||||||
|
# We would have used Erlang's queue module instead
|
||||||
|
# (https://www.erlang.org/doc/man/queue.html),
|
||||||
|
# but it lacks a `delete` function before OTP 24,
|
||||||
|
# and we want this exercise to work on older versions too.
|
||||||
|
|
||||||
|
defstruct in: [], out: []
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
@spec new() :: t()
|
||||||
|
def new(), do: %__MODULE__{}
|
||||||
|
|
||||||
|
@spec push(t(), any()) :: t()
|
||||||
|
def push(%__MODULE__{in: in_q} = q, a), do: %__MODULE__{q | in: [a | in_q]}
|
||||||
|
|
||||||
|
@spec out(t()) :: {{:value, any()}, t()} | {:empty, t()}
|
||||||
|
def out(%__MODULE__{in: [], out: []} = q), do: {:empty, q}
|
||||||
|
def out(%__MODULE__{out: [head | tail]} = q), do: {{:value, head}, %__MODULE__{q | out: tail}}
|
||||||
|
def out(%__MODULE__{in: in_q}), do: out(%__MODULE__{out: Enum.reverse(in_q)})
|
||||||
|
|
||||||
|
@spec empty?(t()) :: boolean()
|
||||||
|
def empty?(%__MODULE__{in: [], out: []}), do: true
|
||||||
|
def empty?(%__MODULE__{}), do: false
|
||||||
|
|
||||||
|
@spec member?(t(), any()) :: boolean()
|
||||||
|
def member?(%__MODULE__{in: in_q, out: out}, a), do: a in in_q or a in out
|
||||||
|
|
||||||
|
@spec delete(t(), any()) :: t()
|
||||||
|
def delete(%__MODULE__{in: in_q, out: out}, a) do
|
||||||
|
out = out ++ Enum.reverse(in_q)
|
||||||
|
out = List.delete(out, a)
|
||||||
|
%__MODULE__{out: out}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec from_list([any()]) :: t()
|
||||||
|
def from_list(list), do: %__MODULE__{out: list}
|
||||||
|
|
||||||
|
@spec to_list(t()) :: [any()]
|
||||||
|
def to_list(%__MODULE__{in: in_q, out: out}), do: out ++ Enum.reverse(in_q)
|
||||||
|
end
|
|
@ -0,0 +1,87 @@
|
||||||
|
defmodule TakeANumberDeluxe.State do
|
||||||
|
defstruct min_number: 1, max_number: 999, queue: nil, auto_shutdown_timeout: :infinity
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
alias TakeANumberDeluxe.Queue
|
||||||
|
|
||||||
|
@spec new(integer, integer, timeout) :: {:ok, TakeANumberDeluxe.State.t()} | {:error, atom()}
|
||||||
|
def new(min_number, max_number, auto_shutdown_timeout \\ :infinity) do
|
||||||
|
if min_and_max_numbers_valid?(min_number, max_number) and
|
||||||
|
timeout_valid?(auto_shutdown_timeout) do
|
||||||
|
{:ok,
|
||||||
|
%__MODULE__{
|
||||||
|
min_number: min_number,
|
||||||
|
max_number: max_number,
|
||||||
|
queue: Queue.new(),
|
||||||
|
auto_shutdown_timeout: auto_shutdown_timeout
|
||||||
|
}}
|
||||||
|
else
|
||||||
|
{:error, :invalid_configuration}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec queue_new_number(TakeANumberDeluxe.State.t()) ::
|
||||||
|
{:ok, integer(), TakeANumberDeluxe.State.t()} | {:error, atom()}
|
||||||
|
def queue_new_number(%__MODULE__{} = state) do
|
||||||
|
case find_next_available_number(state) do
|
||||||
|
{:ok, next_available_number} ->
|
||||||
|
{:ok, next_available_number,
|
||||||
|
%{state | queue: Queue.push(state.queue, next_available_number)}}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec serve_next_queued_number(TakeANumberDeluxe.State.t(), integer() | nil) ::
|
||||||
|
{:ok, integer(), TakeANumberDeluxe.State.t()} | {:error, atom()}
|
||||||
|
def serve_next_queued_number(%__MODULE__{} = state, priority_number) do
|
||||||
|
cond do
|
||||||
|
Queue.empty?(state.queue) ->
|
||||||
|
{:error, :empty_queue}
|
||||||
|
|
||||||
|
is_nil(priority_number) ->
|
||||||
|
{{:value, next_number}, new_queue} = Queue.out(state.queue)
|
||||||
|
{:ok, next_number, %{state | queue: new_queue}}
|
||||||
|
|
||||||
|
Queue.member?(state.queue, priority_number) ->
|
||||||
|
{:ok, priority_number, %{state | queue: Queue.delete(state.queue, priority_number)}}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:error, :priority_number_not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp min_and_max_numbers_valid?(min_number, max_number) do
|
||||||
|
is_integer(min_number) and is_integer(max_number) and min_number < max_number
|
||||||
|
end
|
||||||
|
|
||||||
|
defp timeout_valid?(timeout) do
|
||||||
|
timeout == :infinity || (is_integer(timeout) && timeout >= 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_next_available_number(state) do
|
||||||
|
all_numbers_in_use = Queue.to_list(state.queue)
|
||||||
|
all_numbers = Enum.to_list(state.min_number..state.max_number)
|
||||||
|
|
||||||
|
case all_numbers_in_use do
|
||||||
|
[] ->
|
||||||
|
{:ok, state.min_number}
|
||||||
|
|
||||||
|
list when length(list) == length(all_numbers) ->
|
||||||
|
{:error, :all_possible_numbers_are_in_use}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
current_highest_number = Enum.max(all_numbers_in_use)
|
||||||
|
|
||||||
|
next_available_number =
|
||||||
|
if current_highest_number < state.max_number do
|
||||||
|
current_highest_number + 1
|
||||||
|
else
|
||||||
|
Enum.min(all_numbers -- all_numbers_in_use)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, next_available_number}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule TakeANumber.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :take_a_number_deluxe,
|
||||||
|
version: "0.1.0",
|
||||||
|
# elixir: "~> 1.10",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,400 @@
|
||||||
|
defmodule TakeANumberDeluxeTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
alias TakeANumberDeluxe.Queue
|
||||||
|
|
||||||
|
describe "start_link/1" do
|
||||||
|
@tag task_id: 1
|
||||||
|
test "starts a new process" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert is_pid(pid)
|
||||||
|
assert pid != self()
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "the process doesn't have a name" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert Process.info(pid, :registered_name) == {:registered_name, []}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "min and max numbers get validated" do
|
||||||
|
Process.flag(:trap_exit, true)
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.start_link(min_number: 999, max_number: 99) ==
|
||||||
|
{:error, :invalid_configuration}
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.start_link(min_number: :not_a_number, max_number: 99) ==
|
||||||
|
{:error, :invalid_configuration}
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.start_link(min_number: 1, max_number: "not a number") ==
|
||||||
|
{:error, :invalid_configuration}
|
||||||
|
|
||||||
|
Process.flag(:trap_exit, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "mix and max numbers can be passed in any order" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert is_pid(pid)
|
||||||
|
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(max_number: 333, min_number: 100)
|
||||||
|
assert is_pid(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "the init/1 GenServer callback is defined" do
|
||||||
|
Code.ensure_loaded(TakeANumberDeluxe)
|
||||||
|
assert function_exported?(TakeANumberDeluxe, :init, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "report_state/1" do
|
||||||
|
@tag task_id: 2
|
||||||
|
test "returns the state of the current process" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 4, max_number: 55)
|
||||||
|
{:ok, expected_state} = TakeANumberDeluxe.State.new(4, 55)
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) == expected_state
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "different processes have different states" do
|
||||||
|
{:ok, pid1} = TakeANumberDeluxe.start_link(min_number: 4, max_number: 55)
|
||||||
|
{:ok, pid2} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 19)
|
||||||
|
assert TakeANumberDeluxe.report_state(pid1) != TakeANumberDeluxe.report_state(pid2)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "the handle_call/3 GenServer callback is defined" do
|
||||||
|
Code.ensure_loaded(TakeANumberDeluxe)
|
||||||
|
assert function_exported?(TakeANumberDeluxe, :handle_call, 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "queue_new_number/1" do
|
||||||
|
@tag task_id: 3
|
||||||
|
test "returns the newly queued number" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 4, max_number: 55)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 4}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "can queue multiple numbers" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "returns an error when there are no more available numbers" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 3)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:error, :all_possible_numbers_are_in_use}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "updates the state accordingly" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) ==
|
||||||
|
%TakeANumberDeluxe.State{
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
queue: Queue.new() |> Queue.push(1) |> Queue.push(2) |> Queue.push(3)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "serve_next_queued_number/1" do
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns the new number being served" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 100, max_number: 999)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 100}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 100}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "can serve multiple numbers" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 3}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns an error when there are no more queued" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 3)
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) ==
|
||||||
|
{:error, :empty_queue}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "accepts a priority number to skip the queue" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid, 3) == {:ok, 3}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns an error when the priority number is not in the queue" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 3)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid, 7) ==
|
||||||
|
{:error, :priority_number_not_found}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "updates the state accordingly" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 2}
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) ==
|
||||||
|
%TakeANumberDeluxe.State{
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
queue: Queue.from_list([3])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "reset_state/1" do
|
||||||
|
@tag task_id: 5
|
||||||
|
test "returns :ok" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.reset_state(pid) == :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "updates the state accordingly" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 7, max_number: 77)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 7}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 8}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 9}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 7}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 8}
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.reset_state(pid) == :ok
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) ==
|
||||||
|
%TakeANumberDeluxe.State{
|
||||||
|
min_number: 7,
|
||||||
|
max_number: 77,
|
||||||
|
queue: Queue.new()
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "the handle_cast/2 GenServer callback is defined" do
|
||||||
|
Code.ensure_loaded(TakeANumberDeluxe)
|
||||||
|
assert function_exported?(TakeANumberDeluxe, :handle_cast, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "auto shutdown handling unexpected messages" do
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after initializing" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after reporting state" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
assert TakeANumberDeluxe.report_state(pid)
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after taking a new number" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after an error when taking a new number" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 2,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:error, :all_possible_numbers_are_in_use}
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after serving the next queued number" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 1}
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after an error when serving the next queued number" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:error, :empty_queue}
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "resetting state preserves the auto shutdown timeout" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, expected_state} = TakeANumberDeluxe.State.new(1, 99, timeout)
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) == expected_state
|
||||||
|
assert TakeANumberDeluxe.reset_state(pid)
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) == expected_state
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after resetting state" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
assert TakeANumberDeluxe.reset_state(pid) == :ok
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "unexpected messages do not affect the state" do
|
||||||
|
{:ok, pid} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 99)
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 1}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 2}
|
||||||
|
assert TakeANumberDeluxe.queue_new_number(pid) == {:ok, 3}
|
||||||
|
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 1}
|
||||||
|
|
||||||
|
old_state = TakeANumberDeluxe.report_state(pid)
|
||||||
|
|
||||||
|
send(pid, {:hello, "there"})
|
||||||
|
|
||||||
|
assert TakeANumberDeluxe.report_state(pid) == old_state
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "auto shutdown works after handling unexpected messages" do
|
||||||
|
timeout = 50
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
TakeANumberDeluxe.start_link(
|
||||||
|
min_number: 1,
|
||||||
|
max_number: 99,
|
||||||
|
auto_shutdown_timeout: timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
send(pid, {:hello, "there"})
|
||||||
|
assert Process.alive?(pid)
|
||||||
|
:timer.sleep(timeout * 2)
|
||||||
|
refute Process.alive?(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "the handle_info/2 GenServer callback is defined" do
|
||||||
|
Code.ensure_loaded(TakeANumberDeluxe)
|
||||||
|
assert function_exported?(TakeANumberDeluxe, :handle_info, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
Loading…
Reference in New Issue