Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sofakingworld committed Dec 26, 2023
0 parents commit 9ed100d
Show file tree
Hide file tree
Showing 17 changed files with 532 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
54 changes: 54 additions & 0 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: build

on:
push:
branches:
- "**"

pull_request:
branches:
- master
types: [opened, synchronize, closed]


permissions:
contents: read

jobs:
tests:
runs-on: ubuntu-20.04
name: Tests on ${{matrix.environment.elixir-version}}
strategy:
matrix:
environment:
- elixir-version: 1.15.7
otp-version: 26.1.2
- elixir-version: 1.14
otp-version: 24.3.4

steps:
- uses: actions/checkout@v3
- name: Set up Elixir
uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f
with: ${{matrix.environment}}
- name: Restore dependencies cache
uses: actions/cache@v3
with:
path: deps/
key: deps-${{ runner.os }}-${{ matrix.environment.otp-version }}-${{ matrix.environment.elixir-version }}-${{ hashFiles('**/mix.lock') }}
- name: Restore build cache
uses: actions/cache@v3
with:
path: _build/test/
key: build-${{ runner.os }}-${{ matrix.environment.otp-version }}-${{ matrix.environment.elixir-version }}-${{ hashFiles('**/mix.lock') }}
- name: Install dependencies
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get
mix compile
mix format --check-formatted
- name: Run tests
run: mix test
env:
MIX_ENV: test
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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").
ex_unit_summary-*.tar

# Temporary files, for example, from tests.
/tmp/
33 changes: 33 additions & 0 deletions .recode.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
version: "0.6.4",
# Can also be set/reset with `--autocorrect`/`--no-autocorrect`.
autocorrect: true,
# With "--dry" no changes will be written to the files.
# Can also be set/reset with `--dry`/`--no-dry`.
# If dry is true then verbose is also active.
dry: false,
# Can also be set/reset with `--verbose`/`--no-verbose`.
verbose: false,
# Can be overwritten by calling `mix recode "lib/**/*.ex"`.
inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"],
formatter: {Recode.Formatter, []},
tasks: [
# Tasks could be added by a tuple of the tasks module name and an options
# keyword list. A task can be deactivated by `active: false`. The execution of
# a deactivated task can be forced by calling `mix recode --task ModuleName`.
{Recode.Task.AliasExpansion, []},
{Recode.Task.AliasOrder, []},
{Recode.Task.Dbg, [autocorrect: false]},
{Recode.Task.EnforceLineLength, [active: false]},
{Recode.Task.FilterCount, []},
{Recode.Task.IOInspect, [autocorrect: false]},
{Recode.Task.Nesting, []},
{Recode.Task.PipeFunOne, []},
{Recode.Task.SinglePipe, []},
{Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]},
{Recode.Task.TagFIXME, [exit_code: 2]},
{Recode.Task.TagTODO, [exit_code: 4]},
{Recode.Task.TestFileExt, []},
{Recode.Task.UnusedVariable, [active: false]}
]
]
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ExUnitSummary

The library was created as an extension for EчUnit and adds test case output, just like Rspec does.

It's a very convenient life cycle for fixing and adding tests is obtained.

The developer just needs to select any entry from the results, copy it and paste it again into the console

See the example below

<img src="media/usage_sample.gif" alt="drawing" width="960"/>


## How to add a library to your project

```elixir
def deps do
[
{:ex_unit_summary, "~> 0.1.0", only: [:dev, :test]}}
]
end
```

## How to setup

```elixir
# test_helper.exs

# Start ExUnitSummary application, with recommended config
ExUnitSummary.start(:normal, %ExUnitSummary.Config{filter_results: :failed, print_delay: 100})

# Add ExUnitSummary.Formatter to list of ExUnit's formatters.
ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitSummary.Formatter])
```

# Contribution

Feel free to make a pull request. All contributions are appreciated!
16 changes: 16 additions & 0 deletions lib/ex_unit_summary.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule ExUnitSummary do
@moduledoc false
use Application
alias ExUnitSummary.Config

def start(:normal, %Config{} = config) do
children = [
{ExUnitSummary.ConfigStorage, config},
{ExUnitSummary.Formatter, config},
{ExUnitSummary.Recorder, []}
]

opts = [strategy: :one_for_one, name: ExUnitSummary.Supervisor]
Supervisor.start_link(children, opts)
end
end
60 changes: 60 additions & 0 deletions lib/ex_unit_summary/case_result.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule ExUnitSummary.CaseResult do
@moduledoc """
Wrapper around ExUnit case result
"""
defstruct [
:case_type,
:describe_block_name,
:case_name,
:file,
:line,
:module,
:case_result,
:case_time,
:error_info
]

@type t() :: %{
case_result: :failed | :success,
file: String.t(),
module: String.t(),
line: non_neg_integer(),
case_type: atom(),
describe_block_name: nil | String.t(),
case_time: non_neg_integer(),
error_info: any()
}

