├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib ├── ex_unit_notifier.ex └── ex_unit_notifier │ ├── counter.ex │ ├── message_formatter.ex │ └── notifiers │ ├── notify_send.ex │ ├── terminal_notifier.ex │ ├── terminal_title.ex │ └── tmux_notifier.ex ├── mix.exs ├── mix.lock ├── priv └── icons │ ├── error.icns │ ├── error.png │ ├── ok.icns │ └── ok.png └── test ├── ex_unit_notifier └── message_formatter_test.exs ├── ex_unit_notifier_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.credo,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Elixir 18 | uses: erlef/setup-beam@v1 19 | with: 20 | otp-version: "25.3" 21 | elixir-version: "1.14" 22 | 23 | - name: Restore dependencies cache 24 | uses: actions/cache@v2 25 | with: 26 | path: deps 27 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 28 | restore-keys: ${{ runner.os }}-mix- 29 | 30 | - name: Install dependencies 31 | run: mix deps.get 32 | - name: Run tests 33 | run: mix test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_unit_notifier-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp 27 | 28 | # Misc. 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "noreply" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Navin Peiris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExUnit Notifier 2 | 3 | [![Build Status](https://github.com/navinpeiris/ex_unit_notifier/workflows/CI/badge.svg)](https://github.com/navinpeiris/ex_unit_notifier/actions?query=workflow%3ACI) 4 | [![Hex version](https://img.shields.io/hexpm/v/ex_unit_notifier.svg "Hex version")](https://hex.pm/packages/ex_unit_notifier) 5 | [![Hex docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_unit_notifier/) 6 | [![Hex downloads](https://img.shields.io/hexpm/dt/ex_unit_notifier.svg "Hex downloads")](https://hex.pm/packages/ex_unit_notifier) 7 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) 8 | [![Last Updated](https://img.shields.io/github/last-commit/navinpeiris/ex_unit_notifier.svg)](https://github.com/navinpeiris/ex_unit_notifier/commits/master) 9 | 10 | ![screenshot](http://i.imgur.com/xywj5C1.png) 11 | 12 | Show desktop notifications for ExUnit runs. Works very well with automatic test runners such as [mix-test.watch](https://github.com/lpil/mix-test.watch). (Yes, TDD is awesome!) 13 | 14 | Currently notifications on OS X and Linux are supported. 15 | 16 | ## Installation 17 | 18 | First, add `ExUnitNotifier` to your `mix.exs` dependencies: 19 | 20 | ```elixir 21 | def deps do 22 | [ 23 | {:ex_unit_notifier, "~> 1.3", only: :test} 24 | ] 25 | end 26 | ``` 27 | 28 | Then, update your dependencies: 29 | 30 | ```bash 31 | $ mix deps.get 32 | ``` 33 | 34 | ### For macOS 35 | 36 | Follow [installation instruction](https://github.com/julienXX/terminal-notifier) of `terminal-notifier` if you need to install a particular version. 37 | 38 | Otherwise, install current version via Homebrew: 39 | 40 | ```bash 41 | $ brew install terminal-notifier 42 | ``` 43 | 44 | ### For GNU/Linux 45 | 46 | Install `notify-send`: 47 | 48 | ```bash 49 | $ sudo apt install libnotify-bin 50 | ``` 51 | 52 | ## Usage 53 | 54 | Add `ExUnitNotifier` to your `ExUnit` configuration in `test/test_helper.exs` file. 55 | 56 | ```elixir 57 | ExUnit.configure formatters: [ExUnit.CLIFormatter, ExUnitNotifier] 58 | ExUnit.start 59 | ``` 60 | 61 | Now run `mix test` and you'll see notifications popping up :) 62 | 63 | ## Notification Types 64 | 65 | Notifications will be sent from the first available notifier that is deemed available in the order specified below: 66 | 67 | - terminal-notifier (ExUnitNotifier.Notifiers.TerminalNotifier) 68 | - notify-send (ExUnitNotifier.Notifiers.NotifySend) 69 | - tmux (ExUnitNotifier.Notifiers.TmuxNotifier) 70 | - Terminal Title if non of the above match (ExUnitNotifier.Notifiers.TerminalTitle) 71 | 72 | To force a specific type of notifier to be used, specify the notifier using the following configuration: 73 | 74 | ```elixir 75 | config :ex_unit_notifier, notifier: ExUnitNotifier.Notifiers.TerminalNotifier 76 | ``` 77 | 78 | You can use one of the available notifiers found in [lib/ex_unit_notifier/notifiers](lib/ex_unit_notifier/notifiers), or you can write your own. 79 | 80 | ## Notification Options 81 | 82 | For `notify-send` users, it is possible to clear the notifications from notifications center history using the following configuration, defaults to `false`: 83 | 84 | ```elixir 85 | config :ex_unit_notifier, clear_history: true 86 | ``` 87 | 88 | ### Copyright and License 89 | 90 | Copyright (c) 2016 Navin Peiris 91 | 92 | Source code is released under [the MIT license](./LICENSE.md). 93 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ex_unit_notifier, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ex_unit_notifier, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier do 2 | @moduledoc """ 3 | Shows notifications for ExUnit test runs 4 | 5 | To enable notifications, add `ExUnitNotifier` as a formatter in your `test_helper.exs`: 6 | 7 | ExUnit.configure formatters: [ExUnit.CLIFormatter, ExUnitNotifier] 8 | 9 | """ 10 | 11 | use GenServer 12 | 13 | alias ExUnitNotifier.Counter 14 | alias ExUnitNotifier.MessageFormatter 15 | 16 | @notifiers [ 17 | ExUnitNotifier.Notifiers.TerminalNotifier, 18 | ExUnitNotifier.Notifiers.NotifySend, 19 | ExUnitNotifier.Notifiers.TmuxNotifier, 20 | ExUnitNotifier.Notifiers.TerminalTitle 21 | ] 22 | 23 | def init(_opts), do: {:ok, %Counter{}} 24 | 25 | def handle_cast({:test_finished, %ExUnit.Test{state: nil}}, counter), 26 | do: {:noreply, counter |> Counter.add_test()} 27 | 28 | def handle_cast({:test_finished, %ExUnit.Test{state: {:failed, _}}}, counter), 29 | do: {:noreply, counter |> Counter.add_test() |> Counter.add_failed()} 30 | 31 | def handle_cast({:test_finished, %ExUnit.Test{state: {:excluded, _}}}, counter), 32 | do: {:noreply, counter |> Counter.add_test() |> Counter.add_excluded()} 33 | 34 | def handle_cast({:test_finished, %ExUnit.Test{state: {:skipped, _}}}, counter), 35 | do: {:noreply, counter |> Counter.add_test() |> Counter.add_skipped()} 36 | 37 | def handle_cast({:test_finished, %ExUnit.Test{state: {:invalid, _}}}, counter), 38 | do: {:noreply, counter |> Counter.add_test() |> Counter.add_invalid()} 39 | 40 | # Elixir version < 1.12.0 41 | def handle_cast({:suite_finished, run_us, load_us}, counter) do 42 | apply(notifier(), :notify, [ 43 | status(counter), 44 | MessageFormatter.format(counter, run_us, load_us), 45 | opts() 46 | ]) 47 | 48 | {:noreply, counter} 49 | end 50 | 51 | # Elixir version >= 1.12.0, see https://hexdocs.pm/ex_unit/1.12.0/ExUnit.Formatter.html 52 | def handle_cast({:suite_finished, %{run: run_us, async: _async_us, load: load_us}}, counter) do 53 | apply(notifier(), :notify, [ 54 | status(counter), 55 | MessageFormatter.format(counter, run_us, load_us), 56 | opts() 57 | ]) 58 | 59 | {:noreply, counter} 60 | end 61 | 62 | def handle_cast(_, counter), do: {:noreply, counter} 63 | 64 | defp status(%Counter{failures: failures, invalid: invalid}) when failures > 0 or invalid > 0, 65 | do: :error 66 | 67 | defp status(_), do: :ok 68 | 69 | defp opts, 70 | do: %{ 71 | clear_history: Application.get_env(:ex_unit_notifier, :clear_history, false) 72 | } 73 | 74 | defp notifier, do: Application.get_env(:ex_unit_notifier, :notifier, first_available_notifier()) 75 | 76 | defp first_available_notifier, 77 | do: @notifiers |> Enum.find(fn notifier -> notifier.available?() end) 78 | end 79 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier/counter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.Counter do 2 | @moduledoc false 3 | 4 | defstruct tests: 0, failures: 0, excluded: 0, skipped: 0, invalid: 0 5 | 6 | def add_test(counter), do: %{counter | tests: counter.tests + 1} 7 | def add_failed(counter), do: %{counter | failures: counter.failures + 1} 8 | def add_excluded(counter), do: %{counter | excluded: counter.excluded + 1} 9 | def add_skipped(counter), do: %{counter | skipped: counter.skipped + 1} 10 | def add_invalid(counter), do: %{counter | invalid: counter.invalid + 1} 11 | end 12 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier/message_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.MessageFormatter do 2 | @moduledoc false 3 | 4 | def format(counter, run_us, load_us) do 5 | "#{status_message(counter)} in #{format_time(run_us, load_us)} seconds" 6 | end 7 | 8 | defp status_message(%{tests: tests, failures: failures, excluded: excluded, skipped: skipped}) do 9 | message = "#{tests} tests, #{failures} failures" 10 | 11 | message = if excluded > 0, do: "#{message}, #{excluded} excluded", else: message 12 | message = if skipped > 0, do: "#{message}, #{skipped} skipped", else: message 13 | 14 | message 15 | end 16 | 17 | defp format_time(run_us, load_us), do: format_us(normalize_us(run_us) + normalize_us(load_us)) 18 | 19 | defp normalize_us(nil), do: 0 20 | defp normalize_us(us), do: div(us, 10_000) 21 | 22 | defp format_us(us) when us < 10, do: "0.0#{us}" 23 | 24 | defp format_us(us) do 25 | us = div(us, 10) 26 | "#{div(us, 10)}.#{rem(us, 10)}" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier/notifiers/notify_send.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.Notifiers.NotifySend do 2 | @moduledoc false 3 | 4 | def notify(status, message, opts) do 5 | System.cmd(executable(), build_args(status, message, opts)) 6 | end 7 | 8 | def available?, do: executable() != nil 9 | 10 | defp executable, do: System.find_executable("notify-send") 11 | 12 | defp build_args(status, message, %{clear_history: clear_history}) do 13 | args = [ 14 | "--app-name=ExUnit", 15 | "--icon=#{get_icon(status)}", 16 | "ExUnit", 17 | message 18 | ] 19 | 20 | maybe_add_clear_history(args, clear_history) 21 | end 22 | 23 | defp maybe_add_clear_history(args, true), 24 | do: List.insert_at(args, 2, "--hint=int:transient:1") 25 | 26 | defp maybe_add_clear_history(args, _clear_history), do: args 27 | 28 | defp get_icon(status), 29 | do: Application.app_dir(:ex_unit_notifier, "priv/icons/#{status |> Atom.to_string()}.png") 30 | end 31 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier/notifiers/terminal_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.Notifiers.TerminalNotifier do 2 | @moduledoc false 3 | 4 | def notify(status, message, _opts) do 5 | System.cmd(executable(), [ 6 | "-group", 7 | "ex-unit-notifier", 8 | "-title", 9 | "ExUnit", 10 | "-message", 11 | message, 12 | "-appIcon", 13 | get_icon(status), 14 | "-contentImage", 15 | content_image(status) 16 | ]) 17 | end 18 | 19 | def available?, do: executable() != nil 20 | 21 | defp executable, do: System.find_executable("terminal-notifier") 22 | 23 | defp get_icon(status), 24 | do: Application.app_dir(:ex_unit_notifier, "priv/icons/#{status}.icns") 25 | 26 | defp content_image(status), 27 | do: Application.app_dir(:ex_unit_notifier, "priv/icons/#{status}.png") 28 | end 29 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier/notifiers/terminal_title.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.Notifiers.TerminalTitle do 2 | @moduledoc false 3 | 4 | def notify(_status, message, _opts), do: IO.puts(:stderr, "\e]2;ExUnit - #{message} \a") 5 | 6 | def available?, do: true 7 | end 8 | -------------------------------------------------------------------------------- /lib/ex_unit_notifier/notifiers/tmux_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.Notifiers.TmuxNotifier do 2 | @moduledoc false 3 | 4 | def notify(status, _message, _opts) do 5 | if pane() do 6 | # tmux set-window-option -t"$TMUX_PANE" window-status-style bg=red 7 | System.cmd(executable(), [ 8 | "set-window-option", 9 | "-t#{pane()}", 10 | "window-status-style", 11 | style(status) 12 | ]) 13 | end 14 | end 15 | 16 | def available?, do: executable() != nil && pane() != nil 17 | 18 | defp executable, do: System.find_executable("tmux") 19 | 20 | defp pane, do: System.get_env("TMUX_PANE") 21 | 22 | defp style(status) do 23 | if status == :error do 24 | "bg=red" 25 | else 26 | "bg=green" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/navinpeiris/ex_unit_notifier" 5 | @version "1.3.1" 6 | 7 | def project do 8 | [ 9 | app: :ex_unit_notifier, 10 | version: @version, 11 | name: "ExUnitNotifier", 12 | elixir: "~> 1.14", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | build_embedded: Mix.env() == :prod, 15 | start_permanent: Mix.env() == :prod, 16 | package: package(), 17 | deps: deps(), 18 | docs: docs() 19 | ] 20 | end 21 | 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | defp deps do 29 | [ 30 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 31 | {:mix_test_watch, "~> 1.0", only: :dev} 32 | ] 33 | end 34 | 35 | defp docs do 36 | [ 37 | extras: [{:"LICENSE.md", [title: "License"]}, "README.md"], 38 | main: "readme", 39 | source_url: @source_url, 40 | api_reference: false 41 | ] 42 | end 43 | 44 | defp package do 45 | [ 46 | description: "Show status notifications for ExUnit test runs", 47 | files: ["lib", "priv", "mix.exs", "README*", "LICENSE*"], 48 | maintainers: ["Navin Peiris"], 49 | licenses: ["MIT"], 50 | links: %{"GitHub" => @source_url} 51 | ] 52 | end 53 | 54 | defp elixirc_paths(:test), do: ["lib", "test/support"] 55 | defp elixirc_paths(_), do: ["lib"] 56 | end 57 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 3 | "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, 4 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 5 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 8 | "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, 9 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 10 | } 11 | -------------------------------------------------------------------------------- /priv/icons/error.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navinpeiris/ex_unit_notifier/ff7fac317a9879a2645dc56df11650e6030a9e54/priv/icons/error.icns -------------------------------------------------------------------------------- /priv/icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navinpeiris/ex_unit_notifier/ff7fac317a9879a2645dc56df11650e6030a9e54/priv/icons/error.png -------------------------------------------------------------------------------- /priv/icons/ok.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navinpeiris/ex_unit_notifier/ff7fac317a9879a2645dc56df11650e6030a9e54/priv/icons/ok.icns -------------------------------------------------------------------------------- /priv/icons/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navinpeiris/ex_unit_notifier/ff7fac317a9879a2645dc56df11650e6030a9e54/priv/icons/ok.png -------------------------------------------------------------------------------- /test/ex_unit_notifier/message_formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifier.MessageFormatterTest do 2 | use ExUnit.Case 3 | doctest ExUnitNotifier 4 | 5 | import ExUnitNotifier.MessageFormatter 6 | alias ExUnitNotifier.Counter 7 | 8 | test "format/1 returns message with the correct numbers specified" do 9 | counter = %Counter{tests: 3, failures: 2} 10 | 11 | message = format(counter, 600_456, 100_000) 12 | 13 | assert message == "3 tests, 2 failures in 0.7 seconds" 14 | end 15 | 16 | test "format/1 adds the number of skipped tests if present" do 17 | counter = %Counter{tests: 3, failures: 2, skipped: 7} 18 | 19 | message = format(counter, 600_456, 100_000) 20 | 21 | assert message == "3 tests, 2 failures, 7 skipped in 0.7 seconds" 22 | end 23 | 24 | test "format/1 adds the number of excluded tests if present" do 25 | counter = %Counter{tests: 3, failures: 2, excluded: 5} 26 | 27 | message = format(counter, 600_456, 100_000) 28 | 29 | assert message == "3 tests, 2 failures, 5 excluded in 0.7 seconds" 30 | end 31 | 32 | test "format/1 adds the number of skipped and excluded tests if present" do 33 | counter = %Counter{tests: 3, failures: 2, excluded: 5, skipped: 7} 34 | 35 | message = format(counter, 600_456, 100_000) 36 | 37 | assert message == "3 tests, 2 failures, 5 excluded, 7 skipped in 0.7 seconds" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/ex_unit_notifier_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExUnitNotifierTest do 2 | use ExUnit.Case 3 | doctest ExUnitNotifier 4 | 5 | defmodule TestNotifier do 6 | def notify(status, message, _opts) do 7 | send(test_pid(), {status, message}) 8 | end 9 | 10 | defp test_pid, do: Application.get_env(:ex_unit_notifier, :test_pid) 11 | end 12 | 13 | setup do 14 | Application.put_env(:ex_unit_notifier, :notifier, TestNotifier) 15 | Application.put_env(:ex_unit_notifier, :test_pid, self()) 16 | 17 | on_exit(fn -> 18 | Application.delete_env(:ex_unit_notifier, :notifier) 19 | end) 20 | 21 | :ok 22 | end 23 | 24 | test "sends expected notification when tests are successful" do 25 | defmodule SuccessfulTests do 26 | use ExUnit.Case 27 | 28 | test "successful test" do 29 | assert 1 + 1 == 2 30 | end 31 | 32 | test "successful test 2" do 33 | assert 1 + 1 == 2 34 | end 35 | end 36 | 37 | run_sample_test() 38 | 39 | assert_receive {:ok, message} 40 | assert message =~ ~r(2 tests, 0 failures in \d+\.\d{2} seconds) 41 | end 42 | 43 | test "sends expected notification when tests contains failures" do 44 | defmodule TestsWithFailure do 45 | use ExUnit.Case 46 | 47 | test "successful test" do 48 | assert 1 + 1 == 2 49 | end 50 | 51 | test "failing test" do 52 | assert 1 + 1 == 3 53 | end 54 | end 55 | 56 | run_sample_test() 57 | 58 | assert_receive {:error, message} 59 | assert message =~ ~r(2 tests, 1 failures in \d+\.\d{2} seconds) 60 | end 61 | 62 | test "sends expected notification when tests raises an error" do 63 | defmodule TestsWithErrorRaised do 64 | use ExUnit.Case 65 | 66 | test "failing test" do 67 | raise ArgumentError 68 | end 69 | end 70 | 71 | run_sample_test() 72 | 73 | assert_receive {:error, message} 74 | assert message =~ ~r(1 tests, 1 failures in \d+\.\d{2} seconds) 75 | end 76 | 77 | test "sends expected notification when tests contain pending test" do 78 | defmodule TestsWithPending do 79 | use ExUnit.Case 80 | 81 | test "successful test" do 82 | assert 1 + 1 == 2 83 | end 84 | 85 | @tag :pending 86 | test "pending test" do 87 | assert 1 + 1 == 3 88 | end 89 | 90 | @tag :pending 91 | test "pending test 2" do 92 | assert 1 + 1 == 3 93 | end 94 | 95 | @tag :skip 96 | test "skipped test" do 97 | assert 1 + 1 == 3 98 | end 99 | end 100 | 101 | run_sample_test() 102 | 103 | assert_receive {:ok, message} 104 | assert message =~ ~r(4 tests, 0 failures, 2 excluded, 1 skipped in \d+\.\d{2} seconds) 105 | end 106 | 107 | defp run_sample_test do 108 | ExUnit.Server.modules_loaded(false) 109 | 110 | ExUnit.configure(formatters: [ExUnitNotifier], exclude: [pending: true]) 111 | ExUnit.run() 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitNotifier]) 2 | ExUnit.start() 3 | --------------------------------------------------------------------------------