From 776a104f19a094c9423130dadbafbc78c3171b32 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Fri, 22 Dec 2023 03:59:46 -0500 Subject: [PATCH] file-sniffer --- elixir/file-sniffer/.exercism/config.json | 22 ++ elixir/file-sniffer/.exercism/metadata.json | 1 + elixir/file-sniffer/.formatter.exs | 4 + elixir/file-sniffer/.gitignore | 24 ++ elixir/file-sniffer/HELP.md | 75 +++++++ elixir/file-sniffer/HINTS.md | 28 +++ elixir/file-sniffer/README.md | 109 +++++++++ elixir/file-sniffer/assets/bmp.bmp | Bin 0 -> 30 bytes elixir/file-sniffer/assets/data.dat | Bin 0 -> 12 bytes elixir/file-sniffer/assets/elf.o | Bin 0 -> 76 bytes elixir/file-sniffer/assets/gif.gif | Bin 0 -> 14 bytes elixir/file-sniffer/assets/jpeg.jpg | Bin 0 -> 107 bytes .../file-sniffer/assets/png-transparent.png | Bin 0 -> 67 bytes elixir/file-sniffer/lib/file_sniffer.ex | 42 ++++ elixir/file-sniffer/mix.exs | 28 +++ .../file-sniffer/test/file_sniffer_test.exs | 211 ++++++++++++++++++ elixir/file-sniffer/test/test_helper.exs | 2 + 17 files changed, 546 insertions(+) create mode 100644 elixir/file-sniffer/.exercism/config.json create mode 100644 elixir/file-sniffer/.exercism/metadata.json create mode 100644 elixir/file-sniffer/.formatter.exs create mode 100644 elixir/file-sniffer/.gitignore create mode 100644 elixir/file-sniffer/HELP.md create mode 100644 elixir/file-sniffer/HINTS.md create mode 100644 elixir/file-sniffer/README.md create mode 100644 elixir/file-sniffer/assets/bmp.bmp create mode 100644 elixir/file-sniffer/assets/data.dat create mode 100644 elixir/file-sniffer/assets/elf.o create mode 100644 elixir/file-sniffer/assets/gif.gif create mode 100644 elixir/file-sniffer/assets/jpeg.jpg create mode 100644 elixir/file-sniffer/assets/png-transparent.png create mode 100644 elixir/file-sniffer/lib/file_sniffer.ex create mode 100644 elixir/file-sniffer/mix.exs create mode 100644 elixir/file-sniffer/test/file_sniffer_test.exs create mode 100644 elixir/file-sniffer/test/test_helper.exs diff --git a/elixir/file-sniffer/.exercism/config.json b/elixir/file-sniffer/.exercism/config.json new file mode 100644 index 0000000..768cc2f --- /dev/null +++ b/elixir/file-sniffer/.exercism/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "neenjaw" + ], + "contributors": [ + "angelikatyborska", + "cr0t" + ], + "files": { + "solution": [ + "lib/file_sniffer.ex" + ], + "test": [ + "test/file_sniffer_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about binaries by verifying the mime type of files uploaded to your server." +} diff --git a/elixir/file-sniffer/.exercism/metadata.json b/elixir/file-sniffer/.exercism/metadata.json new file mode 100644 index 0000000..5ee4e77 --- /dev/null +++ b/elixir/file-sniffer/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"file-sniffer","id":"0c7b16debeb2429cb1646ec0ea2858e1","url":"https://exercism.org/tracks/elixir/exercises/file-sniffer","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/file-sniffer/.formatter.exs b/elixir/file-sniffer/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/file-sniffer/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/file-sniffer/.gitignore b/elixir/file-sniffer/.gitignore new file mode 100644 index 0000000..4abee35 --- /dev/null +++ b/elixir/file-sniffer/.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"). +match_binary-*.tar + diff --git a/elixir/file-sniffer/HELP.md b/elixir/file-sniffer/HELP.md new file mode 100644 index 0000000..6d4d2ab --- /dev/null +++ b/elixir/file-sniffer/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/file_sniffer.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/file-sniffer/HINTS.md b/elixir/file-sniffer/HINTS.md new file mode 100644 index 0000000..5fc9b27 --- /dev/null +++ b/elixir/file-sniffer/HINTS.md @@ -0,0 +1,28 @@ +# Hints + +## General + +- Remember to reference the table in the instructions. + +## 1. Given an extension, return the expected media type + +- Use [pattern matching][pattern-matching] to return the correct media type. +- You can use [multiple function clauses][mfc]. + +## 2. Given a binary file, return the expected media type + +- Use the [binary special form][special-forms] available to you for writing a binary literal. +- Use [pattern matching][binary-matching] to match against the first few bytes of the file binary. + +## 3. Given an extension and a binary file, verify that the file matches the expected type + +- Reuse the functions you created for parts 1 and 2. +- Compare the return value of each function, then return the appropriate value. + - You can use the [`if` macro conditional][if] for binary conditions. + +[binary-matching]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#binaries +[if]: https://elixir-lang.org/getting-started/case-cond-and-if.html#if-and-unless +[mfc]: https://elixir-lang.org/getting-started/modules-and-functions.html#named-functions +[mimetype]: https://en.wikipedia.org/wiki/Media_type +[pattern-matching]: https://elixir-lang.org/getting-started/pattern-matching.html#pattern-matching +[special-forms]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1 \ No newline at end of file diff --git a/elixir/file-sniffer/README.md b/elixir/file-sniffer/README.md new file mode 100644 index 0000000..e726c89 --- /dev/null +++ b/elixir/file-sniffer/README.md @@ -0,0 +1,109 @@ +# File Sniffer + +Welcome to File Sniffer 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 + +## Binaries + +Elixir provides an elegant syntax for working with binary data as we have seen with the `<<>>` special form provided for working with [bitstrings][exercism-bitstrings]. + +The binary type is a specialization on the bitstring type. Where bitstrings could be of any length (any number of [bits][wiki-bit]), binaries are where the number of bits can be evenly divided by 8. That is, when working with binaries, we often think of things in terms of [bytes][wiki-byte] (8 bits). A byte can represent integer numbers from `0` to `255`. It is common to work with byte values in [hexadecimal][wiki-hexadecimal], `0x00 - 0xFF`. + +Binary literals are defined using the bitstring special form `<<>>`. When defining a binary literal, we can use integer and string literals. Integer values greater than 255 will overflow and only the last 8 bits of the integer will be used. By default, the `::binary` modifier is applied to the value. We can concatenate binaries with the `<>/2` operator. + +```elixir +<<255>> == <<0xFF>> +<<256>> == <<0>> # Overflowing bits are truncated +<<2, 4, 6, 8, 10, 12, 14, 16>> == <<0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10>> +``` + +A _null-byte_ is another name for `<<0>>`. + +### Pattern matching on binary data + +[Pattern matching][exercism-pattern-matching] is even extended to binaries, and we can pattern match on a portion of binary data much like we could for a list. + +```elixir +# Ignore the first 8 bytes, match and bind the remaining to `body` +<<_::binary-size(8), body::binary>> +``` + +Like with other types of pattern matching, we can use this in function signatures to match when selecting from multiple function clauses. + +[wiki-bit]: https://en.wikipedia.org/wiki/Bit +[wiki-byte]: https://en.wikipedia.org/wiki/Byte +[wiki-hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal +[exercism-bitstrings]: https://exercism.org/tracks/elixir/concepts/bitstrings +[exercism-pattern-matching]: https://exercism.org/tracks/elixir/concepts/pattern-matching + +## Instructions + +You have been working on a project which allows users to upload files to the server to be shared with other users. You have been tasked with writing a function to verify that an upload matches its [media type][mimetype]. You do some research and discover that the first few bytes of a file are generally unique to that file type, giving it a sort of signature. + +Use the following table for reference: + +| File type | Common extension | Media type | binary 'signature' | +| --------- | ---------------- | ---------------------------- | ------------------------------------------------ | +| ELF | `"exe"` | `"application/octet-stream"` | `0x7F, 0x45, 0x4C, 0x46` | +| BMP | `"bmp"` | `"image/bmp"` | `0x42, 0x4D` | +| PNG | `"png"` | `"image/png"` | `0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A` | +| JPG | `"jpg"` | `"image/jpg"` | `0xFF, 0xD8, 0xFF` | +| GIF | `"gif"` | `"image/gif"` | `0x47, 0x49, 0x46` | + +## 1. Given an extension, return the expected media type + +Implement the `type_from_extension/1` function. It should take a file extension (string) and return the media type (string) or nil if the extension does not match with the expected ones. + +```elixir +FileSniffer.type_from_extension("exe") +# => "application/octet-stream" + +FileSniffer.type_from_extension("txt") +# => nil +``` + +## 2. Given a binary file, return the expected media type + +Implement the `type_from_binary/1` function. It should take a file (binary) and return the media type (string) or nil if the extension does not match with the expected ones. + +```elixir +file = File.read!("application.exe") +FileSniffer.type_from_binary(file) +# => "application/octet-stream" + +file = File.read!("example.txt") +FileSniffer.type_from_binary(file) +# => nil +``` + +Don't worry about reading the file as a binary. Assume that has been done for you and is provided by the tests as an argument. + +## 3. Given an extension and a binary file, verify that the file matches the expected type + +Implement the `verify/2` function. It should take a file (binary) and extension (string) and return an `:ok` or `:error` tuple. + +```elixir +file = File.read!("application.exe") + +FileSniffer.verify(file, "exe") +# => {:ok, "application/octet-stream"} + +FileSniffer.verify(file, "png") +# => {:error, "Warning, file format and file extension do not match."} +``` + +[mimetype]: https://en.wikipedia.org/wiki/Media_type + +## Source + +### Created by + +- @neenjaw + +### Contributed to by + +- @angelikatyborska +- @cr0t \ No newline at end of file diff --git a/elixir/file-sniffer/assets/bmp.bmp b/elixir/file-sniffer/assets/bmp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..688af7180a991c7144b071a7108baac2bffd461c GIT binary patch literal 30 ZcmZ?rm1BSaDImoI#Ef7l0c8DW000?J0qy_* literal 0 HcmV?d00001 diff --git a/elixir/file-sniffer/assets/data.dat b/elixir/file-sniffer/assets/data.dat new file mode 100644 index 0000000000000000000000000000000000000000..ac9c7b703a4de0f3606a030fea29352e60cd0b2d GIT binary patch literal 12 OcmWIWW&nf#KmY&``vjN( literal 0 HcmV?d00001 diff --git a/elixir/file-sniffer/assets/elf.o b/elixir/file-sniffer/assets/elf.o new file mode 100644 index 0000000000000000000000000000000000000000..474e9dd91bc34b33750393dc40ba6e6328486ff1 GIT binary patch literal 76 zcmb<-^>JflWMqH=CI)5(5Kq$KY=aI=5J;OaD1gPmLO?kNAoc-rK$sOID8Rq~021W~ AP5=M^ literal 0 HcmV?d00001 diff --git a/elixir/file-sniffer/assets/gif.gif b/elixir/file-sniffer/assets/gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..edaf2b97ab8256e904871ce15643d3275dd771e6 GIT binary patch literal 14 TcmZ?wbhEHbWMp7u00L_O6F~vy literal 0 HcmV?d00001 diff --git a/elixir/file-sniffer/assets/jpeg.jpg b/elixir/file-sniffer/assets/jpeg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..71911bf48766c7181518c1070911019fbb00b1fc GIT binary patch literal 107 zcmWm0!3~2z39Q7kcv6UAQ@H$MqV!6EkIEQ MPgg&ebxsLQ07X&@0{{R3 literal 0 HcmV?d00001 diff --git a/elixir/file-sniffer/lib/file_sniffer.ex b/elixir/file-sniffer/lib/file_sniffer.ex new file mode 100644 index 0000000..74affa5 --- /dev/null +++ b/elixir/file-sniffer/lib/file_sniffer.ex @@ -0,0 +1,42 @@ +defmodule FileSniffer do + @signatures [ + {<<0x7F, 0x45, 0x4C, 0x46>>, "application/octet-stream"}, + {<<0x42, 0x4D>>, "image/bmp"}, + {<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>>, "image/png"}, + {<<0xFF, 0xD8, 0xFF>>, "image/jpg"}, + {<<0x47, 0x49, 0x46>>, "image/gif"} + ] + + @file_types %{ + "exe" => "application/octet-stream", + "bmp" => "image/bmp", + "png" => "image/png", + "jpg" => "image/jpg", + "gif" => "image/gif" + } + + def type_from_extension(extension), do: @file_types[extension] + + def type_from_binary(file_binary) do + Enum.find_value(@signatures, fn {signature, type} -> + with signature_size <- byte_size(signature), + <> <- file_binary, + true <- file_signature == signature do + type + else + _ -> nil + end + end) + end + + def verify(file_binary, extension) do + ext_type = type_from_extension(extension) + bin_type = type_from_binary(file_binary) + case ext_type == bin_type do + true when ext_type != nil and bin_type != nil -> + {:ok, ext_type} + _ -> + {:error, "Warning, file format and file extension do not match."} + end + end +end diff --git a/elixir/file-sniffer/mix.exs b/elixir/file-sniffer/mix.exs new file mode 100644 index 0000000..54d2aa3 --- /dev/null +++ b/elixir/file-sniffer/mix.exs @@ -0,0 +1,28 @@ +defmodule FileSniffer.MixProject do + use Mix.Project + + def project do + [ + app: :file_sniffer, + 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/file-sniffer/test/file_sniffer_test.exs b/elixir/file-sniffer/test/file_sniffer_test.exs new file mode 100644 index 0000000..d7b4e08 --- /dev/null +++ b/elixir/file-sniffer/test/file_sniffer_test.exs @@ -0,0 +1,211 @@ +defmodule FileSnifferTest do + use ExUnit.Case + + @bmp_file File.read!(Path.join("assets", "bmp.bmp")) + @gif_file File.read!(Path.join("assets", "gif.gif")) + @jpg_file File.read!(Path.join("assets", "jpeg.jpg")) + @png_file File.read!(Path.join("assets", "png-transparent.png")) + @exe_file File.read!(Path.join("assets", "elf.o")) + @dat_file File.read!(Path.join("assets", "data.dat")) + + describe "get type from extension" do + @tag task_id: 1 + test "bmp" do + assert FileSniffer.type_from_extension("bmp") == "image/bmp" + end + + @tag task_id: 1 + test "gif" do + assert FileSniffer.type_from_extension("gif") == "image/gif" + end + + @tag task_id: 1 + test "jpg" do + assert FileSniffer.type_from_extension("jpg") == "image/jpg" + end + + @tag task_id: 1 + test "png" do + assert FileSniffer.type_from_extension("png") == "image/png" + end + + @tag task_id: 1 + test "exe" do + assert FileSniffer.type_from_extension("exe") == "application/octet-stream" + end + end + + describe "return nil when type doesn't match" do + @tag task_id: 1 + test "txt" do + assert FileSniffer.type_from_extension("txt") == nil + end + + @tag task_id: 1 + test "md" do + assert FileSniffer.type_from_extension("md") == nil + end + + @tag task_id: 1 + test "dat" do + assert FileSniffer.type_from_extension("dat") == nil + end + end + + describe "get type from binary" do + @tag task_id: 2 + test "bmp" do + assert FileSniffer.type_from_binary(@bmp_file) == "image/bmp" + end + + @tag task_id: 2 + test "gif" do + assert FileSniffer.type_from_binary(@gif_file) == "image/gif" + end + + @tag task_id: 2 + test "jpg" do + assert FileSniffer.type_from_binary(@jpg_file) == "image/jpg" + end + + @tag task_id: 2 + test "png" do + assert FileSniffer.type_from_binary(@png_file) == "image/png" + end + + @tag task_id: 2 + test "exe" do + assert FileSniffer.type_from_binary(@exe_file) == "application/octet-stream" + end + end + + describe "return nil when given uncompleted signature file" do + @tag task_id: 2 + test "bmp" do + assert FileSniffer.type_from_binary(String.slice(@bmp_file, 0..0)) == nil + end + + @tag task_id: 2 + test "gif" do + assert FileSniffer.type_from_binary(String.slice(@gif_file, 0..1)) == nil + end + + @tag task_id: 2 + test "jpg" do + assert FileSniffer.type_from_binary(String.slice(@jpg_file, 0..1)) == nil + end + + @tag task_id: 2 + test "png" do + assert FileSniffer.type_from_binary(String.slice(@png_file, 0..5)) == nil + end + + @tag task_id: 2 + test "exe" do + assert FileSniffer.type_from_binary(String.slice(@exe_file, 0..2)) == nil + end + end + + describe "return nil when given unknown signature" do + @tag task_id: 2 + test "dat" do + assert FileSniffer.type_from_binary(@dat_file) == nil + end + end + + describe "return nil when input is a bitstring, but not a binary" do + @tag task_id: 2 + test "bmp" do + assert FileSniffer.type_from_binary(<<0x42, 0x4D, 0::1>>) == nil + end + + @tag task_id: 2 + test "gif" do + assert FileSniffer.type_from_binary(<<0x47, 0x49, 0x46, 0::1>>) == nil + end + + @tag task_id: 2 + test "jpg" do + assert FileSniffer.type_from_binary(<<0xFF, 0xD8, 0xFF, 0::1>>) == nil + end + + @tag task_id: 2 + test "png" do + assert FileSniffer.type_from_binary( + <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0::1>> + ) == nil + end + + @tag task_id: 2 + test "exe" do + assert FileSniffer.type_from_binary(<<0x7F, 0x45, 0x4C, 0x46, 0::1>>) == nil + end + end + + describe "verify valid files" do + @tag task_id: 3 + test "bmp" do + assert FileSniffer.verify(@bmp_file, "bmp") == {:ok, "image/bmp"} + end + + @tag task_id: 3 + test "gif" do + assert FileSniffer.verify(@gif_file, "gif") == {:ok, "image/gif"} + end + + @tag task_id: 3 + test "jpg" do + assert FileSniffer.verify(@jpg_file, "jpg") == {:ok, "image/jpg"} + end + + @tag task_id: 3 + test "png" do + assert FileSniffer.verify(@png_file, "png") == {:ok, "image/png"} + end + + @tag task_id: 3 + test "exe" do + assert FileSniffer.verify(@exe_file, "exe") == {:ok, "application/octet-stream"} + end + end + + describe "reject invalid files" do + @tag task_id: 3 + test "bmp" do + assert FileSniffer.verify(@exe_file, "bmp") == + {:error, "Warning, file format and file extension do not match."} + end + + @tag task_id: 3 + test "gif" do + assert FileSniffer.verify(@exe_file, "gif") == + {:error, "Warning, file format and file extension do not match."} + end + + @tag task_id: 3 + test "jpg" do + assert FileSniffer.verify(@exe_file, "jpg") == + {:error, "Warning, file format and file extension do not match."} + end + + @tag task_id: 3 + test "png" do + assert FileSniffer.verify(@exe_file, "png") == + {:error, "Warning, file format and file extension do not match."} + end + + @tag task_id: 3 + test "exe" do + assert FileSniffer.verify(@png_file, "exe") == + {:error, "Warning, file format and file extension do not match."} + end + end + + describe "reject unknown file types" do + @tag task_id: 3 + test "dat" do + assert FileSniffer.verify(@dat_file, "dat") == + {:error, "Warning, file format and file extension do not match."} + end + end +end diff --git a/elixir/file-sniffer/test/test_helper.exs b/elixir/file-sniffer/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/file-sniffer/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)