From def44c9d466c53733b2e5f19261c7bc620ab9246 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Sun, 10 Mar 2024 00:20:16 -0500 Subject: [PATCH] take_a_number_deluxe --- .../.exercism/config.json | 26 ++ .../.exercism/metadata.json | 1 + elixir/take-a-number-deluxe/.formatter.exs | 4 + elixir/take-a-number-deluxe/.gitignore | 24 ++ elixir/take-a-number-deluxe/HELP.md | 75 ++++ elixir/take-a-number-deluxe/HINTS.md | 81 ++++ elixir/take-a-number-deluxe/README.md | 293 +++++++++++++ .../lib/take_a_number_deluxe.ex | 94 ++++ .../lib/take_a_number_deluxe/queue.ex | 42 ++ .../lib/take_a_number_deluxe/state.ex | 87 ++++ elixir/take-a-number-deluxe/mix.exs | 28 ++ .../test/take_a_number_deluxe_test.exs | 400 ++++++++++++++++++ .../take-a-number-deluxe/test/test_helper.exs | 2 + 13 files changed, 1157 insertions(+) create mode 100644 elixir/take-a-number-deluxe/.exercism/config.json create mode 100644 elixir/take-a-number-deluxe/.exercism/metadata.json create mode 100644 elixir/take-a-number-deluxe/.formatter.exs create mode 100644 elixir/take-a-number-deluxe/.gitignore create mode 100644 elixir/take-a-number-deluxe/HELP.md create mode 100644 elixir/take-a-number-deluxe/HINTS.md create mode 100644 elixir/take-a-number-deluxe/README.md create mode 100644 elixir/take-a-number-deluxe/lib/take_a_number_deluxe.ex create mode 100644 elixir/take-a-number-deluxe/lib/take_a_number_deluxe/queue.ex create mode 100644 elixir/take-a-number-deluxe/lib/take_a_number_deluxe/state.ex create mode 100644 elixir/take-a-number-deluxe/mix.exs create mode 100644 elixir/take-a-number-deluxe/test/take_a_number_deluxe_test.exs create mode 100644 elixir/take-a-number-deluxe/test/test_helper.exs diff --git a/elixir/take-a-number-deluxe/.exercism/config.json b/elixir/take-a-number-deluxe/.exercism/config.json new file mode 100644 index 0000000..05e29d6 --- /dev/null +++ b/elixir/take-a-number-deluxe/.exercism/config.json @@ -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." +} diff --git a/elixir/take-a-number-deluxe/.exercism/metadata.json b/elixir/take-a-number-deluxe/.exercism/metadata.json new file mode 100644 index 0000000..b0b840f --- /dev/null +++ b/elixir/take-a-number-deluxe/.exercism/metadata.json @@ -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} \ No newline at end of file diff --git a/elixir/take-a-number-deluxe/.formatter.exs b/elixir/take-a-number-deluxe/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/take-a-number-deluxe/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/take-a-number-deluxe/.gitignore b/elixir/take-a-number-deluxe/.gitignore new file mode 100644 index 0000000..cb54963 --- /dev/null +++ b/elixir/take-a-number-deluxe/.gitignore @@ -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 + diff --git a/elixir/take-a-number-deluxe/HELP.md b/elixir/take-a-number-deluxe/HELP.md new file mode 100644 index 0000000..717c76d --- /dev/null +++ b/elixir/take-a-number-deluxe/HELP.md @@ -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/.exs:LINENUM` - runs only a single test, the test from `.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. \ No newline at end of file diff --git a/elixir/take-a-number-deluxe/HINTS.md b/elixir/take-a-number-deluxe/HINTS.md new file mode 100644 index 0000000..8dac4bd --- /dev/null +++ b/elixir/take-a-number-deluxe/HINTS.md @@ -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 \ No newline at end of file diff --git a/elixir/take-a-number-deluxe/README.md b/elixir/take-a-number-deluxe/README.md new file mode 100644 index 0000000..8a433f4 --- /dev/null +++ b/elixir/take-a-number-deluxe/README.md @@ -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 \ No newline at end of file diff --git a/elixir/take-a-number-deluxe/lib/take_a_number_deluxe.ex b/elixir/take-a-number-deluxe/lib/take_a_number_deluxe.ex new file mode 100644 index 0000000..006b244 --- /dev/null +++ b/elixir/take-a-number-deluxe/lib/take_a_number_deluxe.ex @@ -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 diff --git a/elixir/take-a-number-deluxe/lib/take_a_number_deluxe/queue.ex b/elixir/take-a-number-deluxe/lib/take_a_number_deluxe/queue.ex new file mode 100644 index 0000000..013eac5 --- /dev/null +++ b/elixir/take-a-number-deluxe/lib/take_a_number_deluxe/queue.ex @@ -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 diff --git a/elixir/take-a-number-deluxe/lib/take_a_number_deluxe/state.ex b/elixir/take-a-number-deluxe/lib/take_a_number_deluxe/state.ex new file mode 100644 index 0000000..09381b5 --- /dev/null +++ b/elixir/take-a-number-deluxe/lib/take_a_number_deluxe/state.ex @@ -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 diff --git a/elixir/take-a-number-deluxe/mix.exs b/elixir/take-a-number-deluxe/mix.exs new file mode 100644 index 0000000..af96157 --- /dev/null +++ b/elixir/take-a-number-deluxe/mix.exs @@ -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 diff --git a/elixir/take-a-number-deluxe/test/take_a_number_deluxe_test.exs b/elixir/take-a-number-deluxe/test/take_a_number_deluxe_test.exs new file mode 100644 index 0000000..a9333c9 --- /dev/null +++ b/elixir/take-a-number-deluxe/test/take_a_number_deluxe_test.exs @@ -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 diff --git a/elixir/take-a-number-deluxe/test/test_helper.exs b/elixir/take-a-number-deluxe/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/take-a-number-deluxe/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)