@spec from_exunit_event({any(), any()}) :: nil | ExUnitSummary.CaseResult.t()
def from_exunit_event({:test_finished, %ExUnit.Test{state: nil} = event}) do
from_exunit_finish_event(event)
end

def from_exunit_event({:test_finished, %ExUnit.Test{state: {:failed, _info}} = event}) do
from_exunit_finish_event(event)
end

def from_exunit_event({_another_event, _event}) do
nil
end

defp from_exunit_finish_event(%ExUnit.Test{} = event) do
%__MODULE__{
case_type: event.tags.test_type,
case_name: String.replace_leading(to_string(event.name), "test ", ""),
module: event.module,
case_result: case_result(event.state),
error_info: error_info(event.state),
case_time: time_to_ms(event.time),
describe_block_name: event.tags.describe,
line: event.tags.line,
file: event.tags.file
}
end

defp time_to_ms(time), do: trunc(time / 1000)
defp case_result(nil), do: :success
defp case_result({:failed, _reason}), do: :failed
defp error_info(nil), do: []
defp error_info({:failed, reason}), do: reason
end
15 changes: 15 additions & 0 deletions lib/ex_unit_summary/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule ExUnitSummary.Config do
@moduledoc """
Library Configurations
* print_delay is used to delay output, becouse there is race condition with default ExUnit Formatter
* filter_results is used to filter, which results should be printed in console
"""
defstruct print_delay: nil,
filter_results: nil

@type t() :: %{
print_delay: nil | non_neg_integer(),
filter_results: :failed | :success | :all | nil
}
end
30 changes: 30 additions & 0 deletions lib/ex_unit_summary/config_storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule ExUnitSummary.ConfigStorage do
@moduledoc """
Storage for ExUnitSummary Config
(Global State)
"""
use GenServer

alias ExUnitSummary.Config

def start_link(%Config{} = config) do
GenServer.start_link(__MODULE__, config, name: __MODULE__)
end

@impl GenServer

def init(%Config{} = config) do
{:ok, config}
end

@spec get_config() :: any()
def get_config() do
GenServer.call(__MODULE__, :get_config)
end

@impl GenServer
def handle_call(:get_config, _from, %Config{} = config) do
{:reply, config, config}
end
end
38 changes: 38 additions & 0 deletions lib/ex_unit_summary/formatter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule ExUnitSummary.Formatter do
@moduledoc false
use GenServer

alias ExUnitSummary.CaseResult
alias ExUnitSummary.Printer
alias ExUnitSummary.Recorder

@spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()}
def start_link(_any) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end

def init(ex_unit_config) do
{:ok, ex_unit_config}
end

def handle_cast({:suite_finished, _data}, ex_unit_config) do
results = get_list_of_case_results()
write_output(results)
{:noreply, ex_unit_config}
end

def handle_cast({_event_type, _event_data} = event, ex_unit_config) do
case_result = CaseResult.from_exunit_event(event)
Recorder.write(case_result)

{:noreply, ex_unit_config}
end

defp get_list_of_case_results() do
Recorder.get_results()
end

defp write_output(results) do
Printer.call(results)
end
end
56 changes: 56 additions & 0 deletions lib/ex_unit_summary/printer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule ExUnitSummary.Printer do
@moduledoc """
This module prints results to console
"""
alias ExUnitSummary.CaseResult
alias ExUnitSummary.Config
alias ExUnitSummary.ConfigStorage

@spec call(maybe_improper_list()) :: :ok
def call(results) when is_list(results) do
%Config{print_delay: delay} = config = ConfigStorage.get_config()

# Delay is used for ensure, that ExUnitSummary print own report after ExUnit.CLIFormatter
if delay, do: :timer.sleep(delay)

print(config, results)
end

def print(%Config{filter_results: filter_results} = _config, results) do
failed_list =
results
|> Enum.filter(&filter_function(&1, filter_results))
|> Enum.sort_by(&{&1.file, &1.line, &1.case_result})
|> Enum.map(&build_row/1)

if length(failed_list) > 0 do
result_string =
"ExUnitSummary (#{filter_results || :all}): \n\n" <> Enum.join(failed_list, "\n")

IO.puts(result_string)
end

:ok
end

defp filter_function(%CaseResult{} = _result, filter) when filter in [nil, :all] do
true
end

defp filter_function(%CaseResult{case_result: result} = _result, filter)
when filter in [:failed, :success] do
result == filter
end

defp build_row(%CaseResult{case_result: result} = case_result) do
Enum.join([
if(result == :success, do: IO.ANSI.green(), else: IO.ANSI.red()),
"mix test ",
"#{case_result.file}:#{case_result.line}",
IO.ANSI.blue(),
" # ",
"#{case_result.case_name}",
IO.ANSI.reset()
])
end
end
Loading

0 comments on commit 9ed100d

Please sign in to comment.