├── VERSION ├── .gitignore ├── lib ├── amrita │ ├── fact_pending.ex │ ├── system.ex │ ├── message.ex │ ├── checkers │ │ ├── messages.ex │ │ ├── exceptions.ex │ │ ├── collection.ex │ │ └── simple.ex │ ├── elixir │ │ ├── version.ex │ │ └── pipeline.ex │ ├── fact_error.ex │ ├── engine │ │ ├── test_picker.ex │ │ ├── start.ex │ │ └── runner.ex │ ├── mock_error.ex │ ├── mocks │ │ └── history.ex │ ├── syntax │ │ └── describe.ex │ ├── checkers.ex │ ├── formatter │ │ ├── format.ex │ │ ├── progress.ex │ │ └── documentation.ex │ └── mocks.ex ├── mix │ └── tasks │ │ └── amrita.ex └── amrita.ex ├── mix.lock ├── test ├── integration │ ├── t_async.exs │ ├── t_callback.exs │ ├── t_scoping.exs │ ├── t_describe.exs │ ├── t_checkers.exs │ ├── t_mix.exs │ ├── t_mocks.exs │ └── t_amrita.exs ├── unit │ ├── amrita │ │ ├── elixir │ │ │ ├── t_version.exs │ │ │ └── t_pipeline.exs │ │ ├── t_fact_error.exs │ │ ├── t_mock_error.exs │ │ ├── t_checker.exs │ │ ├── mocks │ │ │ └── t_history.exs │ │ └── t_mocks.exs │ └── t_amrita.exs └── test_helper.exs ├── .travis.yml ├── package.exs ├── mix.exs ├── Makefile └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build/* 2 | ebin/* 3 | deps/* 4 | vendor/* 5 | -------------------------------------------------------------------------------- /lib/amrita/fact_pending.ex: -------------------------------------------------------------------------------- 1 | defexception Amrita.FactPending, message: "Pending" do 2 | end -------------------------------------------------------------------------------- /lib/amrita/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.System do 2 | 3 | @doc """ 4 | Returns Amritas current version 5 | """ 6 | def version do 7 | String.strip(File.read!("VERSION")) 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"ex_doc": {:git, "git://github.com/elixir-lang/ex_doc.git", "1ce6f1068cf1eeb97e9104977bed87e2a8c3be61", []}, 2 | "meck": {:git, "git://github.com/eproxus/meck.git", "9987eea73dab37ce014dbd4fa715a31cc07d351e", [branch: "master"]}} 3 | -------------------------------------------------------------------------------- /test/integration/t_async.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test_helper.exs", __ENV__.file 2 | 3 | defmodule Integration.AsyncFacts do 4 | use Amrita.Sweet, async: true 5 | 6 | fact "testing async" do 7 | 1 |> 1 8 | end 9 | 10 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 17.0 4 | install: 5 | - git clone git://github.com/rebar/rebar.git 6 | - cd rebar && ./bootstrap 7 | - export PATH=$PATH:`pwd` && cd .. 8 | - sudo apt-get update 9 | - sudo apt-get install erlang 10 | script: "make ci" -------------------------------------------------------------------------------- /test/integration/t_callback.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule Integration.CallbackFacts do 4 | use Amrita.Sweet 5 | 6 | setup do 7 | { :ok, ping: :hello } 8 | end 9 | 10 | fact "passed data from setup", meta do 11 | meta[:ping] |> :hello 12 | end 13 | 14 | facts "within a facts group" do 15 | fact "passed data from setup", meta do 16 | meta[:ping] |> :hello 17 | end 18 | end 19 | 20 | end -------------------------------------------------------------------------------- /test/unit/amrita/elixir/t_version.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule VersionFacts do 4 | use Amrita.Sweet 5 | 6 | alias Amrita.Elixir.Version, as: Version 7 | 8 | fact "older_than", provided: [System.version |> "0.9.2"] do 9 | Version.less_than_or_equal?([0,9,1]) |> falsey 10 | Version.less_than_or_equal?([0,9,2]) |> truthy 11 | Version.less_than_or_equal?([1,0,0]) |> truthy 12 | Version.less_than_or_equal?([0,10,0]) |> truthy 13 | end 14 | end -------------------------------------------------------------------------------- /package.exs: -------------------------------------------------------------------------------- 1 | version = String.strip(File.read!("VERSION")) 2 | 3 | Expm.Package.new(name: "amrita", 4 | description: "A polite, well mannered and thoroughly upstanding testing framework for Elixir", 5 | homepage: "http://amrita.io", 6 | version: version, 7 | keywords: ["testing", "tdd", "bdd", "elixir"], 8 | maintainers: [[name: "Joseph Wilk", email: "joe@josephwilk.net"]], 9 | repositories: [[github: "josephwilk/amrita"]]) 10 | -------------------------------------------------------------------------------- /lib/amrita/message.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Message do 2 | @moduledoc false 3 | 4 | def fail(candidate, fun) do 5 | raise Amrita.FactError, actual: candidate, 6 | predicate: fun 7 | end 8 | 9 | def fail(actual, expected, fun) do 10 | raise Amrita.FactError, expected: expected, 11 | actual: actual, 12 | predicate: fun 13 | end 14 | 15 | def mock_fail(errors) do 16 | raise Amrita.MockError, errors: errors 17 | end 18 | 19 | def pending(message) do 20 | raise Amrita.FactPending, message: message 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/unit/t_amrita.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test_helper.exs", __ENV__.file 2 | 3 | defmodule Unit.AmritaFacts do 4 | use Amrita.Sweet 5 | 6 | fixtures = [[[playerA: 0, playerB: 0], :playerA, [playerA: 15, playerB: 0]]] 7 | 8 | facts "Dynamically constructed fact/facts names" do 9 | lc [state, player, expected_state] inlist fixtures do 10 | @player player 11 | @state state 12 | @expected_state expected_state 13 | 14 | facts "When <#{player}> scores" do 15 | fact "the new state should be <#{inspect(expected_state)}>" do 16 | true |> truthy 17 | end 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/unit/amrita/t_fact_error.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule FactErrorFacts do 4 | use Amrita.Sweet 5 | 6 | facts "about error messages" do 7 | fact "message contains predicate with expected value" do 8 | error = Amrita.FactError.new(expected: "fun", actual: "not-fun", predicate: "contains") 9 | 10 | error.message |> contains "not-fun |> contains(\"fun\")" 11 | end 12 | 13 | fact "actual gets inspected when its not a string" do 14 | error = Amrita.FactError.new(actual: nil, predicate: "truthy") 15 | 16 | error.message |> contains "nil |> truthy" 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/amrita/checkers/messages.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Checkers.Messages do 2 | alias Amrita.Message, as: Message 3 | 4 | @moduledoc """ 5 | Checkers relating to messages. 6 | """ 7 | 8 | 9 | @doc """ 10 | Returns a function to return the received message with parameters for 11 | checking 12 | 13 | ## Examples 14 | send(self, :hello) 15 | received |> msg(:hello) 16 | 17 | """ 18 | def received, do: fn -> _received end 19 | 20 | @doc false 21 | def _received do 22 | timeout = 0 23 | receive do 24 | other -> other 25 | after 26 | timeout -> 27 | Message.fail("Expected to have received message", __ENV__.function) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/amrita/elixir/version.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Elixir.Version do 2 | @moduledoc false 3 | 4 | def less_than_or_equal?(version) do 5 | elixir_version = as_ints 6 | 7 | Enum.fetch!(elixir_version, 0) < Enum.fetch!(version, 0) || 8 | Enum.fetch!(elixir_version, 0) == Enum.fetch!(version, 0) && Enum.fetch!(elixir_version, 1) < Enum.fetch!(version, 1) || 9 | Enum.fetch!(elixir_version, 0) == Enum.fetch!(version, 0) && Enum.fetch!(elixir_version, 1) == Enum.fetch!(version, 1) && Enum.fetch!(elixir_version, 2) <= Enum.fetch!(version, 2) 10 | end 11 | 12 | 13 | def as_ints do 14 | elixir_version = String.split(System.version, ~r"[\.-]") 15 | Enum.map elixir_version, fn x -> if x != "dev", do: binary_to_integer(x) end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/amrita/fact_error.ex: -------------------------------------------------------------------------------- 1 | defexception Amrita.FactError, 2 | expected: nil, 3 | actual: nil, 4 | predicate: "", 5 | negation: false, 6 | prelude: "Expected" do 7 | 8 | def message do 9 | "fact failed" 10 | end 11 | 12 | def message(exception) do 13 | "#{exception.prelude}:\n" <> 14 | " #{exception.actual_result} |> #{exception.full_checker}" 15 | end 16 | 17 | def full_checker(exception) do 18 | Amrita.Checkers.to_s exception.predicate, exception.expected 19 | end 20 | 21 | def actual_result(exception) do 22 | if is_bitstring(exception.actual) do 23 | exception.actual 24 | else 25 | inspect exception.actual 26 | end 27 | end 28 | 29 | end -------------------------------------------------------------------------------- /test/integration/t_scoping.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule Integration.ScopingFacts do 4 | use Amrita.Sweet 5 | 6 | def echo, do: 1 7 | 8 | fact "echo 1", do: echo |> 1 9 | 10 | facts "echo nested" do 11 | def echo, do: 2 12 | 13 | future_fact "echo 2", do: echo |> 2 14 | end 15 | 16 | setup do 17 | {:ok, ping: :pong} 18 | end 19 | 20 | facts "setup within facts" do 21 | setup do 22 | {:ok, pong: :ping} 23 | end 24 | 25 | fact "has access to parent and local setup", meta do 26 | meta[:ping] |> :pong 27 | meta[:pong] |> :ping 28 | end 29 | end 30 | 31 | future_fact "parent should only have access to root setup", meta do 32 | meta[:ping] |> :pong 33 | meta[:pong] |> ! :ping 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/amrita/engine/test_picker.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Engine.TestPicker do 2 | @moduledoc false 3 | 4 | def run?(case_name, function, selectors) do 5 | if Enum.empty?(selectors) do 6 | true 7 | else 8 | meta = meta_for(case_name, function) 9 | if meta do 10 | relevant_selectors = Enum.filter selectors, fn selector -> String.contains?(meta[:file], selector[:file]) end 11 | if Enum.empty?(relevant_selectors) do 12 | true 13 | else 14 | Enum.any? relevant_selectors, fn selector -> 15 | case selector[:line] do 16 | nil -> true 17 | _ -> selector[:line] == meta[:line] 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | defp meta_for(case_name, function) do 26 | try do 27 | apply(case_name, :"__#{function}__", []) 28 | rescue 29 | #If its not an Amrita test it will not have metadata. 30 | _ -> nil 31 | end 32 | end 33 | 34 | end -------------------------------------------------------------------------------- /lib/amrita/engine/start.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Engine.Start do 2 | @moduledoc false 3 | 4 | def now(options \\ []) do 5 | :application.start(:elixir) 6 | :application.start(:ex_unit) 7 | 8 | configure(options) 9 | 10 | System.at_exit fn 11 | 0 -> 12 | failures = Amrita.Engine.Start.run 13 | System.at_exit fn _ -> 14 | if failures > 0, do: System.halt(1), else: System.halt(0) 15 | end 16 | _ -> 17 | :ok 18 | end 19 | end 20 | 21 | def configure(options) do 22 | Enum.each options, fn { k, v } -> 23 | :application.set_env(:ex_unit, k, v) 24 | end 25 | end 26 | 27 | def configuration do 28 | :application.get_all_env(:ex_unit) 29 | end 30 | 31 | def run do 32 | { async, sync, load_us } = ExUnit.Server.start_run 33 | 34 | async = Enum.sort async, fn(c,c1) -> c <= c1 end 35 | sync = Enum.sort sync, fn(c,c1) -> c <= c1 end 36 | 37 | Amrita.Engine.Runner.run async, sync, configuration, load_us 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /test/unit/amrita/t_mock_error.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule MockErrorFacts do 4 | use Amrita.Sweet 5 | alias Amrita.Mocks.Provided, as: Provided 6 | 7 | facts "about mock error messages" do 8 | fact "message contains actual call" do 9 | error = Provided.Error.new(module: Amrita, 10 | fun: :pants, 11 | args: [:hatter], 12 | history: [{Amrita, :pants, [:platter]}]) 13 | error = Amrita.MockError.new(mock_fail: true, errors: [error]) 14 | 15 | error.message |> contains "Amrita.pants(:platter)" 16 | end 17 | 18 | fact "message contains expected call" do 19 | error = Provided.Error.new(module: Amrita, 20 | fun: :pants, 21 | args: [:hatter], 22 | history: [{Amrita, :pants, [:platter]}]) 23 | error = Amrita.MockError.new(mock_fail: true, errors: [error]) 24 | 25 | error.message |> contains "Amrita.pants(:hatter)" 26 | end 27 | 28 | end 29 | end -------------------------------------------------------------------------------- /lib/amrita/mock_error.ex: -------------------------------------------------------------------------------- 1 | defexception Amrita.MockError, 2 | errors: [], 3 | prelude: "Expected" do 4 | 5 | def message(exception) do 6 | "#{exception.prelude}:\n" <> messages(exception) 7 | end 8 | 9 | defp messages(exception) do 10 | errors = Enum.map(exception.errors, fn error -> expected_call(error) <> actual_calls(error) end) 11 | Enum.join(errors, "\n") 12 | end 13 | 14 | defp actual_calls(e) do 15 | history = Enum.map e.history, fn({m,f,a}) -> " * #{Amrita.Checkers.to_s(m, f, a)}" end 16 | 17 | if not(Enum.empty?(history)) do 18 | "\n\n Actuals calls:\n" <> Enum.join(history, "\n") 19 | else 20 | "" 21 | end 22 | end 23 | 24 | defp expected_call(e) do 25 | " #{Amrita.Checkers.to_s(e.module, e.fun, printable_args(e))} to be called but was called 0 times." 26 | end 27 | 28 | defp printable_args(e) do 29 | index = -1 30 | Enum.map e.args, fn arg -> 31 | index = index + 1 32 | case arg do 33 | {:"$meck.matcher", :predicate, _} -> Macro.to_string(Enum.at(e.raw_args, index)) 34 | _ -> arg 35 | end 36 | end 37 | end 38 | 39 | end -------------------------------------------------------------------------------- /test/integration/t_describe.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test_helper.exs", __ENV__.file 2 | 3 | defmodule Integration.Syntax.Describe do 4 | use Amrita.Sweet 5 | 6 | import Support 7 | 8 | describe "we can use describe in place of facts" do 9 | it "works like fact" do 10 | 10 |> 10 11 | 12 | fail do 13 | 1 |> 10 14 | end 15 | end 16 | end 17 | 18 | context "we can use context in place of facts" do 19 | specify "specify works like fact" do 20 | 10 |> 10 21 | end 22 | end 23 | 24 | describe "hooks" do 25 | before_all do 26 | {:ok, before_all: :ok} 27 | end 28 | 29 | before_each do 30 | {:ok, before_each: :ok} 31 | end 32 | 33 | specify "context information should be available in specs", context do 34 | assert context[:before_each] == :ok 35 | assert context[:before_all] == :ok 36 | end 37 | 38 | after_each context do 39 | assert context[:before_each] == :ok 40 | assert context[:before_all] == :ok 41 | :ok 42 | end 43 | 44 | after_all context do 45 | assert context[:before_each] == nil 46 | assert context[:before_all] == :ok 47 | :ok 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/unit/amrita/t_checker.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule CheckerFacts do 4 | use Amrita.Sweet 5 | 6 | facts "converting predicates into strings" do 7 | fact "atom argument is rendered" do 8 | checker_as_string = Amrita.Checkers.to_s { :equals, 2 }, :pants 9 | checker_as_string |> "equals(:pants)" 10 | end 11 | 12 | fact "string argument is rendered" do 13 | checker_as_string = Amrita.Checkers.to_s { :equals, 2 }, "pants" 14 | checker_as_string |> "equals(\"pants\")" 15 | end 16 | 17 | fact "nil argument is rendered" do 18 | checker_as_string = Amrita.Checkers.to_s { :equals, 2 }, nil 19 | checker_as_string |> "equals(nil)" 20 | end 21 | 22 | fact "arity 1 renders just predicate" do 23 | checker_as_string = Amrita.Checkers.to_s { :truthy, 1 }, nil 24 | checker_as_string |> "truthy" 25 | end 26 | 27 | end 28 | 29 | facts "converts negated predicates into strings" do 30 | fact "contains both predicate and negation symbol" do 31 | checker_as_string = Amrita.Checkers.to_s :!, {:pants, {:equals, 1}} 32 | checker_as_string |> "! equals(:pants)" 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | Code.ensure_loaded?(Hex) and Hex.start 2 | 3 | defmodule Amrita.Mixfile do 4 | use Mix.Project 5 | 6 | def project do 7 | [app: :amrita, 8 | version: version, 9 | name: "Amrita", 10 | description: "A polite, well mannered and thoroughly upstanding testing framework for Elixir", 11 | source_url: "https://github.com/josephwilk/amrita", 12 | elixir: "~> 0.13.0", 13 | homepage_url: "http://amrita.io", 14 | package: [links: [{"Website", "http://amrita.io"}, 15 | {"Source", "http://github.com/josephwilk/amrita"}], 16 | contributors: ["Joseph Wilk"], 17 | licenses: ["MIT"]], 18 | deps: deps(Mix.env)] 19 | end 20 | 21 | def version do 22 | String.strip(File.read!("VERSION")) 23 | end 24 | 25 | def application do 26 | [] 27 | end 28 | 29 | defp deps(:dev) do 30 | base_deps 31 | end 32 | 33 | defp deps(:test) do 34 | base_deps ++ dev_deps 35 | end 36 | 37 | defp deps(_) do 38 | base_deps 39 | end 40 | 41 | defp base_deps do 42 | [{:meck, [branch: "master" ,github: "eproxus/meck"]}] 43 | end 44 | 45 | defp dev_deps do 46 | [{:ex_doc, github: "elixir-lang/ex_doc"}] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/unit/amrita/mocks/t_history.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule HistoryFacts do 4 | use Amrita.Sweet 5 | 6 | alias Amrita.Mocks.History, as: History 7 | 8 | #Example of meck history 9 | #[{#PID<0.301.0>,{Faker,:shout,["the mighty BOOSH"]},"the mighty BOOSH"}] 10 | 11 | facts "about History.match?" do 12 | fact "regex arguments match bit_string arguments" do 13 | :meck.new(Faker, [:non_strict]) 14 | :meck.expect(Faker, :shout, fn x -> x end) 15 | :meck.expect(Faker, :whisper, fn x -> x end) 16 | 17 | Faker.shout("the mighty BOOSH") 18 | Faker.whisper("journey through space and time") 19 | 20 | History.match?(Faker, :shout, [~r"mighty"]) |> truthy 21 | History.match?(Faker, :shout, [~r"wrong"]) |> falsey 22 | end 23 | 24 | fact "regex arguments match regex arguments" do 25 | :meck.new(Saker, [:non_strict]) 26 | :meck.expect(Saker, :shout, fn x -> x end) 27 | :meck.expect(Saker, :whisper, fn x -> x end) 28 | 29 | Saker.shout(~r"funk") 30 | Saker.whisper("shh") 31 | 32 | History.match?(Saker, :shout, [~r"funk"]) |> truthy 33 | History.match?(Saker, :shout, [~r"sunk"]) |> falsey 34 | end 35 | 36 | end 37 | end -------------------------------------------------------------------------------- /test/integration/t_checkers.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test_helper.exs", __ENV__.file 2 | 3 | defmodule Integration.CheckerFacts do 4 | use Amrita.Sweet 5 | import Support 6 | 7 | facts "about checkers with no expected argument" do 8 | defchecker thousand(actual) do 9 | actual |> equals 1000 10 | end 11 | 12 | fact "supports ! and positive form" do 13 | 1000 |> thousand 14 | 1001 |> ! thousand 15 | 16 | fail do 17 | 1001 |> thousand 18 | 1000 |> ! thousand 19 | end 20 | end 21 | end 22 | 23 | facts "about checkers with an expected argument" do 24 | defchecker valid(actual, expected) do 25 | actual |> equals expected 26 | end 27 | 28 | fact "supports ! and postive form" do 29 | 100 |> valid 100 30 | 100 |> ! valid 101 31 | 32 | fail do 33 | 100 |> valid 101 34 | 100 |> ! valid 100 35 | end 36 | end 37 | end 38 | 39 | # facts "about checkers with many expected arguments" do 40 | # defchecker sumed_up(actual, expected, x, y, z) do 41 | # actual |> equals expected + x + y + z 42 | # end 43 | # 44 | # fact "supports ! and postive form" do 45 | # Integration.CheckerFacts.sumed_up(190, 100, 20, 30, 40) 46 | # 47 | # fail do 48 | # 100 |> sumed_up 100, 20, 30, 40 49 | # end 50 | # end 51 | # end 52 | 53 | end -------------------------------------------------------------------------------- /lib/amrita/mocks/history.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Mocks.History do 2 | @moduledoc false 3 | 4 | def matches(module, fun) do 5 | Enum.filter fn_invocations(module), fn { m, f, _a } -> 6 | m == module && f == fun 7 | end 8 | end 9 | 10 | def matches(module, fun, args) do 11 | matching_fns = matches(module, fun) 12 | Enum.filter matching_fns, fn { _, _, a } -> 13 | args_match(args, a) 14 | end 15 | end 16 | 17 | def match?(module, fun, args) do 18 | !Enum.empty?(matches(module, fun, args)) 19 | end 20 | 21 | def fn_invocations(module) do 22 | Enum.map history(module), fn fn_call -> 23 | case fn_call do 24 | {_, fn_invoked, _} -> fn_invoked 25 | {_, fn_invoked, :error, :function_clause, _} -> fn_invoked 26 | end 27 | end 28 | end 29 | 30 | defp history(module) do 31 | :meck.history(module) 32 | end 33 | 34 | defp args_match([expected_arg|t1], [actual_arg|t2]) when is_bitstring(actual_arg) do 35 | if Regex.regex?(expected_arg) do 36 | Regex.match?(expected_arg, actual_arg) && args_match(t1, t2) 37 | else 38 | (expected_arg == actual_arg) && args_match(t1, t2) 39 | end 40 | end 41 | 42 | defp args_match([expected_arg|t1], [actual_arg|t2]) do 43 | (expected_arg == actual_arg) && args_match(t1, t2) 44 | end 45 | 46 | defp args_match([], []) do 47 | true 48 | end 49 | 50 | defp args_match(_, _) do 51 | false 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/integration/t_mix.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test_helper.exs", __ENV__.file 2 | 3 | defmodule Integration.Mix do 4 | use Amrita.Sweet 5 | 6 | def run_mix(cmd) do 7 | mix = System.find_executable("mix") || "vendor/elixir/bin/elixir vendor/elixir/bin/mix" 8 | iolist_to_binary(:os.cmd(~c(sh -c "#{mix} #{cmd}"))) 9 | end 10 | 11 | setup do 12 | File.mkdir_p("tmp/test") 13 | { :ok, [] } 14 | end 15 | 16 | teardown do 17 | File.rm_rf!("tmp") 18 | { :ok, [] } 19 | end 20 | 21 | @pants_template " 22 | defmodule PantsFacts do 23 | use Amrita.Sweet 24 | 25 | fact \"failing example\" do 26 | 10 |> 11 27 | end 28 | 29 | fact \"passing example\" do 30 | 10 |> 10 31 | end 32 | end" 33 | 34 | facts "about `mix amrita`" do 35 | fact "supports running tests at a specific line number" do 36 | File.write!"tmp/test/t_pants.exs", @pants_template 37 | 38 | out = run_mix "amrita tmp/test/t_pants.exs:9" 39 | 40 | out |> contains "passing example\n" 41 | out |> contains "1 facts, 0 failures" 42 | end 43 | 44 | fact "appends runtime when trace option is specified" do 45 | File.write!"tmp/test/t_pants_trace.exs", @pants_template 46 | 47 | out = run_mix "amrita tmp/test/t_pants_trace.exs --trace" 48 | 49 | out |> contains ~r/passing example \(.+?ms\)/ 50 | out |> contains ~r/failing example \(.+?ms\)/ 51 | out |> contains "2 facts, 1 failures" 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/amrita/syntax/describe.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Syntax.Describe do 2 | require ExUnit.Callbacks 3 | 4 | @moduledoc """ 5 | Provides an alternative DSL to facts and fact. 6 | """ 7 | lc facts_alias inlist [:context, :describe] do 8 | defmacro unquote(facts_alias)(description, thing \\ quote(do: _), contents) do 9 | quote do 10 | Amrita.Facts.facts(unquote(description), unquote(thing), unquote(contents)) 11 | end 12 | end 13 | end 14 | 15 | lc fact_alias inlist [:it, :specify] do 16 | defmacro unquote(fact_alias)(description, provided \\ [], meta \\ quote(do: _), contents) do 17 | quote do 18 | Amrita.Facts.fact(unquote(description), unquote(provided), unquote(meta), unquote(contents)) 19 | end 20 | end 21 | 22 | defmacro unquote(fact_alias)(description) do 23 | quote do 24 | Amrita.Facts.fact(unquote(description)) 25 | end 26 | end 27 | end 28 | 29 | defmacro before_each(var \\ quote(do: _), block) do 30 | quote do: ExUnit.Callbacks.setup(unquote(var), unquote(block)) 31 | end 32 | 33 | defmacro before_all(var \\ quote(do: _), block) do 34 | quote do: ExUnit.Callbacks.setup_all(unquote(var), unquote(block)) 35 | end 36 | 37 | defmacro after_each(var \\ quote(do: _), block) do 38 | quote do: ExUnit.Callbacks.teardown(unquote(var), unquote(block)) 39 | end 40 | 41 | defmacro after_all(var \\ quote(do: _), block) do 42 | quote do: ExUnit.Callbacks.teardown_all(unquote(var), unquote(block)) 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /lib/mix/tasks/amrita.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Amrita do 2 | use Mix.Task 3 | 4 | @switches [force: :boolean, color: :boolean, cover: :boolean, 5 | trace: :boolean, max_cases: :integer] 6 | 7 | @shortdoc "Run Amrita tests" 8 | @recursive true 9 | 10 | def run(args) do 11 | { opts, files, _ } = OptionParser.parse(args, switches: @switches) 12 | 13 | selectors = Enum.map files, fn file -> 14 | splits = String.split(file, ":") 15 | case splits do 16 | [file, line] -> [file: file, line: binary_to_integer(line)] 17 | _ -> [file: file] 18 | end 19 | end 20 | 21 | selectors = Enum.reject selectors, fn selector -> selector == nil end 22 | files = Enum.map selectors, fn selector -> selector[:file] end 23 | 24 | opts = opts ++ [selectors: selectors] 25 | 26 | unless System.get_env("MIX_ENV") do 27 | Mix.env(:test) 28 | end 29 | 30 | Mix.Task.run "app.start", args 31 | 32 | project = Mix.project 33 | 34 | :application.load(:ex_unit) 35 | Amrita.Engine.Start.configure(Dict.take(opts, [:trace, :max_cases, :color, :selectors])) 36 | 37 | test_paths = project[:test_paths] || ["test"] 38 | Enum.each(test_paths, &require_test_helper(&1)) 39 | 40 | test_paths = if files == [], do: test_paths, else: files 41 | test_pattern = project[:test_pattern] || "*.exs" 42 | 43 | files = Mix.Utils.extract_files(test_paths, test_pattern) 44 | Kernel.ParallelRequire.files files 45 | end 46 | 47 | defp require_test_helper(dir) do 48 | file = Path.join(dir, "test_helper.exs") 49 | 50 | if File.exists?(file) do 51 | Code.require_file file 52 | else 53 | raise Mix.Error, message: "Cannot run tests because test helper file #{inspect file} does not exist" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VENDORED_ELIXIR=${PWD}/vendor/elixir/bin/elixir 2 | VENDORED_MIX=${PWD}/vendor/elixir/bin/mix 3 | RUN_VENDORED_MIX=${VENDORED_ELIXIR} ${VENDORED_MIX} 4 | VERSION := $(strip $(shell cat VERSION)) 5 | STABLE_ELIXIR_VERSION = 0.13.0 6 | 7 | .PHONY: all test 8 | 9 | all: clean test 10 | 11 | clean: 12 | mix clean 13 | 14 | test: 15 | MIX_ENV=test mix do deps.get, clean, compile, amrita 16 | 17 | docs: 18 | MIX_ENV=dev mix deps.get 19 | git checkout gh-pages && git pull --rebase && git rm -rf docs && git commit -m "remove old docs" 20 | git checkout master 21 | mix docs 22 | elixir -pa ebin deps/ex_doc/bin/ex_doc "Amrita" "${VERSION}" -u "https://github.com/josephwilk/amrita" 23 | git checkout gh-pages && git add docs && git commit -m "adding new docs" && git push origin gh-pages 24 | git checkout master 25 | 26 | ci: ci_${STABLE_ELIXIR_VERSION} 27 | 28 | vendor/${STABLE_ELIXIR_VERSION}: 29 | @rm -rf vendor/* 30 | @mkdir -p vendor/elixir 31 | @wget --no-clobber -q https://github.com/elixir-lang/elixir/releases/download/v${STABLE_ELIXIR_VERSION}/precompiled.zip && unzip -qq precompiled.zip -d vendor/elixir 32 | 33 | vendor/master: 34 | @rm -rf vendor/* 35 | @mkdir -p vendor/elixir 36 | git clone --quiet https://github.com/elixir-lang/elixir.git vendor/elixir 37 | make --quiet -C vendor/elixir > /dev/null 2>&1 38 | 39 | ci_master: vendor/master 40 | @${VENDORED_ELIXIR} --version 41 | @MIX_ENV=test ${RUN_VENDORED_MIX} do clean, deps.get, compile, amrita 42 | 43 | ci_$(STABLE_ELIXIR_VERSION): vendor/${STABLE_ELIXIR_VERSION} 44 | @${VENDORED_ELIXIR} --version 45 | @MIX_ENV=test ${RUN_VENDORED_MIX} do clean, deps.get, compile, amrita 46 | 47 | test_vendored: 48 | @${VENDORED_ELIXIR} --version 49 | @${RUN_VENDORED_MIX} clean 50 | @MIX_ENV=test ${RUN_VENDORED_MIX} do clean, deps.get, compile, amrita 51 | -------------------------------------------------------------------------------- /lib/amrita/checkers/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Checkers.Exceptions do 2 | alias Amrita.Message, as: Message 3 | 4 | @moduledoc """ 5 | Checkers for expectations about Exceptions. 6 | """ 7 | 8 | @doc """ 9 | Checks if an exception was raised and that it was of the expected type or matches the 10 | expected message. Note it does not currently match when throw (Erlang errors) . 11 | 12 | ## Example 13 | fn -> raise Exception end |> raises Exception ; true 14 | fn -> raise "Jolly jolly gosh" end |> raises ~r"j(\w)+y" ; true 15 | 16 | fn -> true end |> raises Exception ; false 17 | """ 18 | def raises(function, expected_exception) when is_function(function) do 19 | try do 20 | function.() 21 | Message.fail expected_exception, "No exception raised", __ENV__.function 22 | rescue 23 | error in [expected_exception] -> error 24 | error -> 25 | name = error.__record__(:name) 26 | 27 | if name in [ExUnit.AssertionError, ExUnit.ExpectationError, Amrita.FactError, Amrita.MockError] do 28 | raise(error) 29 | else 30 | failed_exception_match(error, expected_exception) 31 | end 32 | end 33 | end 34 | 35 | defp failed_exception_match(error, expected) when is_bitstring(expected) do 36 | message = error.message 37 | if not(String.contains?(expected, message)) do 38 | Message.fail message, expected, __ENV__.function 39 | end 40 | end 41 | 42 | defp failed_exception_match(error, expected) do 43 | if Regex.regex?(expected) do 44 | message = error.message 45 | if not(Regex.match?(expected, message)) do 46 | Message.fail message, expected, __ENV__.function 47 | end 48 | else 49 | Message.fail error.__record__(:name), expected, __ENV__.function 50 | end 51 | end 52 | 53 | @doc false 54 | def raises(expected_exception) do 55 | fn function -> 56 | function |> raises expected_exception 57 | {expected_exception, __ENV__.function} 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/amrita/checkers.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Checkers do 2 | @moduledoc false 3 | 4 | defmodule Helper do 5 | 6 | @doc """ 7 | Helper function to create your own checker functions. 8 | 9 | ## Example: 10 | 11 | defchecker thousand(actual) do 12 | actual |> 1000 13 | end 14 | 15 | fact "using thousand checker" do 16 | 1000 |> thousand 17 | 1001 |> ! thousand 18 | end 19 | 20 | """ 21 | defmacro defchecker(name, _ \\ quote(do: _), contents) do 22 | { fun_name, _, args } = name 23 | 24 | neg_args = Enum.drop(args, 1) 25 | expected_arg = Enum.at(args,1) 26 | 27 | actual_arg = Enum.take(args,1) 28 | call_args = Enum.concat(actual_arg, Enum.drop(args,2)) 29 | called_with_args = Enum.concat(actual_arg, Enum.drop(args,1)) 30 | 31 | quote do 32 | def unquote(fun_name)(unquote_splicing(args)) do 33 | import Kernel, except: [|>: 2] 34 | import Amrita.Elixir.Pipeline 35 | 36 | unquote(contents) 37 | end 38 | 39 | def unquote(fun_name)(unquote_splicing(neg_args)) do 40 | case Enum.count(unquote(neg_args)) do 41 | 0 -> fn(actual) -> unquote(fun_name)(actual); {nil, __ENV__.function} end 42 | 43 | _ -> fn(unquote_splicing(call_args)) -> 44 | unquote(fun_name)(unquote_splicing(called_with_args)) 45 | {unquote(expected_arg), __ENV__.function} 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | 53 | def to_s(module, fun, args) do 54 | to_s "#{inspect(module)}.#{fun}", args 55 | end 56 | 57 | def to_s({function_name, 1}, _) do 58 | "#{function_name}" 59 | end 60 | 61 | def to_s({function_name, _arity}, args) do 62 | to_s(function_name, args) 63 | end 64 | 65 | def to_s(:!, { expected, { fun, arity }}) do 66 | "! " <> to_s({ fun, arity + 1 }, expected) 67 | end 68 | 69 | def to_s(function_name, args) when is_list(args) do 70 | str_args = Enum.map args, fn a -> inspect(a) end 71 | "#{function_name}(#{Enum.join(str_args, ",")})" 72 | end 73 | 74 | def to_s(function_name, args) do 75 | "#{function_name}(#{inspect(args)})" 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule Support do 2 | defexception FactDidNotFail, [:line, :file, :form] do 3 | def message(exception) do 4 | "Expected:\n" <> 5 | " #{Macro.to_string(exception.form)} " <> exception.location <> "\n" <> 6 | " to fail but it passed." 7 | end 8 | 9 | def location(exception) do 10 | IO.ANSI.escape_fragment("%{cyan}") <> 11 | "# #{Path.relative_to(exception.file, System.cwd)}:#{exception.line}" <> 12 | IO.ANSI.escape_fragment("%{red}") 13 | end 14 | end 15 | 16 | defmacro failing_fact(name, _ \\ quote(do: _), contents) do 17 | quote do 18 | fact unquote(name) do 19 | fail unquote(name) do 20 | unquote(Support.Wrap.assertions(contents)) 21 | end 22 | end 23 | end 24 | end 25 | 26 | defmacro fail(_name \\ "", _ \\ quote(do: _), contents) do 27 | Support.Wrap.assertions(contents) 28 | end 29 | 30 | defmodule Wrap do 31 | def assertions([ do: forms ]) when is_list(forms), do: [do: Enum.map(forms, &assertions(&1))] 32 | 33 | def assertions([ do: { :provided, [line: line], [a, mocks] } ]) do 34 | inject_exception_test([ do: { :provided, [line: line], [a, assertions(mocks)]}], line) 35 | end 36 | 37 | def assertions([ do: thing ]), do: [do: assertions(thing)] 38 | 39 | def assertions({ :__block__, m, forms }) do 40 | { :__block__, m, Enum.map(forms, &assertions(&1)) } 41 | end 42 | 43 | def assertions({ :|>, [line: line], _args } = test) do 44 | inject_exception_test(test, line) 45 | end 46 | 47 | def assertions(form), do: form 48 | 49 | defp inject_exception_test(form, line) do 50 | quote do 51 | try do 52 | unquote(form) 53 | 54 | raise FactDidNotFail, file: __ENV__.file, line: unquote(line), form: unquote(Macro.escape(form)) 55 | catch 56 | #Raised by :meck when a match is not found with a mock 57 | :error, error when error in [:function_clause, :undef] -> true 58 | rescue 59 | error in [Amrita.FactError, Amrita.MockError] -> true 60 | end 61 | end 62 | end 63 | 64 | end 65 | end 66 | 67 | if Amrita.Elixir.Version.less_than_or_equal?([0, 9, 3]) do 68 | Amrita.start 69 | else 70 | Amrita.start(formatter: Amrita.Formatter.Documentation) 71 | end 72 | -------------------------------------------------------------------------------- /test/unit/amrita/t_mocks.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule MocksFacts do 4 | use Amrita.Sweet 5 | 6 | alias Amrita.Mocks.Provided, as: Provided 7 | 8 | facts "about parsing valid prerequisites" do 9 | fact "returns a dict indexed by module and function with {module, function, argument, return_value}" do 10 | prerequisites = Provided.Parse.prerequisites(quote do: [Funk.monkey(4) |> 10]) 11 | mocks = Provided.Prerequisites.by_module_and_fun(prerequisites, Funk, :monkey) 12 | 13 | mocks |> contains {Funk, :monkey, [4], 10} 14 | end 15 | 16 | fact "stores mocks with same module and function together" do 17 | prerequisites = Provided.Parse.prerequisites(quote do: [Funk.monkey(4) |> 10, Funk.monkey(5) |> 11]) 18 | mocks = Provided.Prerequisites.by_module_and_fun(prerequisites, Funk, :monkey) 19 | 20 | 21 | mocks |> contains {Funk, :monkey, [4], 10} 22 | mocks |> contains {Funk, :monkey, [5], 11} 23 | end 24 | 25 | fact "returns an empty dict when there are no prerequeistes" do 26 | Provided.Parse.prerequisites(quote do: []) |> equals Provided.Prerequisites.new [] 27 | end 28 | end 29 | 30 | facts "about parsing invalid prerequisites" do 31 | fact "raises a parse exception" do 32 | fn -> 33 | Provided.Parse.prerequisites(quote do: [monkey(4) |> 10]) 34 | end |> raises Amrita.Mocks.Provided.Parse.Error 35 | 36 | fn -> 37 | Provided.Parse.prerequisites(quote do: [monkey(4)]) 38 | end |> raises Amrita.Mocks.Provided.Parse.Error 39 | 40 | fn -> 41 | Provided.Parse.prerequisites(quote do: [4 |> 10]) 42 | end |> raises Amrita.Mocks.Provided.Parse.Error 43 | 44 | fn -> 45 | Provided.Parse.prerequisites(quote do: [10]) 46 | end |> raises Amrita.Mocks.Provided.Parse.Error 47 | end 48 | end 49 | 50 | facts "about resolving mock arguments" do 51 | fact "anything resolves to _" do 52 | prerequisites = Provided.Parse.prerequisites(quote do: [Funk.monkey(anything) |> 10]) 53 | resolved_prerequisites = Provided.__resolve_args__(prerequisites, __MODULE__, __ENV__) 54 | 55 | mocks = Provided.Prerequisites.by_module_and_fun(resolved_prerequisites, Funk, :monkey) 56 | 57 | mocks |> equals [{Funk, :monkey, [:_], 10}] 58 | end 59 | end 60 | 61 | end -------------------------------------------------------------------------------- /lib/amrita/formatter/format.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Formatter.Format do 2 | @moduledoc false 3 | 4 | import Exception, only: [format_stacktrace_entry: 1] 5 | 6 | @doc """ 7 | Receives a pending test and formats it. 8 | """ 9 | def format_test_pending(ExUnit.Test[] = test, counter, color) do 10 | ExUnit.Test[case: test_case, name: test_name, state: { :failed, { _kind, _reason, stacktrace }}] = test 11 | 12 | test_info("#{counter})", color) <> 13 | error_info("#{format_test_name(test)}", color) <> 14 | format_location(find_case(stacktrace, test_case, test_name), test_case, test_name, color) 15 | end 16 | 17 | defp find_case([head|tail], test_case, test_name) do 18 | case head do 19 | { ^test_case, ^test_name, _, _ } -> [head] 20 | _ -> find_case(tail, test_case, test_name) 21 | end 22 | end 23 | 24 | defp find_case([], _, _) do 25 | [] 26 | end 27 | 28 | def format_test_name(ExUnit.Test[name: name]) do 29 | case atom_to_binary(name) do 30 | "test_" <> rest -> rest 31 | "test " <> rest -> rest 32 | end 33 | end 34 | 35 | def colorize(escape, string) do 36 | if System.get_env("NO_COLOR") do 37 | string 38 | else 39 | IO.ANSI.escape_fragment("%{#{escape}}") <> string <> IO.ANSI.escape_fragment("%{reset}") 40 | end 41 | end 42 | 43 | defp format_location([{ test_case, test, _, [ file: file, line: line ] }|_], test_case, test, color) do 44 | location_info("# #{Path.relative_to_cwd(file)}:#{line}", color) 45 | end 46 | 47 | defp format_location(stacktrace, _case, test, color) do 48 | location_info("# #{Enum.map_join(stacktrace, fn(s) -> format_stacktrace(s, test, nil, color) end)}", color) 49 | end 50 | 51 | defp format_stacktrace([{ test_case, test, _, [ file: file, line: line ] }|_], test_case, test, color) do 52 | location_info("at #{Path.relative_to_cwd(file)}:#{line}", color) 53 | end 54 | 55 | defp format_stacktrace(stacktrace, _case, _test, color) do 56 | location_info("stacktrace:", color) <> 57 | Enum.map_join(stacktrace, fn(s) -> stacktrace_info format_stacktrace_entry(s), color end) 58 | end 59 | 60 | defp test_info(msg, nil), do: " " <> msg <> " " 61 | defp test_info(msg, color), do: test_info(color.(:test_info, msg), nil) 62 | 63 | defp error_info(msg, nil), do: "" <> msg <> "\n" 64 | defp error_info(msg, color), do: error_info(color.(:error_info, msg), nil) 65 | 66 | defp location_info(msg, nil), do: " " <> msg <> "\n" 67 | defp location_info(msg, color), do: location_info(color.(:location_info, msg), nil) 68 | 69 | defp stacktrace_info(msg, nil), do: " " <> msg <> "\n" 70 | defp stacktrace_info(msg, color), do: stacktrace_info(color.(:stacktrace_info, msg), nil) 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/amrita/elixir/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Elixir.Pipeline do 2 | @moduledoc false 3 | 4 | import Kernel, except: [|>: 2] 5 | 6 | defmacro left |> right do 7 | pipeline_op(left, right) 8 | end 9 | 10 | defp pipeline_op(left, { :|>, _, [middle, right] }) do 11 | pipeline_op(pipeline_op(left, middle), right) 12 | end 13 | 14 | defp pipeline_op(left, { call, line, atom }) when is_atom(atom) do 15 | quote do 16 | local_var_value = binding[unquote(call)] 17 | if local_var_value do 18 | unquote(left) |> Amrita.Checkers.Simple.equals local_var_value 19 | else 20 | Code.eval_quoted({unquote(call), unquote(line), [unquote(left)]}, 21 | binding, 22 | __ENV__.to_keywords |> Keyword.put(:delegate_locals_to, __MODULE__)) 23 | end 24 | end 25 | end 26 | 27 | # Comparing to tuples 28 | defp pipeline_op(left, { :{}, _, _ }=right) do 29 | quote do 30 | unquote(left) |> Amrita.Checkers.Simple.equals unquote(right) 31 | end 32 | end 33 | 34 | defp pipeline_op(left, { _, _ }=right) do 35 | quote do 36 | unquote(left) |> Amrita.Checkers.Simple.equals unquote(right) 37 | end 38 | end 39 | 40 | # Comparing ranges 41 | defp pipeline_op({ :.., _, _ }=left, { :.., _, _ }=right) do 42 | quote do 43 | unquote(left) |> Amrita.Checkers.Simple.equals unquote(right) 44 | end 45 | end 46 | 47 | # Comparing HashDict 48 | defp pipeline_op(left, {{ :., _, [{ :__aliases__, _, [:HashDict]}, _] }, _, _ }=right) do 49 | quote do 50 | unquote(left) |> Amrita.Checkers.Simple.equals unquote(right) 51 | end 52 | end 53 | 54 | # Comparing HashSet 55 | defp pipeline_op(left, {{ :., _, [{ :__aliases__, _, [:HashSet]}, _] }, _, _ }=right) do 56 | quote do 57 | unquote(left) |> Amrita.Checkers.Simple.equals unquote(right) 58 | end 59 | end 60 | 61 | defp pipeline_op(left, { call, line, args }) when is_list(args) do 62 | { call, line, [left|args] } 63 | end 64 | 65 | #Patching pipeline so it supports non-fn values 66 | defp pipeline_op(left, right) when is_integer(right) or 67 | is_bitstring(right) or 68 | is_atom(right) or 69 | is_list(right) or 70 | is_tuple(right) do 71 | quote do 72 | unquote(left) |> Amrita.Checkers.Simple.equals unquote(right) 73 | end 74 | end 75 | 76 | defp pipeline_op(left, atom) when is_atom(atom) do 77 | { { :., [], [left, atom] }, [], [] } 78 | end 79 | 80 | defp pipeline_op(_, other) do 81 | pipeline_error(other) 82 | end 83 | 84 | defp pipeline_error(arg) do 85 | raise ArgumentError, message: "Unsupported expression in pipeline |> operator: #{Macro.to_string arg}" 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /test/unit/amrita/elixir/t_pipeline.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../../../test_helper.exs", __ENV__.file 2 | 3 | defmodule PipelineFacts do 4 | use Amrita.Sweet 5 | import Support 6 | 7 | def example(x) do 8 | x |> equals 10 9 | end 10 | 11 | fact "|> supports expected value as a var" do 12 | a = "var test" 13 | b = "var test" 14 | 15 | a |> b 16 | 17 | a = 10 18 | 10 |> a 19 | 20 | b = 10 21 | b |> example 22 | 23 | fail do 24 | a = "var test" 25 | b = "fail" 26 | 27 | a |> b 28 | 29 | a = 10 30 | a |> 11 31 | 32 | b = 11 33 | 10 |> b 34 | end 35 | end 36 | 37 | facts "defaults to equals checker" do 38 | 39 | fact "strings" do 40 | "yes" |> "yes" 41 | 42 | fail :strings do 43 | "yes" |> "no" 44 | end 45 | end 46 | 47 | fact "integers" do 48 | 1 |> 1 49 | 50 | fail :integers do 51 | 1 |> 2 52 | end 53 | end 54 | 55 | fact "atoms" do 56 | :yes |> :yes 57 | 58 | fail do 59 | :no |> :yes 60 | end 61 | end 62 | 63 | fact "lists" do 64 | [1, 2, 3] |> [1, 2, 3] 65 | 66 | fail do 67 | [1, 2, 3] |> [1, 2, 4] 68 | end 69 | end 70 | 71 | fact "tuples" do 72 | { 1, 2, 3 } |> { 1, 2, 3 } 73 | 74 | fail do 75 | { 1, 2, 3 } |> { 1, 2, 4 } 76 | end 77 | end 78 | 79 | fact "tuples with a pattern match" do 80 | { 1, 2, 3 } |> { 1, _, 3 } 81 | 82 | fail do 83 | { 1, 2, 4 } |> { _, 2, 5 } 84 | end 85 | end 86 | 87 | fact "lists with a pattern match" do 88 | [ 1, 2, 3 ] |> [ 1, _, 3 ] 89 | 90 | fail do 91 | [ 1, 2, 3 ] |> [ 2, _, 3 ] 92 | end 93 | end 94 | 95 | fact "ranges" do 96 | 1..2 |> 1..2 97 | 98 | fail do 99 | 1..2 |> 1..3 100 | end 101 | end 102 | 103 | fact "hashdict" do 104 | Enum.into([{:b, 1}], [{:a, 2}]) |> equals Enum.into([{:b, 1}], [{:a, 2}]) 105 | 106 | fail do 107 | Enum.into([{:b, 1}], [{:a, 2}]) |> equals Enum.into([{:b, 1}], [{:a, 6}]) 108 | end 109 | end 110 | 111 | fact "hashset" do 112 | HashSet.new([1,2,3]) |> HashSet.new([1,2,3]) 113 | 114 | fail do 115 | HashSet.new([1,2,3]) |> HashSet.new([1,2,4]) 116 | end 117 | end 118 | 119 | end 120 | 121 | facts "pipelines non test assertion behaviour" do 122 | fact "simple" do 123 | [1, [2], 3] |> List.flatten |> [1, 2, 3] 124 | 125 | fail do 126 | [1, [2], 3] |> List.flatten |> [1, 2, 4] 127 | end 128 | end 129 | 130 | fact "nested" do 131 | [1, [2], 3] |> List.flatten |> Enum.map(&(&1 *2)) |> [2, 4, 6] 132 | 133 | fail do 134 | [1, [2], 3] |> List.flatten |> Enum.map(fn(x) -> (x * 2) end) |> [2, 4, 9] 135 | end 136 | end 137 | 138 | end 139 | 140 | end 141 | -------------------------------------------------------------------------------- /lib/amrita/checkers/collection.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Checkers.Collections do 2 | alias Amrita.Message, as: Message 3 | 4 | @moduledoc """ 5 | Checkers which are designed to work with collections (lists, tuples, keyword lists, strings). 6 | """ 7 | 8 | @doc """ 9 | Checks that the collection contains element. 10 | 11 | ## Examples 12 | [1, 2, 3] |> contains 3 13 | {1, 2, 3} |> contains 2 14 | 15 | "elixir of life" |> contains "of" 16 | 17 | "elixir of life" |> contains ~r/"of"/ 18 | 19 | """ 20 | def contains(collection,element) do 21 | if Regex.regex?(element) do 22 | r = Regex.match?(element, collection) 23 | else 24 | r = case collection do 25 | c when is_tuple(c) -> element in tuple_to_list(c) 26 | c when is_list(c) -> element in c 27 | c when is_bitstring(element) -> String.contains?(c, element) 28 | end 29 | end 30 | if (not r), do: Message.fail(collection, element, __ENV__.function) 31 | end 32 | 33 | @doc false 34 | def contains(element) do 35 | fn collection -> 36 | collection |> contains element 37 | { element, __ENV__.function } 38 | end 39 | end 40 | 41 | @doc """ 42 | Checks that the actual result starts with the expected result. 43 | 44 | ## Examples 45 | [1 2 3] |> has_prefix [1 2] ; true 46 | [1 2 3] |> has_prefix [2 1] ; false 47 | 48 | {1, 2, 3} |> has_prefix {1, 2} ; true 49 | 50 | "I cannot explain myself for I am not myself" |> has_prefix "I" 51 | 52 | """ 53 | def has_prefix(collection, prefix) when is_list(collection) and is_record(prefix, HashSet) do 54 | collection_prefix = Enum.take(collection, Enum.count(prefix)) 55 | 56 | r = fail_fast_contains?(collection_prefix, prefix) 57 | 58 | if not(r), do: Message.fail(prefix, collection, __ENV__.function) 59 | end 60 | 61 | def has_prefix(collection, prefix) do 62 | r = case collection do 63 | c when is_tuple(c) -> 64 | collection_prefix = Enum.take(tuple_to_list(collection), tuple_size(prefix)) 65 | collection_prefix = list_to_tuple(collection_prefix) 66 | collection_prefix == prefix 67 | c when is_list(c) -> 68 | Enum.take(collection, Enum.count(prefix)) == prefix 69 | _ when is_bitstring(prefix) -> 70 | String.starts_with?(collection, prefix) 71 | end 72 | 73 | if not(r), do: Message.fail(prefix, collection, __ENV__.function) 74 | end 75 | 76 | @doc false 77 | def has_prefix(element) do 78 | fn collection -> 79 | collection |> has_prefix element 80 | {element, __ENV__.function} 81 | end 82 | end 83 | 84 | @doc """ 85 | Checks that the actual result ends with the expected result. 86 | 87 | ## Examples: 88 | [1 2 3] |> has_suffix [2 3] ; true 89 | [1 2 3] |> has_suffix [3 2] ; false 90 | 91 | {1, 2, 3} |> has_suffix [3] ; true 92 | 93 | "I cannot explain myself for I am not myself" |> has_suffix "myself" 94 | 95 | """ 96 | def has_suffix(collection, suffix) when is_list(collection) and is_record(suffix, HashSet) do 97 | collection_suffix = Enum.drop(collection, Enum.count(collection) - Enum.count(suffix)) 98 | 99 | r = fail_fast_contains?(collection_suffix, suffix) 100 | 101 | if not(r), do: Message.fail(suffix, collection, __ENV__.function) 102 | end 103 | 104 | def has_suffix(collection, suffix) do 105 | r = case collection do 106 | c when is_tuple(c) -> 107 | collection_suffix = Enum.drop(tuple_to_list(collection), tuple_size(collection) - tuple_size(suffix)) 108 | collection_suffix = list_to_tuple(collection_suffix) 109 | collection_suffix == suffix 110 | c when is_list(c) -> 111 | collection_suffix = Enum.drop(collection, Enum.count(collection) - Enum.count(suffix)) 112 | collection_suffix == suffix 113 | _ when is_bitstring(suffix) -> 114 | String.ends_with?(collection, suffix) 115 | end 116 | 117 | if not(r), do: Message.fail(suffix, collection, __ENV__.function) 118 | end 119 | 120 | @doc false 121 | def has_suffix(element) do 122 | fn collection -> 123 | collection |> has_suffix element 124 | {element, __ENV__.function} 125 | end 126 | end 127 | 128 | @doc """ 129 | Checks whether a predicate holds for all elements in a collection. 130 | 131 | ## Examples: 132 | [1, 3, 5, 7] |> for_all odd(&1) ; true 133 | [2, 3, 5, 7] |> for_all odd(&1) ; false 134 | """ 135 | def for_all(collection, fun) do 136 | Enum.each(collection, fun) 137 | end 138 | 139 | @doc """ 140 | Checks whether a predicate holds for at least one element in a collection. 141 | 142 | ## Examples: 143 | [2, 4, 7, 8] |> for_some odd(&1) ; true 144 | [2, 4, 6, 8] |> for_some odd(&1) ; false 145 | """ 146 | def for_some(collection, fun) do 147 | r = Enum.any?(Enum.map(collection, (fn value -> 148 | try do 149 | fun.(value) 150 | true 151 | rescue 152 | [Amrita.FactError, Amrita.MockError, ExUnit.AssertionError] -> false 153 | end 154 | end))) 155 | 156 | if not(r), do: Message.fail(fun, collection, __ENV__.function) 157 | end 158 | 159 | defp fail_fast_contains?(collection1, collection2) do 160 | try do 161 | Enum.reduce(collection1, true, fn(value, acc) -> 162 | case value in collection2 do 163 | true -> acc 164 | _ -> throw(:error) 165 | end 166 | end) 167 | catch 168 | :error -> false 169 | end 170 | end 171 | end -------------------------------------------------------------------------------- /lib/amrita/checkers/simple.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Checkers.Simple do 2 | alias Amrita.Message, as: Message 3 | 4 | @moduledoc """ 5 | Checkers for operating on single forms like numbers, atoms, bools, floats, etc. 6 | """ 7 | 8 | @doc """ 9 | Check if actual is odd. 10 | 11 | ## Example 12 | 2 |> even ; true 13 | 14 | """ 15 | def odd(number) when is_integer(number) do 16 | r = rem(number, 2) == 1 17 | 18 | if not(r), do: Message.fail(number, __ENV__.function) 19 | end 20 | 21 | @doc """ 22 | Check if actual is even. 23 | 24 | ## Example 25 | 2 |> even ; true 26 | """ 27 | def even(number) when is_integer(number) do 28 | r = rem(number, 2) == 0 29 | 30 | if not(r), do: Message.fail(number, __ENV__.function) 31 | end 32 | 33 | @doc """ 34 | Check if `actual` evaluates to precisely true. 35 | 36 | ## Example 37 | "mercury" |> truthy ; true 38 | nil |> truthy ; false 39 | """ 40 | def truthy(actual) do 41 | if actual do 42 | r = true 43 | else 44 | r = false 45 | end 46 | 47 | if not(r), do: Message.fail(actual, __ENV__.function) 48 | end 49 | 50 | @doc false 51 | def truthy do 52 | fn actual -> 53 | actual |> truthy 54 | {nil, __ENV__.function} 55 | end 56 | end 57 | 58 | @doc """ 59 | Check if `actual` evaluates to precisely false. 60 | 61 | ## Example 62 | nil |> falsey ; true 63 | "" |> falsey ; false 64 | """ 65 | def falsey(actual) do 66 | if actual do 67 | r = false 68 | else 69 | r = true 70 | end 71 | 72 | if not(r), do: Message.fail(actual, __ENV__.function) 73 | end 74 | 75 | @doc false 76 | def falsey do 77 | fn actual -> 78 | actual |> falsey 79 | {nil, __ENV__.function} 80 | end 81 | end 82 | 83 | @doc """ 84 | Checks if actual is within delta of the expected value. 85 | 86 | ## Example 87 | 0.1 |> roughly 0.2, 0.2 ; true 88 | 0.1 |> roughly 0.01, 0.2 ; false 89 | """ 90 | def roughly(actual, expected, delta) do 91 | r = (expected >= (actual - delta)) and (expected <= (actual + delta)) 92 | 93 | if not(r), do: Message.fail(actual, "#{expected} +-#{delta}", __ENV__.function) 94 | end 95 | 96 | @doc """ 97 | Checks if actual is a value within 1/1000th of the expected value. 98 | 99 | ## Example 100 | 0.10001 |> roughly 0.1 ; true 101 | 0.20001 |> roughly 0.1 ; false 102 | """ 103 | def roughly(actual, expected) do 104 | roughly(actual, expected, 0.01) 105 | end 106 | 107 | @doc false 108 | def roughly(expected) do 109 | fn actual -> 110 | actual |> roughly expected 111 | {expected, __ENV__.function} 112 | end 113 | end 114 | 115 | @doc """ 116 | Checks if a tuple matches another tuple. 117 | 118 | ## Example 119 | { 1, 2, 3 } |> matches { _, 2, _ } 120 | """ 121 | defmacro matches(actual, expected) do 122 | need_extract = case actual do 123 | { :received, _, _ } -> true 124 | _ -> false 125 | end 126 | 127 | if(need_extract) do 128 | quote do 129 | r = match?(unquote(expected), (unquote(actual).())) 130 | if not(r), do: Amrita.Message.fail(unquote(actual), unquote(Macro.to_string(expected)), __ENV__.function) 131 | end 132 | else 133 | quote do 134 | r = match?(unquote(expected), unquote(actual)) 135 | if not(r), do: Amrita.Message.fail(unquote(actual), unquote(Macro.to_string(expected)), __ENV__.function) 136 | end 137 | end 138 | end 139 | 140 | @doc """ 141 | Checks if actual == expected. 142 | 143 | ## Example 144 | 1000 |> equals 1000 ; true 145 | 1000 |> equals 0 ; false 146 | """ 147 | defmacro equals(actual, expected) do 148 | use_match = case expected do 149 | { :{}, _, _ } -> true 150 | { _, _ } -> true 151 | e when is_list(e) -> true 152 | _ -> false 153 | end 154 | 155 | need_extract = case actual do 156 | { :received, _, _ } -> true 157 | _ -> false 158 | end 159 | 160 | if(use_match) do 161 | if(need_extract) do 162 | quote do 163 | unquote(actual).() |> matches unquote(expected) 164 | end 165 | else 166 | quote do 167 | unquote(actual) |> matches unquote(expected) 168 | end 169 | end 170 | else 171 | if(need_extract) do 172 | quote do 173 | r = ((unquote(actual).()) == unquote(expected)) 174 | 175 | if (not r), do: Message.fail((unquote(actual).()), unquote(expected), { :equals, 2 }) 176 | end 177 | else 178 | quote do 179 | r = (unquote(actual) == unquote(expected)) 180 | 181 | if (not r), do: Message.fail(unquote(actual), unquote(expected), { :equals, 2 }) 182 | end 183 | end 184 | end 185 | end 186 | 187 | @doc false 188 | def equals(expected) do 189 | fn actual -> 190 | actual |> equals expected 191 | {expected, __ENV__.function} 192 | end 193 | end 194 | 195 | @doc """ 196 | Checks if the function returns the expected result, this is used for 197 | checking received messages 198 | 199 | ## Examples 200 | fn -> :hello end |> :hello 201 | received |> msg(:hello) 202 | 203 | """ 204 | def msg(function, expected) do 205 | actual = function.() 206 | actual |> equals expected 207 | end 208 | 209 | @doc """ 210 | Negates all following checkers. 211 | 212 | ## Examples 213 | 214 | [1, 2, 3, 4] |> ! contains 999 ; true 215 | [1, 2, 3, 4] |> ! contains 4 ; false 216 | """ 217 | def unquote(:!)(actual, checker) when is_function(checker) do 218 | r = try do 219 | checker.(actual) 220 | rescue 221 | error in [Amrita.FactError, Amrita.MockError, ExUnit.AssertionError] -> false 222 | error -> raise(error) 223 | end 224 | 225 | if r, do: Message.fail(actual, r, __ENV__.function) 226 | end 227 | 228 | def unquote(:!)(actual, value) do 229 | value |> ! equals actual 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/amrita.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita do 2 | @moduledoc """ 3 | A polite, well mannered and thoroughly upstanding testing framework for Elixir. 4 | """ 5 | 6 | @doc """ 7 | Start Amrita for a test run. 8 | 9 | This should be called in your test_helper.exs file. 10 | 11 | Supports optional config: 12 | 13 | # Use a custom formatter. Defaults to Progress formatter. 14 | Amrita.start(formatter: Amrita.Formatter.Documentation) 15 | 16 | """ 17 | def start(opts \\ []) do 18 | formatter = Keyword.get(opts, :formatter, Amrita.Formatter.Progress) 19 | Amrita.Engine.Start.now formatter: formatter 20 | end 21 | 22 | @doc """ 23 | Polite version of start. 24 | """ 25 | def please_start(opts \\ []) do 26 | start(opts) 27 | end 28 | 29 | defmodule Sweet do 30 | @moduledoc """ 31 | Responsible for loading Amrita within a test module. 32 | 33 | ## Example: 34 | defmodule TestsAboutSomething do 35 | use Amrita.Sweet 36 | end 37 | """ 38 | 39 | @doc false 40 | defmacro __using__(opts \\ []) do 41 | async = Keyword.get(opts, :async, false) 42 | quote do 43 | if !Enum.any?(__ENV__.requires, fn(x) -> x == ExUnit.Case end) do 44 | use ExUnit.Case, async: unquote(async) 45 | end 46 | 47 | import ExUnit.Callbacks 48 | import ExUnit.Assertions 49 | import ExUnit.Case 50 | @ex_unit_case true 51 | 52 | use Amrita.Facts 53 | use Amrita.Mocks 54 | 55 | import Amrita.Checkers.Helper 56 | import Amrita.Checkers.Simple 57 | import Amrita.Checkers.Collections 58 | import Amrita.Checkers.Exceptions 59 | import Amrita.Checkers.Messages 60 | import Amrita.Syntax.Describe 61 | end 62 | end 63 | end 64 | 65 | defmodule Facts do 66 | @moduledoc """ 67 | Express facts about your code. 68 | """ 69 | 70 | @doc false 71 | defmacro __using__(_) do 72 | quote do 73 | @name_stack [] 74 | import Amrita.Facts 75 | end 76 | end 77 | 78 | defp fact_name(name) do 79 | cond do 80 | is_binary(name) -> name 81 | is_tuple(name) -> name 82 | true -> "#{name}" 83 | end 84 | end 85 | 86 | @doc """ 87 | A fact is the container of your test logic. 88 | 89 | ## Example 90 | fact "about addition" do 91 | ... 92 | end 93 | 94 | If you are using mocks you can define them as part of your fact. 95 | 96 | ## Example 97 | fact "about mock", provided: [Flip.flop(:ok) |> true] do 98 | Flip.flop(:ok) |> truthy 99 | end 100 | 101 | You can optionally examine meta data passed to each fact. Useful when used 102 | with callbacks: 103 | 104 | ## Example 105 | setup do 106 | {:ok, ping: "pong"} 107 | end 108 | 109 | fact "with meta data", meta do 110 | meta[:pong] |> "pong" 111 | end 112 | """ 113 | defmacro fact(description, provided \\ [], var \\ quote(do: _), contents) do 114 | var = case provided do 115 | [provided: _] -> var 116 | [] -> var 117 | _ -> provided 118 | end 119 | 120 | quote do 121 | deffact Enum.join(@name_stack, "") <> unquote(fact_name(description)), unquote(var) do 122 | import Kernel, except: [|>: 2] 123 | import Amrita.Elixir.Pipeline 124 | 125 | unquote do 126 | if is_list(provided) && !Enum.empty?(provided) && match?({:provided, _}, Enum.at(provided, 0)) do 127 | { :provided, mocks } = Enum.at(provided, 0) 128 | quote do 129 | provided unquote(mocks) do 130 | unquote(contents) 131 | end 132 | end 133 | else 134 | quote do 135 | unquote(contents) 136 | end 137 | end 138 | end 139 | end 140 | end 141 | end 142 | 143 | @doc """ 144 | A fact without a body is a pending fact. Much like a TODO. 145 | It prints a reminder when the tests are run. 146 | 147 | ## Example 148 | fact "something thing I need to implement at somepoint" 149 | 150 | """ 151 | defmacro fact(description) do 152 | quote do 153 | deffact Enum.join(@name_stack, "") <> unquote(fact_name(description)) do 154 | Amrita.Message.pending unquote(description) 155 | end 156 | end 157 | end 158 | 159 | @doc """ 160 | A future_fact is a pending fact. Its body is *NEVER* run. 161 | Instead it simply prints an reminder that it is yet to be run. 162 | 163 | ## Example: 164 | future_fact "about something that does not work yet" do 165 | .. 166 | end 167 | """ 168 | defmacro future_fact(description, _ \\ quote(do: _), _) do 169 | quote do 170 | deffact Enum.join(@name_stack, "") <> unquote(fact_name(description)) do 171 | Amrita.Message.pending unquote(description) 172 | end 173 | end 174 | end 175 | 176 | @doc """ 177 | facts are used to group with a name a number of fact tests. 178 | You can nest as many facts as you feel you need. 179 | 180 | ## Example 181 | facts "about arithmetic" do 182 | fact "about addition" do 183 | ... 184 | end 185 | end 186 | """ 187 | defmacro facts(description, _ \\ quote(do: _), contents) do 188 | quote do 189 | @name_stack Enum.concat(@name_stack, [unquote(fact_name(description)) <> " - "]) 190 | unquote(contents) 191 | if Enum.count(@name_stack) > 0 do 192 | @name_stack Enum.take(@name_stack, Enum.count(@name_stack) - 1) 193 | end 194 | end 195 | end 196 | 197 | @doc false 198 | defmacro deffact(message, var \\ quote(do: _), contents) do 199 | contents = 200 | case contents do 201 | [do: _] -> 202 | quote do 203 | unquote(contents) 204 | :ok 205 | end 206 | _ -> 207 | quote do 208 | try(unquote(contents)) 209 | :ok 210 | end 211 | end 212 | 213 | var = Macro.escape(var) 214 | contents = Macro.escape(contents, unquote: true) 215 | 216 | quote bind_quoted: binding do 217 | message = if is_binary(message) do 218 | :"test #{message}" 219 | else 220 | :"test_#{message}" 221 | end 222 | 223 | def unquote(message)(unquote(var)) do 224 | unquote(contents) 225 | end 226 | 227 | def unquote(:"__#{message}__")(), do: [file: __ENV__.file, line: __ENV__.line] 228 | end 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/amrita/formatter/progress.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Formatter.Progress do 2 | @moduledoc """ 3 | Provides a abbreviated summary of test output: 4 | . = Pass 5 | F = Fail 6 | P = Pending 7 | 8 | Along with a summary detailing all fails 9 | """ 10 | 11 | @behaviour ExUnit.Formatter 12 | @timeout 30_000 13 | use GenServer.Behaviour 14 | 15 | import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 6, format_test_case_failure: 5] 16 | 17 | defrecord Config, tests_counter: 0, invalid_counter: 0, pending_counter: 0, 18 | test_failures: [], case_failures: [], pending_failures: [], trace: false 19 | 20 | ## Behaviour 21 | 22 | def suite_started(opts) do 23 | { :ok, pid } = :gen_server.start_link(__MODULE__, opts[:trace], []) 24 | pid 25 | end 26 | 27 | def suite_finished(id, run_us, load_us) do 28 | :gen_server.call(id, { :suite_finished, run_us, load_us }, @timeout) 29 | end 30 | 31 | def case_started(id, test_case) do 32 | :gen_server.cast(id, { :case_started, test_case }) 33 | end 34 | 35 | def case_finished(id, test_case) do 36 | :gen_server.cast(id, { :case_finished, test_case }) 37 | end 38 | 39 | def test_started(id, test) do 40 | :gen_server.cast(id, { :test_started, test }) 41 | end 42 | 43 | def test_finished(id, test) do 44 | :gen_server.cast(id, { :test_finished, test }) 45 | end 46 | 47 | ## Callbacks 48 | 49 | def init(trace) do 50 | { :ok, Config[trace: trace] } 51 | end 52 | 53 | def handle_call({ :suite_finished, run_us, load_us }, _from, config) do 54 | print_suite(config.tests_counter, config.invalid_counter, config.pending_counter, 55 | config.test_failures, config.case_failures, config.pending_failures, run_us, load_us) 56 | { :stop, :normal, length(config.test_failures), config } 57 | end 58 | 59 | def handle_call(reqest, from, config) do 60 | super(reqest, from, config) 61 | end 62 | 63 | def handle_cast({ :test_started, ExUnit.Test[] = test }, config) do 64 | if config.trace, do: IO.write(" * #{trace_test_name test}") 65 | { :noreply, config } 66 | end 67 | 68 | def handle_cast({ :test_finished, ExUnit.Test[state: :passed] = test }, config) do 69 | if config.trace do 70 | IO.puts success("\r * #{trace_test_name test}") 71 | else 72 | IO.write success(".") 73 | end 74 | { :noreply, config.update_tests_counter(&(&1 + 1)) } 75 | end 76 | 77 | def handle_cast({ :test_finished, ExUnit.Test[state: { :invalid, _ }] = test }, config) do 78 | if config.trace do 79 | IO.puts invalid("\r * #{trace_test_name test}") 80 | else 81 | IO.write invalid("?") 82 | end 83 | { :noreply, config.update_tests_counter(&(&1 + 1)). 84 | update_invalid_counter(&(&1 + 1)) } 85 | end 86 | 87 | def handle_cast({ :test_finished, test }, config) do 88 | ExUnit.Test[case: _test_case, name: _test, state: { :failed, { _kind, reason, _stacktrace }}] = test 89 | exception_type = reason.__record__(:name) 90 | 91 | if exception_type == Elixir.Amrita.FactPending do 92 | if config.trace do 93 | IO.puts invalid("\r * #{trace_test_name test}") 94 | else 95 | IO.write invalid("P") 96 | end 97 | { :noreply, config.update_pending_counter(&(&1 + 1)). 98 | update_pending_failures(&([test|&1])) } 99 | else 100 | if config.trace do 101 | IO.puts failure("\r * #{trace_test_name test}") 102 | else 103 | IO.write failure("F") 104 | end 105 | { :noreply, config.update_tests_counter(&(&1 + 1)). 106 | update_test_failures(&([test|&1])) } 107 | end 108 | end 109 | 110 | def handle_cast({ :case_started, ExUnit.TestCase[name: name] }, config) do 111 | if config.trace, do: IO.puts("\n#{name}") 112 | { :noreply, config } 113 | end 114 | 115 | def handle_cast({ :case_finished, test_case }, config) do 116 | if test_case.state && test_case.state != :passed do 117 | { :noreply, config.update_case_failures(&([test_case|&1])) } 118 | else 119 | { :noreply, config } 120 | end 121 | end 122 | 123 | def handle_cast(request, config) do 124 | super(request, config) 125 | end 126 | 127 | defp trace_test_name(ExUnit.Test[name: name]) do 128 | case atom_to_binary(name) do 129 | "test_" <> rest -> rest 130 | "test " <> rest -> rest 131 | end 132 | end 133 | 134 | defp print_suite(counter, 0, num_pending, [], [], pending_failures, run_us, load_us) do 135 | IO.write "\n\nPending:\n\n" 136 | Enum.reduce Enum.reverse(pending_failures), 0, &print_test_pending(&1, &2) 137 | 138 | IO.puts format_time(run_us, load_us) 139 | IO.write success("#{counter} facts, ") 140 | if num_pending > 0 do 141 | IO.write success("#{num_pending} pending, ") 142 | end 143 | IO.write success "0 failures" 144 | IO.write "\n" 145 | end 146 | 147 | defp print_suite(counter, num_invalids, num_pending, test_failures, case_failures, pending_failures, run_us, load_us) do 148 | IO.write "\n\n" 149 | 150 | if num_pending > 0 do 151 | IO.write "Pending:\n\n" 152 | Enum.reduce Enum.reverse(pending_failures), 0, &print_test_pending(&1, &2) 153 | end 154 | 155 | IO.write "Failures:\n\n" 156 | num_fails = Enum.reduce Enum.reverse(test_failures), 0, &print_test_failure(&1, &2) 157 | Enum.reduce Enum.reverse(case_failures), num_fails, &print_test_case_failure(&1, &2) 158 | 159 | IO.puts format_time(run_us, load_us) 160 | message = "#{counter} facts" 161 | 162 | if num_invalids > 0 do 163 | message = message <> ", #{num_invalids} invalid" 164 | end 165 | if num_pending > 0 do 166 | message = message <> ", #{num_pending} pending" 167 | end 168 | 169 | message = message <> ", #{num_fails} failures" 170 | 171 | cond do 172 | num_fails > 0 -> IO.puts failure(message) 173 | num_invalids > 0 -> IO.puts invalid(message) 174 | true -> IO.puts success(message) 175 | end 176 | end 177 | 178 | defp print_test_failure(ExUnit.Test[name: name, case: mod, state: { :failed, tuple }], acc) do 179 | IO.puts format_test_failure(mod, name, tuple, acc + 1, :infinity, &formatter/2) 180 | acc + 1 181 | end 182 | 183 | defp print_test_case_failure(ExUnit.TestCase[name: name, state: { :failed, tuple }], acc) do 184 | IO.puts format_test_case_failure(name, tuple, acc + 1, :infinity, &formatter/2) 185 | acc + 1 186 | end 187 | 188 | # Color styles 189 | 190 | defp formatter(:error_info, msg), do: Amrita.Formatter.Format.colorize("red", msg) 191 | defp formatter(:location_info, msg), do: Amrita.Formatter.Format.colorize("cyan", msg) 192 | defp formatter(_, msg), do: msg 193 | 194 | defp print_test_pending(test, acc) do 195 | IO.puts Amrita.Formatter.Format.format_test_pending(test, acc + 1, &pending_formatter/2) 196 | acc + 1 197 | end 198 | 199 | defp success(msg) do 200 | Amrita.Formatter.Format.colorize("green", msg) 201 | end 202 | 203 | defp invalid(msg) do 204 | Amrita.Formatter.Format.colorize("yellow", msg) 205 | end 206 | 207 | defp failure(msg) do 208 | Amrita.Formatter.Format.colorize("red", msg) 209 | end 210 | 211 | # Color styles 212 | 213 | defp pending_formatter(:error_info, msg), do: Amrita.Formatter.Format.colorize("yellow", msg) 214 | defp pending_formatter(:location_info, msg), do: Amrita.Formatter.Format.colorize("cyan", msg) 215 | defp pending_formatter(_, msg), do: msg 216 | end 217 | -------------------------------------------------------------------------------- /lib/amrita/engine/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Engine.Runner do 2 | @moduledoc false 3 | 4 | defrecord Config, formatter: Amrita.Formatter.Progress, formatter_id: nil, 5 | max_cases: 4, taken_cases: 0, async_cases: [], sync_cases: [], selectors: [] 6 | 7 | def run(async, sync, opts, load_us) do 8 | opts = normalize_opts(opts) 9 | 10 | config = Config[max_cases: :erlang.system_info(:schedulers_online)] 11 | config = config.update(opts) 12 | 13 | { run_us, config } = 14 | :timer.tc fn -> 15 | loop config.async_cases(async).sync_cases(sync). 16 | formatter_id(config.formatter.suite_started(opts)) 17 | end 18 | 19 | config.formatter.suite_finished(config.formatter_id, run_us, load_us) 20 | end 21 | 22 | defp normalize_opts(opts) do 23 | if opts[:trace] do 24 | Keyword.put_new(opts, :max_cases, 1) 25 | else 26 | Keyword.put(opts, :trace, false) 27 | end 28 | end 29 | 30 | defp loop(Config[] = config) do 31 | available = config.max_cases - config.taken_cases 32 | 33 | cond do 34 | # No cases available, wait for one 35 | available <= 0 -> 36 | wait_until_available config 37 | 38 | # Slots are available, start with async cases 39 | tuple = take_async_cases(config, available) -> 40 | { config, cases } = tuple 41 | spawn_cases(config, cases) 42 | 43 | # No more async cases, wait for them to finish 44 | config.taken_cases > 0 -> 45 | wait_until_available config 46 | 47 | # So we can start all sync cases 48 | tuple = take_sync_cases(config) -> 49 | { config, cases } = tuple 50 | spawn_cases(config, cases) 51 | 52 | # No more cases, we are done! 53 | true -> 54 | config 55 | end 56 | end 57 | 58 | # Loop expecting messages from the spawned cases. Whenever 59 | # a test case has finished executing, decrease the taken 60 | # cases counter and attempt to spawn new ones. 61 | defp wait_until_available(config) do 62 | receive do 63 | { _pid, :case_finished, _test_case } -> 64 | loop config.update_taken_cases(&(&1-1)) 65 | end 66 | end 67 | 68 | defp spawn_cases(config, cases) do 69 | Enum.each cases, &spawn_case(config, &1) 70 | loop config.update_taken_cases(&(&1+length(cases))) 71 | end 72 | 73 | defp spawn_case(config, test_case) do 74 | pid = self() 75 | spawn_link fn -> 76 | run_test_case(config, pid, test_case) 77 | end 78 | end 79 | 80 | defp run_test_case(config, pid, case_name) do 81 | test_case = ExUnit.TestCase[name: case_name] 82 | config.formatter.case_started(config.formatter_id, test_case) 83 | 84 | self_pid = self 85 | { case_pid, case_ref } = Process.spawn_monitor fn -> 86 | { test_case, context } = try do 87 | { :ok, context } = case_name.__ex_unit__(:setup_all, [case: test_case]) 88 | { test_case, context } 89 | catch 90 | kind, error -> 91 | { test_case.state({ :failed, { kind, Exception.normalize(kind, error)}, filtered_stacktrace }), nil } 92 | end 93 | 94 | tests = tests_for(case_name, config) 95 | 96 | if test_case.state != nil do 97 | tests = Enum.map tests, fn test -> test.state({ :invalid, test_case }) end 98 | send(self_pid, { self, :case_finished, test_case, tests }) 99 | else 100 | Enum.each tests, &run_test(config, &1, context) 101 | 102 | test_case = try do 103 | case_name.__ex_unit__(:teardown_all, context) 104 | test_case 105 | catch 106 | kind, error -> 107 | test_case.state({ :failed, { kind, Exception.normalize(kind, error), filtered_stacktrace }}) 108 | end 109 | 110 | send(self_pid, { self, :case_finished, test_case, [] }) 111 | end 112 | end 113 | 114 | receive do 115 | { ^case_pid, :case_finished, test_case, tests } -> 116 | Enum.map tests, &config.formatter.test_finished(config.formatter_id, &1) 117 | config.formatter.case_finished(config.formatter_id, test_case) 118 | send(pid, { case_pid, :case_finished, test_case }) 119 | { :DOWN, ^case_ref, :process, ^case_pid, { error, stacktrace } } -> 120 | test_case = test_case.state({ :failed, { :EXIT, error, filter_stacktrace(stacktrace) }}) 121 | config.formatter.case_finished(config.formatter_id, test_case) 122 | send(pid, { case_pid, :case_finished, test_case }) 123 | end 124 | end 125 | 126 | defp run_test(config, test, context) do 127 | case_name = test.case 128 | config.formatter.test_started(config.formatter_id, test) 129 | 130 | # Run test in a new process so that we can trap exits for a single test 131 | self_pid = self 132 | { test_pid, test_ref } = Process.spawn_monitor fn -> 133 | { us, test } = :timer.tc(fn -> 134 | try do 135 | { :ok, context } = case_name.__ex_unit__(:setup, Keyword.put(context, :test, test)) 136 | 137 | test = try do 138 | apply case_name, test.name, [context] 139 | test.state(:passed) 140 | catch 141 | kind1, error1 -> 142 | test.state({ :failed, { kind1, Exception.normalize(kind1, error1), filtered_stacktrace }}) 143 | end 144 | 145 | case_name.__ex_unit__(:teardown, Keyword.put(context, :test, test)) 146 | test 147 | catch 148 | kind2, error2 -> 149 | test.state({ :failed, { kind2, Exception.normalize(kind2, error2), filtered_stacktrace }}) 150 | end 151 | end) 152 | 153 | send(self_pid, { self, :test_finished, test.time(us) }) 154 | end 155 | 156 | receive do 157 | { ^test_pid, :test_finished, test } -> 158 | config.formatter.test_finished(config.formatter_id, test) 159 | { :DOWN, ^test_ref, :process, ^test_pid, { error, stacktrace } } -> 160 | test = test.state({ :failed, { :EXIT, error, filter_stacktrace(stacktrace) }}) 161 | config.formatter.test_finished(config.formatter_id, test) 162 | end 163 | end 164 | 165 | ## Helpers 166 | 167 | defp take_async_cases(Config[] = config, count) do 168 | case config.async_cases do 169 | [] -> nil 170 | cases -> 171 | { response, remaining } = Enum.split(cases, count) 172 | { config.async_cases(remaining), response } 173 | end 174 | end 175 | 176 | defp take_sync_cases(Config[] = config) do 177 | case config.sync_cases do 178 | [h|t] -> { config.sync_cases(t), [h] } 179 | [] -> nil 180 | end 181 | end 182 | 183 | defp tests_for(case_name, config) do 184 | exports = case_name.__info__(:functions) 185 | 186 | lc { function, 1 } inlist exports, is_test?(atom_to_list(function)) && 187 | Amrita.Engine.TestPicker.run?(case_name, function, config.selectors) do 188 | ExUnit.Test[name: function, case: case_name] 189 | end 190 | end 191 | 192 | defp is_test?('test_' ++ _), do: true 193 | defp is_test?('test ' ++ _), do: true 194 | defp is_test?(_) , do: false 195 | 196 | defp filtered_stacktrace, do: filter_stacktrace(System.stacktrace) 197 | 198 | # Assertions can pop-up in the middle of the stack 199 | defp filter_stacktrace([{ ExUnit.Assertions, _, _, _ }|t]), do: filter_stacktrace(t) 200 | 201 | # As soon as we see a Runner, it is time to ignore the stacktrace 202 | defp filter_stacktrace([{ Amrita.Engine.Runner, _, _, _ }|_]), do: [] 203 | 204 | # All other cases 205 | defp filter_stacktrace([h|t]), do: [h|filter_stacktrace(t)] 206 | defp filter_stacktrace([]), do: [] 207 | end 208 | -------------------------------------------------------------------------------- /test/integration/t_mocks.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule Integration.MockFacts do 4 | use Amrita.Sweet 5 | 6 | import Support 7 | 8 | defmodule Polite do 9 | def swear? do 10 | false 11 | end 12 | 13 | def message do 14 | "oh swizzlesticks" 15 | end 16 | end 17 | 18 | fact "check unstubbed module was preserved after stub" do 19 | Polite.swear? |> falsey 20 | Polite.message |> "oh swizzlesticks" 21 | end 22 | 23 | fact "simple mock on existing module" do 24 | provided [Integration.MockFacts.Polite.swear? |> true] do 25 | Polite.swear? |> truthy 26 | end 27 | end 28 | 29 | failing_fact "provided when not called raises a fail" do 30 | provided [Integration.MockFacts.Polite.swear? |> true] do 31 | Polite.message |> "oh swizzlesticks" 32 | end 33 | end 34 | 35 | fact "check again that unstubbed module was preserved after stub" do 36 | Polite.swear? |> falsey 37 | Polite.message |> "oh swizzlesticks" 38 | end 39 | 40 | fact "multi mocks on same module" do 41 | provided [Integration.MockFacts.Polite.swear? |> true, 42 | Integration.MockFacts.Polite.message |> "funk"] do 43 | Polite.swear? |> truthy 44 | Polite.message |> "funk" 45 | end 46 | end 47 | 48 | defmodule Rude do 49 | def swear? do 50 | true 51 | end 52 | end 53 | 54 | fact "multi mocks on different modules" do 55 | provided [Integration.MockFacts.Polite.swear? |> true, 56 | Integration.MockFacts.Rude.swear? |> false] do 57 | Polite.swear? |> truthy 58 | Rude.swear? |> falsey 59 | end 60 | end 61 | 62 | facts "about mocks with fn matcher arguments" do 63 | 64 | fact "fn matches against a regexp" do 65 | provided [Flip.flop(fn x -> x =~ ~r"moo" end) |> true] do 66 | Flip.flop("this is a mooo thing") |> true 67 | end 68 | 69 | fail do 70 | provided [Flip.flop(fn x -> x =~ ~r"moo" end) |> true] do 71 | Flip.flop("this is a doo thing") |> true 72 | Flip.flop("this is a zoo thing") |> true 73 | end 74 | end 75 | end 76 | 77 | end 78 | 79 | facts "about mocks with non checker arguments" do 80 | 81 | defmodule Funk do 82 | def hip?(_arg) do 83 | true 84 | end 85 | end 86 | 87 | fact "mock with a single argument" do 88 | provided [Integration.MockFacts.Funk.hip?(:yes) |> false] do 89 | Funk.hip?(:yes) |> falsey 90 | end 91 | end 92 | 93 | facts "mock with elixir types" do 94 | fact "regex" do 95 | provided [Integration.MockFacts.Funk.hip?(~r"monkey") |> false] do 96 | Funk.hip?(~r"monkey") |> falsey 97 | end 98 | 99 | fail do 100 | provided [Integration.MockFacts.Funk.hip?(~r"monkey") |> false] do 101 | Funk.hip?(~r"mon") |> falsey 102 | end 103 | end 104 | end 105 | 106 | fact "list" do 107 | provided [Integration.MockFacts.Funk.hip?([1, 2, 3]) |> false] do 108 | Funk.hip?([1, 2, 3]) |> falsey 109 | end 110 | 111 | fail do 112 | provided [Integration.MockFacts.Funk.hip?([1, 2, 3]) |> false] do 113 | Funk.hip?([1, 2, 3, 4]) |> falsey 114 | end 115 | end 116 | end 117 | 118 | fact "tuple" do 119 | provided [Integration.MockFacts.Funk.hip?({1, 2, 3}) |> false] do 120 | Funk.hip?({1, 2, 3}) |> falsey 121 | end 122 | 123 | fail do 124 | provided [Integration.MockFacts.Funk.hip?({1, 2, 3}) |> false] do 125 | Funk.hip?({1, 2}) |> falsey 126 | end 127 | end 128 | end 129 | 130 | fact "dict" do 131 | provided [Integration.MockFacts.Funk.hip?(Enum.into([{:a, 1}], [])) |> false] do 132 | Funk.hip?(Enum.into([{:a, 1}], [])) |> falsey 133 | end 134 | 135 | fail do 136 | provided [Integration.MockFacts.Funk.hip?(Enum.into([{:a, 1}], [])) |> false] do 137 | Funk.hip?(Enum.into([{:a, 2}], [])) |> falsey 138 | end 139 | end 140 | end 141 | 142 | fact "range" do 143 | provided [Integration.MockFacts.Funk.hip?(1..10) |> false] do 144 | Funk.hip?(1..10) |> falsey 145 | end 146 | 147 | fail do 148 | provided [Integration.MockFacts.Funk.hip?(1..10) |> false] do 149 | Funk.hip?(1..11) |> falsey 150 | end 151 | end 152 | end 153 | end 154 | 155 | failing_fact "mock with an argument that does not match fails" do 156 | provided [Integration.MockFacts.Funk.hip?(:yes) |> false] do 157 | Funk.hip?(:no) |> falsey 158 | end 159 | end 160 | 161 | fact "mock with a wildcard" do 162 | provided [Integration.MockFacts.Funk.hip?(:_) |> false] do 163 | Funk.hip?(:yes) |> falsey 164 | Funk.hip?(:whatever) |> falsey 165 | end 166 | end 167 | 168 | fact "mock with a _ wildcard" do 169 | provided [Integration.MockFacts.Funk.hip?(_) |> false] do 170 | Funk.hip?(:yes) |> falsey 171 | Funk.hip?(:whatever) |> falsey 172 | end 173 | end 174 | 175 | fact "mock anything wildcard" do 176 | provided [Integration.MockFacts.Funk.hip?(anything, anything, anything) |> false] do 177 | Funk.hip?(:yes, :no, :maybe) |> falsey 178 | end 179 | end 180 | 181 | failing_fact "failing anything wildcard" do 182 | provided [Integration.MockFacts.Funk.hip?(anything, anything, anything) |> false] do 183 | Funk.hip?(:yes, :no, :maybe, :funk) |> falsey 184 | end 185 | end 186 | 187 | def tiplet do 188 | "brandy" 189 | end 190 | 191 | fact "mock with a function defined inside a test" do 192 | provided [Integration.MockFacts.Funk.hip?(tiplet) |> false] do 193 | Funk.hip?("brandy") |> falsey 194 | end 195 | end 196 | 197 | def tiplet(count) do 198 | "brandy#{count}" 199 | end 200 | 201 | fact "mock with a function with args defined inside a test" do 202 | provided [Integration.MockFacts.Funk.hip?(tiplet(1)) |> true] do 203 | Funk.hip?("brandy1") |> truthy 204 | end 205 | 206 | provided [Integration.MockFacts.Funk.hip?(Integration.MockFacts.tiplet(1)) |> true] do 207 | Funk.hip?("brandy1") |> truthy 208 | end 209 | end 210 | 211 | fact "mock with many arguments" do 212 | provided [Integration.MockFacts.Funk.flop?(:yes, :no, :yes) |> false] do 213 | Funk.flop?(:yes, :no, :yes) |> falsey 214 | end 215 | end 216 | 217 | failing_fact "mock with a mismatch in arity of arguments fails" do 218 | provided [Integration.MockFacts.Funk.hip?(:yes) |> false] do 219 | Funk.hip?(:yes, :no) |> falsey 220 | end 221 | end 222 | 223 | fact "mock with > 6 arguments" do 224 | provided [Integration.MockFacts.Funk.flop?(:a, :b, :c, :d, :e, :f, :g, :h) |> false] do 225 | Funk.flop?(:a, :b, :c, :d, :e, :f, :g, :h) |> falsey 226 | end 227 | end 228 | 229 | fact "mock the same function based on different arguments" do 230 | provided [Integration.MockFacts.Funk.hip?(:cats) |> false, Integration.MockFacts.Funk.hip?(:coffee) |> true] do 231 | Integration.MockFacts.Funk.hip?(:cats) |> falsey 232 | Integration.MockFacts.Funk.hip?(:coffee) |> truthy 233 | end 234 | end 235 | 236 | end 237 | 238 | fact "mock with a return value as a function" do 239 | provided [Integration.MockFacts.Funk.hip?(_) |> tiplet(2)] do 240 | Funk.hip?("brandy") |> "brandy2" 241 | end 242 | 243 | provided [Integration.MockFacts.Funk.hip?(_) |> tiplet] do 244 | Funk.hip?("shandy") |> "brandy" 245 | end 246 | 247 | provided [Integration.MockFacts.Funk.hip?(_) |> Integration.MockFacts.tiplet] do 248 | Funk.hip?("shandy") |> "brandy" 249 | end 250 | end 251 | 252 | fact "mock with a return value as a local var" do 253 | x = 10 254 | provided [Integration.MockFacts.Funk.hip?(_) |> x] do 255 | Funk.hip?("shandy") |> 10 256 | end 257 | end 258 | 259 | fact "mock with alternative syntax", provided: [Flip.flop(:ok) |> true] do 260 | Flip.flop(:ok) |> truthy 261 | end 262 | 263 | end 264 | -------------------------------------------------------------------------------- /test/integration/t_amrita.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "../../test_helper.exs", __ENV__.file 2 | 3 | defmodule Integration.AmritaFacts do 4 | use Amrita.Sweet 5 | 6 | import Support 7 | 8 | #Testing a single fact 9 | fact "addition" do 10 | assert 1 + 1 == 2 11 | end 12 | 13 | #Testing a fact group 14 | facts "about subtraction" do 15 | fact "postive numbers" do 16 | assert 2 - 2 == 0 17 | end 18 | 19 | fact "negative numbers" do 20 | assert -2 - -2 == 0 21 | end 22 | end 23 | 24 | #Testing multiple depths of facts 25 | facts "more about subtraction" do 26 | facts "zero results" do 27 | fact "postive numbers" do 28 | assert 2 - 2 == 0 29 | end 30 | fact "negative numbers" do 31 | assert -2 - -2 == 0 32 | end 33 | end 34 | end 35 | 36 | #Matchers 37 | facts "about simple matchers" do 38 | fact "|> defaults to equality when given ints or strings" do 39 | 10 |> 10 40 | "hello" |> "hello" 41 | [1,2,3,4] |> [1,2,3,4] 42 | true |> true 43 | false |> false 44 | 45 | fail do 46 | false |> true 47 | end 48 | end 49 | 50 | fact "|> defaults to equality when given an atom" do 51 | :hello |> :hello 52 | 53 | fail do 54 | :hello |> :bye 55 | end 56 | end 57 | 58 | fact "about odd" do 59 | 1 |> odd 60 | 61 | fail do 62 | 2 |> odd 63 | end 64 | end 65 | 66 | fact "about even" do 67 | 2 |> even 68 | 69 | fail do 70 | 1 |> even 71 | end 72 | end 73 | 74 | fact "truthy" do 75 | true |> truthy 76 | [] |> truthy 77 | "" |> truthy 78 | 79 | fail do 80 | false |> truthy 81 | end 82 | end 83 | 84 | fact "falsey" do 85 | false |> falsey 86 | nil |> falsey 87 | 88 | fail do 89 | true |> falsey 90 | end 91 | end 92 | 93 | fact "roughly" do 94 | 0.1001 |> roughly 0.1 95 | 96 | 0.1 |> roughly 0.2, 0.2 97 | 98 | 1 |> roughly 2, 2 99 | 100 | fail do 101 | 0.1 |> equals 0.2 102 | end 103 | end 104 | 105 | fact "equals" do 106 | 999 |> equals 999 107 | 108 | fail do 109 | 999 |> equals 998 110 | end 111 | end 112 | 113 | fact "msg" do 114 | fn -> :hello end |> msg(:hello) 115 | 116 | fail do 117 | fn -> :sod end |> msg(:hello) 118 | end 119 | end 120 | 121 | facts "equals with wild cards" do 122 | 123 | fact "tuples use matches when used with equals" do 124 | { 1, 2, 3 } |> equals { 1, _, 3 } 125 | # { 1, 2 } is actually a different code path than { 1, 2, 3 } 126 | { 1, 2 } |> equals { 1, _ } 127 | { 1, 2, { 1, 2 } } |> equals { 1, _, { 1, _ } } 128 | 129 | fail do 130 | { 1, 2, 3 } |> { 2, _, _ } 131 | 132 | { 1, 2, { 1, 2 } } |> equals { 1, _, { 1, 4 } } 133 | end 134 | end 135 | 136 | fact "tuples use equals matcher implicitly" do 137 | { 1, 2, 3 } |> { 1, _, 3 } 138 | { 1, 2 } |> { 1, _ } 139 | end 140 | 141 | fact "lists use matches when used with equals" do 142 | [ 3, 2, 1 ] |> equals [ 3, _, 1 ] 143 | 144 | fail do 145 | [ 3, 2, 1 ] |> equals [ 3, _, 2 ] 146 | end 147 | end 148 | 149 | end 150 | end 151 | 152 | facts "about collection matchers" do 153 | fact "contains" do 154 | [1, 2, 3] |> contains 3 155 | 156 | {4, 5, 6} |> contains 5 157 | 158 | [a: 1, b: 2] |> contains({:a, 1}) 159 | 160 | "mad hatter tea party" |> contains "hatter" 161 | 162 | "mad hatter tea party" |> contains ~r"h(\w+)er" 163 | 164 | fail do 165 | [1, 2, 3] |> contains 4 166 | "mad" |> contains "hatter" 167 | end 168 | end 169 | 170 | fact "has_prefix" do 171 | [1, 2, 3] |> has_prefix [1, 2] 172 | 173 | {4, 5, 6} |> has_prefix {4, 5} 174 | 175 | "mad hatter tea party" |> has_prefix "mad" 176 | 177 | fail do 178 | [1, 2, 3] |> has_prefix [2, 1] 179 | "mad" |> has_prefix "hatter" 180 | end 181 | end 182 | 183 | fact "has_prefix with Sets" do 184 | [1, 2, 3] |> has_prefix Enum.into([2,1], HashSet.new) 185 | end 186 | 187 | fact "has_suffix" do 188 | [1, 2, 3, 4, 5] |> has_suffix [3, 4, 5] 189 | 190 | {1, 2, 3, 4, 5} |> has_suffix {3, 4, 5} 191 | 192 | "white rabbit" |> has_suffix "rabbit" 193 | 194 | fail do 195 | [1, 2, 3, 4, 5] |> has_suffix [4, 3, 5] 196 | "mad" |> has_suffix "hatter" 197 | end 198 | end 199 | 200 | fact "hash suffix with Sets" do 201 | [1, 2, 3] |> has_suffix Enum.into([3,2],HashSet.new) 202 | end 203 | 204 | fact "for_all" do 205 | [2, 4, 6, 8] |> for_all(fn(x) -> even(x) end) 206 | 207 | [2, 4, 6, 8] |> Enum.all?(fn(x) -> even(x) end) 208 | 209 | fail do 210 | [2, 4, 7, 8] |> for_all(fn(x) -> even(x) end) 211 | end 212 | end 213 | 214 | fact "for_some" do 215 | [2, 4, 7, 8] |> for_some(fn(x) -> odd(x) end) 216 | 217 | fail do 218 | [1, 3, 5, 7] |> for_some(fn(x) -> even(x) end) 219 | end 220 | end 221 | 222 | fact "without a body is considered pending" 223 | 224 | end 225 | 226 | facts "message checkers" do 227 | future_fact "receive" do 228 | receive |> :hello 229 | send(self, :hello) 230 | end 231 | 232 | fact "received" do 233 | send(self, :hello) 234 | received |> :hello 235 | 236 | fail "wrong match" do 237 | send(self, :sod) 238 | received |> :hello 239 | end 240 | 241 | fail "never received message" do 242 | received |> :hello 243 | end 244 | end 245 | 246 | fact "received tuples" do 247 | send(self, { :hello, 1, 2 }) 248 | received |> { :hello, _, 2 } 249 | end 250 | end 251 | 252 | defexception TestException, message: "golly gosh, sorry" 253 | 254 | facts "exceptions" do 255 | fact "should allow checking of exceptions" do 256 | fn -> raise TestException end |> raises Integration.AmritaFacts.TestException 257 | 258 | fail do 259 | fn -> true end |> raises Integration.AmritaFacts.TestException 260 | end 261 | end 262 | 263 | fact "should allow checking of exceptions by message" do 264 | fn -> raise TestException end |> raises ~r".*gosh.*" 265 | 266 | fn -> raise TestException end |> raises "golly gosh, sorry" 267 | 268 | fail do 269 | fn -> raise TestException end |> raises ~r"pants" 270 | end 271 | end 272 | end 273 | 274 | facts "! negates the predicate" do 275 | fact "contains" do 276 | [1, 2, 3, 4] |> ! contains 9999 277 | 278 | fail do 279 | [1, 2, 3, 4] |> ! contains 1 280 | end 281 | end 282 | 283 | fact "equals" do 284 | 1999 |> ! equals 0 285 | 286 | fail do 287 | 199 |> ! 199 288 | end 289 | end 290 | 291 | fact "roughly" do 292 | 0.1001 |> ! roughly 0.2 293 | 294 | fail do 295 | 0.1001 |> ! roughly 0.1 296 | end 297 | end 298 | 299 | fact "has_suffix" do 300 | [1, 2, 3, 4] |> ! has_suffix [3,1] 301 | 302 | fail do 303 | [1, 2, 3, 4] |> ! has_suffix [3,4] 304 | end 305 | end 306 | 307 | fact "has_prefix" do 308 | [1, 2, 3, 4] |> ! has_prefix [1, 3] 309 | 310 | fail do 311 | [1, 2, 3, 4] |> ! has_prefix [1, 2] 312 | end 313 | end 314 | 315 | fact "raises" do 316 | fn -> raise TestException end |> ! raises AmritaFacts.MadeUpException 317 | 318 | fn -> raise TestException end |> ! raises ~r".*posh.*" 319 | 320 | fail do 321 | fn -> raise TestException end |> ! raises TestException 322 | end 323 | end 324 | 325 | fact "|> defaulting to not(equality)" do 326 | 1 |> ! 2 327 | 328 | fail do 329 | 1 |> ! 1 330 | end 331 | end 332 | 333 | fact "falsey" do 334 | true |> ! falsey 335 | 336 | fail do 337 | false |> ! falsey 338 | end 339 | end 340 | 341 | fact "truthy" do 342 | false |> ! truthy 343 | 344 | fail do 345 | true |> ! truthy 346 | end 347 | end 348 | end 349 | 350 | test "Backwards compatible with ExUnit" do 351 | assert 2 + 2 == 4 352 | end 353 | 354 | facts :atoms_are_ok do 355 | fact :atoms_still_ok do 356 | 1 |> 1 357 | end 358 | end 359 | end 360 | -------------------------------------------------------------------------------- /lib/amrita/mocks.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Mocks do 2 | @moduledoc """ 3 | Add support for prerequisites or mocks to tests. 4 | Automatically imported with `use Amrita.Sweet` 5 | 6 | ## Example: 7 | use Amrita.Mocks 8 | 9 | """ 10 | 11 | defmacro __using__(_ \\ []) do 12 | quote do 13 | import Amrita.Mocks.Provided 14 | end 15 | end 16 | 17 | defmodule Provided do 18 | @moduledoc """ 19 | The Mocking DSL. 20 | """ 21 | 22 | defrecord Error, module: nil, fun: nil, args: nil, raw_args: nil, history: [] 23 | 24 | @doc """ 25 | Adds prerequisites to a test. 26 | 27 | ## Example 28 | defmodule Polite do 29 | def swear?(word) do 30 | word == "bugger" 31 | end 32 | end 33 | 34 | provided [Polite.swear?("bugger") |> false] do 35 | Polite.swear?("bugger") |> falsey 36 | end 37 | 38 | #With a wildcard argument matcher 39 | provided [Polite.swear?(anything) |> false] do 40 | Polite.swear?("bugger") |> falsey 41 | Polite.swear?("pants") |> falsey 42 | end 43 | 44 | """ 45 | defmacro provided(forms, test) do 46 | prerequisites = Amrita.Mocks.Provided.Parse.prerequisites(forms) 47 | mock_modules = Provided.Prerequisites.all_modules(prerequisites) 48 | prerequisite_list = Macro.escape(Provided.Prerequisites.to_list(prerequisites)) 49 | quote do 50 | prerequisites = Provided.Prerequisites.to_prerequisites(unquote(prerequisite_list)) 51 | local_bindings = binding() 52 | 53 | :meck.new(unquote(mock_modules), [:passthrough, :non_strict]) 54 | 55 | prerequisites = unquote(__MODULE__).__resolve_args__(prerequisites, __MODULE__, __ENV__, local_bindings) 56 | 57 | Provided.Prerequisites.each_mock_list prerequisites, fn mocks -> 58 | unquote(__MODULE__).__add_expect__(mocks, __MODULE__, __ENV__, local_bindings) 59 | end 60 | 61 | try do 62 | unquote(test) 63 | 64 | Enum.map unquote(mock_modules), fn mock_module -> 65 | :meck.validate(mock_module) |> truthy 66 | end 67 | 68 | after 69 | fails = Provided.Check.fails(prerequisites) 70 | :meck.unload(unquote(mock_modules)) 71 | 72 | if not(Enum.empty?(fails)) do 73 | Amrita.Message.mock_fail(fails) 74 | end 75 | 76 | end 77 | end 78 | end 79 | 80 | def __resolve_args__(prerequisites, target_module, env, local_bindings \\ []) do 81 | Provided.Prerequisites.map prerequisites, fn { module, fun, args, _, value } -> 82 | new_args = Enum.map args, fn arg -> __resolve_arg__(arg, target_module, env, local_bindings) end 83 | { module, fun, new_args, args, value } 84 | end 85 | end 86 | 87 | def __resolve_arg__(arg, target_module, env, local_bindings \\ []) do 88 | case arg do 89 | { :_, _, _ } -> anything 90 | { :fn, _meta, args } -> { evaled_arg, _ } = Code.eval_quoted(arg, [], env) 91 | :meck.is(evaled_arg) 92 | { name, _meta, args } -> 93 | args = args || [] 94 | if __in_scope__(name, args, target_module) do 95 | apply(target_module, name, args) 96 | else 97 | if is_atom(name) && Keyword.has_key?(local_bindings, name) do 98 | local_bindings[name] 99 | else 100 | { evaled_arg, _ } = Code.eval_quoted(arg, [], env) 101 | evaled_arg 102 | end 103 | end 104 | _ -> arg 105 | end 106 | end 107 | 108 | def __in_scope__(name, args, target_module) do 109 | Enum.any? target_module.__info__(:functions), 110 | fn { method, arity } -> method == name && arity == Enum.count(args) end 111 | end 112 | 113 | 114 | def __add_expect__(mocks, target_module, env, local_vars) do 115 | args_specs = Enum.map mocks, fn { _, _, args, _, value } -> 116 | value = __resolve_arg__(value, target_module, env, local_vars) 117 | { args, value } 118 | end 119 | { mock_module, fn_name, _, _, _ } = Enum.at(mocks, 0) 120 | 121 | :meck.expect(mock_module, fn_name, args_specs) 122 | end 123 | 124 | @doc """ 125 | alias for :_ the wild card checker for arguments 126 | """ 127 | def anything do 128 | :_ 129 | end 130 | end 131 | 132 | defmodule Provided.Check do 133 | @moduledoc false 134 | 135 | def fails(prerequisites) do 136 | Provided.Prerequisites.reduce prerequisites, [], fn mock -> called?(mock) end 137 | end 138 | 139 | defp called?({module, fun, args, raw_args, _}) do 140 | case :meck.called(module, fun, args) do 141 | false -> [Provided.Error.new(module: module, 142 | fun: fun, 143 | args: args, 144 | raw_args: raw_args, 145 | history: Amrita.Mocks.History.matches(module, fun))] 146 | _ -> [] 147 | end 148 | end 149 | end 150 | 151 | defmodule Provided.Prerequisites do 152 | @moduledoc false 153 | 154 | defrecordp :prereqs, bucket: [HashDict.new(HashDict.new)] 155 | 156 | def new(prerequisites) do 157 | bucket = Enum.reduce prerequisites, HashDict.new, fn {module, fun, args, value}, acc -> 158 | mocks_by_module = HashDict.get(acc, module, HashDict.new) 159 | mocks_by_fun = HashDict.get(mocks_by_module, fun, []) 160 | mocks = Enum.concat(mocks_by_fun, [{module, fun, args, nil ,value}]) 161 | 162 | Dict.put(acc, module, Dict.put(mocks_by_module, fun, mocks)) 163 | end 164 | 165 | prereqs(bucket: bucket) 166 | end 167 | 168 | def all_modules(prereqs(bucket: bucket)) do 169 | Dict.keys(bucket) 170 | end 171 | 172 | def each_mock_list(prereqs(bucket: bucket), fun) do 173 | Enum.each bucket, fn { _, mocks_by_module } -> 174 | Enum.each mocks_by_module, fn { _, mocks } -> 175 | fun.(mocks) 176 | end 177 | end 178 | end 179 | 180 | def map(prereqs(bucket: bucket), fun) do 181 | new_bucket = Enum.map bucket, fn { module_key, mocks_by_module } -> 182 | new_mocks_by_module = Enum.map mocks_by_module, fn {fun_key, mocks} -> 183 | new_mocks_by_fun = Enum.map mocks, fun 184 | { fun_key, new_mocks_by_fun } 185 | end 186 | { module_key, new_mocks_by_module } 187 | end 188 | prereqs(bucket: new_bucket) 189 | end 190 | 191 | def reduce(prereqs(bucket: bucket), start, fun) do 192 | Enum.reduce bucket, start, fn { _, mocks_by_module }, all_acc -> 193 | results = Enum.reduce mocks_by_module, [], fn { _, mocks }, fn_acc -> 194 | results = Enum.reduce mocks, [], fn mock, acc -> 195 | result = fun.(mock) 196 | Enum.concat(acc, result) 197 | end 198 | Enum.concat(fn_acc, results) 199 | end 200 | Enum.concat(all_acc, results) 201 | end 202 | end 203 | 204 | def to_list(prereqs(bucket: bucket)) do 205 | Dict.to_list(bucket) 206 | end 207 | 208 | def to_prerequisites(list) do 209 | prereqs(bucket: list) 210 | end 211 | 212 | def by_module_and_fun(prereqs(bucket: bucket), module, fun) do 213 | mocks = by_fun(by_module(bucket, module), fun) 214 | Enum.map mocks, fn {m,f,a,_,v} -> {m,f,a,v} end 215 | end 216 | 217 | defp by_fun(bucket, fun) do 218 | Dict.get(bucket, fun) 219 | end 220 | 221 | defp by_module(bucket, module) do 222 | Dict.get(bucket, module) 223 | end 224 | 225 | end 226 | 227 | defmodule Provided.Parse do 228 | @moduledoc false 229 | 230 | defexception Error, form: [] do 231 | def message(exception) do 232 | "Amrita could not understand your `provided`:\n" <> 233 | " " <> Macro.to_string(exception.form) <> "\n" <> 234 | " Make sure it uses this format: [Module.fun |> :return_value]" 235 | end 236 | end 237 | 238 | def prerequisites(forms) do 239 | prerequisites = Enum.map(forms, &extract(&1)) 240 | Provided.Prerequisites.new(prerequisites) 241 | end 242 | 243 | defp extract({:|>, _, [{fun, _, args}, value]}) do 244 | { module_name, function_name } = extract(fun) 245 | { module_name, function_name, args, value } 246 | end 247 | 248 | defp extract({:., _, [ns, method_name]}) do 249 | { extract(ns), method_name } 250 | end 251 | 252 | defp extract({:__aliases__, _, ns}) do 253 | Module.concat ns 254 | end 255 | 256 | defp extract(form) do 257 | raise Error.new(form: form) 258 | end 259 | 260 | end 261 | 262 | end 263 | -------------------------------------------------------------------------------- /lib/amrita/formatter/documentation.ex: -------------------------------------------------------------------------------- 1 | defmodule Amrita.Formatter.Documentation do 2 | @moduledoc """ 3 | Provides a documentation focused formatter. Outputting the full test names indenting based on the fact groups. 4 | """ 5 | 6 | @behaviour ExUnit.Formatter 7 | @timeout 30_000 8 | use GenServer.Behaviour 9 | 10 | import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 6, format_test_case_failure: 5] 11 | 12 | defrecord Config, tests_counter: 0, invalid_counter: 0, pending_counter: 0, scope: HashDict.new, 13 | test_failures: [], case_failures: [], pending_failures: [], trace: false 14 | 15 | ## Behaviour 16 | 17 | def suite_started(opts) do 18 | { :ok, pid } = :gen_server.start_link(__MODULE__, opts[:trace], []) 19 | pid 20 | end 21 | 22 | def suite_finished(id, run_us, load_us) do 23 | :gen_server.call(id, { :suite_finished, run_us, load_us }, @timeout) 24 | end 25 | 26 | def case_started(id, test_case) do 27 | :gen_server.cast(id, { :case_started, test_case }) 28 | end 29 | 30 | def case_finished(id, test_case) do 31 | :gen_server.cast(id, { :case_finished, test_case }) 32 | end 33 | 34 | def test_started(id, test) do 35 | :gen_server.cast(id, { :test_started, test }) 36 | end 37 | 38 | def test_finished(id, test) do 39 | :gen_server.cast(id, { :test_finished, test }) 40 | end 41 | 42 | ## Callbacks 43 | 44 | def init(trace) do 45 | { :ok, Config[trace: trace] } 46 | end 47 | 48 | def handle_call({ :suite_finished, run_us, load_us }, _from, config) do 49 | print_suite(config.tests_counter, config.invalid_counter, config.pending_counter, 50 | config.test_failures, config.case_failures, config.pending_failures, run_us, load_us) 51 | { :stop, :normal, length(config.test_failures), config } 52 | end 53 | 54 | def handle_call(reqest, from, config) do 55 | super(reqest, from, config) 56 | end 57 | 58 | def handle_cast({ :test_started, ExUnit.Test[] = test }, config) do 59 | if(name_parts = scoped(test)) do 60 | if(scope = new_scope(config, name_parts)) do 61 | print_scopes(name_parts) 62 | config = config.update_scope fn s -> HashDict.put(s, scope, []) end 63 | end 64 | end 65 | 66 | { :noreply, config } 67 | end 68 | 69 | def handle_cast({ :test_finished, ExUnit.Test[state: :passed] = test }, config) do 70 | if(name_parts = scoped(test)) do 71 | print_indent(name_parts) 72 | IO.write success(String.lstrip "#{Enum.at(name_parts, Enum.count(name_parts)-1)}#{trace_test_time(test, config)}\n") 73 | 74 | { :noreply, config.update_tests_counter(&(&1 + 1)) } 75 | else 76 | IO.puts success("\r #{format_test_name test}#{trace_test_time(test, config)}") 77 | { :noreply, config.update_tests_counter(&(&1 + 1)) } 78 | end 79 | end 80 | 81 | def handle_cast({ :test_finished, ExUnit.Test[state: { :invalid, _ }] = test }, config) do 82 | IO.puts invalid("\r #{format_test_name test}") 83 | { :noreply, config.update_tests_counter(&(&1 + 1)).update_invalid_counter(&(&1 + 1)) } 84 | end 85 | 86 | def handle_cast({ :test_finished, test }, config) do 87 | ExUnit.Test[case: _test_case, name: _test, state: { :failed, { _kind, reason, _stacktrace }}] = test 88 | exception_type = reason.__record__(:name) 89 | 90 | name_parts = scoped(test) 91 | if(name_parts) do 92 | print_indent(name_parts) 93 | end 94 | 95 | if exception_type == Elixir.Amrita.FactPending do 96 | if(name_parts) do 97 | IO.write pending(String.lstrip "#{Enum.at(name_parts, Enum.count(name_parts)-1)}\n") 98 | else 99 | IO.puts pending(" #{format_test_name test}") 100 | end 101 | { :noreply, config.update_pending_counter(&(&1 + 1)). 102 | update_pending_failures(&([test|&1])) } 103 | else 104 | if(name_parts) do 105 | IO.write failure(String.lstrip "#{Enum.at(name_parts, Enum.count(name_parts)-1)}#{trace_test_time(test, config)}\n") 106 | else 107 | IO.puts failure(" #{format_test_name test}#{trace_test_time(test, config)}") 108 | end 109 | { :noreply, config.update_tests_counter(&(&1 + 1)).update_test_failures(&([test|&1])) } 110 | end 111 | end 112 | 113 | def handle_cast({ :case_started, ExUnit.TestCase[name: name] }, config) do 114 | IO.puts("\n#{name}") 115 | { :noreply, config } 116 | end 117 | 118 | def handle_cast({ :case_finished, test_case }, config) do 119 | if test_case.state && test_case.state != :passed do 120 | { :noreply, config.update_case_failures(&([test_case|&1])) } 121 | else 122 | { :noreply, config } 123 | end 124 | end 125 | 126 | def handle_cast(request, config) do 127 | super(request, config) 128 | end 129 | 130 | defp format_test_name(ExUnit.Test[] = test) do 131 | Amrita.Formatter.Format.format_test_name(test) 132 | end 133 | 134 | defp print_suite(counter, 0, num_pending, [], [], pending_failures, run_us, load_us) do 135 | IO.write "\n\nPending:\n\n" 136 | Enum.reduce Enum.reverse(pending_failures), 0, &print_test_pending(&1, &2) 137 | 138 | IO.puts format_time(run_us, load_us) 139 | IO.write success("#{counter} facts, ") 140 | if num_pending > 0 do 141 | IO.write success("#{num_pending} pending, ") 142 | end 143 | IO.write success "0 failures" 144 | IO.write "\n" 145 | end 146 | 147 | defp print_suite(counter, num_invalids, num_pending, test_failures, case_failures, pending_failures, run_us, load_us) do 148 | IO.write "\n\n" 149 | 150 | if num_pending > 0 do 151 | IO.write "Pending:\n\n" 152 | Enum.reduce Enum.reverse(pending_failures), 0, &print_test_pending(&1, &2) 153 | end 154 | 155 | IO.write "Failures:\n\n" 156 | num_fails = Enum.reduce Enum.reverse(test_failures), 0, &print_test_failure(&1, &2) 157 | Enum.reduce Enum.reverse(case_failures), num_fails, &print_test_case_failure(&1, &2) 158 | 159 | IO.puts format_time(run_us, load_us) 160 | message = "#{counter} facts" 161 | 162 | if num_invalids > 0 do 163 | message = message <> ", #{num_invalids} invalid" 164 | end 165 | if num_pending > 0 do 166 | message = message <> ", #{num_pending} pending" 167 | end 168 | 169 | message = message <> ", #{num_fails} failures" 170 | 171 | cond do 172 | num_fails > 0 -> IO.puts failure(message) 173 | num_invalids > 0 -> IO.puts invalid(message) 174 | true -> IO.puts success(message) 175 | end 176 | end 177 | 178 | defp print_test_pending(test, acc) do 179 | IO.puts Amrita.Formatter.Format.format_test_pending(test, acc + 1, &pending_formatter/2) 180 | acc + 1 181 | end 182 | 183 | defp print_test_failure(ExUnit.Test[name: name, case: mod, state: { :failed, tuple }], acc) do 184 | IO.puts format_test_failure(mod, name, tuple, acc + 1, :infinity, &formatter/2) 185 | acc + 1 186 | end 187 | 188 | defp print_test_case_failure(ExUnit.TestCase[name: name, state: { :failed, tuple }], acc) do 189 | IO.puts format_test_case_failure(name, tuple, acc + 1, :infinity, &formatter/2) 190 | acc + 1 191 | end 192 | 193 | defp print_scopes(name_parts) do 194 | Enum.each 0..Enum.count(name_parts)-2, fn n -> 195 | Enum.each 0..n, fn _ -> IO.write(" ") end 196 | IO.write(Enum.at(name_parts, n)) 197 | IO.write("\n") 198 | end 199 | end 200 | 201 | defp print_indent(name_parts) do 202 | Enum.each 0..Enum.count(name_parts)-1, fn _ -> IO.write " " end 203 | end 204 | 205 | defp new_scope(config, name_parts) do 206 | scope = Enum.take(name_parts, Enum.count(name_parts)-1) 207 | scope = Enum.join(scope, ".") 208 | if !HashDict.has_key?(config.scope, scope) do 209 | scope 210 | end 211 | end 212 | 213 | defp scoped(test) do 214 | name = format_test_name(test) 215 | name_parts = String.split(name, "-") 216 | if Enum.count(name_parts) > 1 do 217 | name_parts 218 | end 219 | end 220 | 221 | # Color styles 222 | 223 | defp success(msg) do 224 | Amrita.Formatter.Format.colorize("green", msg) 225 | end 226 | 227 | defp invalid(msg) do 228 | Amrita.Formatter.Format.colorize("yellow", msg) 229 | end 230 | 231 | defp pending(msg) do 232 | Amrita.Formatter.Format.colorize("yellow", msg) 233 | end 234 | 235 | defp failure(msg) do 236 | Amrita.Formatter.Format.colorize("red", msg) 237 | end 238 | 239 | defp pending_formatter(:error_info, msg), do: Amrita.Formatter.Format.colorize("yellow", msg) 240 | defp pending_formatter(:location_info, msg), do: Amrita.Formatter.Format.colorize("cyan", msg) 241 | defp pending_formatter(_, msg), do: msg 242 | 243 | defp formatter(:error_info, msg), do: Amrita.Formatter.Format.colorize("red", msg) 244 | defp formatter(:location_info, msg), do: Amrita.Formatter.Format.colorize("cyan", msg) 245 | defp formatter(_, msg), do: msg 246 | 247 | defp trace_test_time(_test, Config[trace: false]), do: "" 248 | defp trace_test_time(test, _config) do 249 | " (#{format_us(test.time)}ms)" 250 | end 251 | 252 | defp format_us(us) do 253 | us = div(us, 10) 254 | if us < 10 do 255 | "0.0#{us}" 256 | else 257 | us = div us, 10 258 | "#{div(us, 10)}.#{rem(us, 10)}" 259 | end 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Amrita 2 | 3 | [![Build Status](https://travis-ci.org/josephwilk/amrita.png?branch=master)](https://travis-ci.org/josephwilk/amrita) 4 | 5 | A polite, well mannered and thoroughly upstanding testing framework for [Elixir](http://elixir-lang.org/). 6 | 7 | ![Elixir of life](http://s2.postimg.org/kmlrx9dp5/6337_33695901.jpg) 8 | 9 | ##Install 10 | 11 | Add to your mix.exs 12 | 13 | ```elixir 14 | defp deps do 15 | [ 16 | {:amrita, "~>0.2", github: "josephwilk/amrita"} 17 | ] 18 | end 19 | ``` 20 | 21 | After adding Amrita as a dependency, to install please run: 22 | 23 | ```console 24 | mix deps.get 25 | ``` 26 | 27 | ##Getting started 28 | 29 | Ensure you start Amrita in: test/test_helper.exs 30 | ```elixir 31 | Amrita.start 32 | 33 | #Or if you want a more documentation focused formatter: 34 | 35 | Amrita.start(formatter: Amrita.Formatter.Documentation) 36 | ``` 37 | 38 | * Require test_helper.exs in every test (this will ensure Amrita is started): 39 | * Mix in `Amrita.Sweet` which will bring in everything you need to use Amrita: 40 | 41 | ```elixir 42 | Code.require_file "../test_helper.exs", __ENV__.file 43 | 44 | defmodule ExampleFacts do 45 | use Amrita.Sweet 46 | 47 | fact "addition" do 48 | 1 + 1 |> 2 49 | end 50 | end 51 | ``` 52 | 53 | Run your tests through mix: 54 | 55 | ```shell 56 | $ mix amrita # Run all your tests 57 | 58 | $ mix amrita test/integration/t_mocks.ex # Run a specific file 59 | 60 | $ mix amrita test/integration/t_mocks.ex:10 # Run a specific test at a line number 61 | 62 | $ mix amrita --trace # Show execution time for slow tests 63 | ``` 64 | 65 | Now time to write some tests! 66 | 67 | ## Prerequisites / Mocks 68 | 69 | Amrita supports BDD style mocks. 70 | 71 | Examples: 72 | 73 | ```elixir 74 | defmodule Polite do 75 | def swear? do 76 | false 77 | end 78 | 79 | def swear?(word) do 80 | false 81 | end 82 | end 83 | ``` 84 | 85 | #### A Simple mock 86 | ```elixir 87 | fact "mock with a wildcard" do 88 | provided [Polite.swear? |> true] do 89 | Polite.swear? |> truthy 90 | end 91 | end 92 | ``` 93 | 94 | #### Wildcard matchers for argument 95 | 96 | ```elixir 97 | fact "mock with a wildcard" 98 | provided [Polite.swear?(_) |> true] do 99 | Polite.swear?(:yes) |> truthy 100 | Polite.swear?(:whatever) |> truthy 101 | end 102 | end 103 | ``` 104 | 105 | #### Powerful custom predicates for argument matching 106 | ```elixir 107 | fact "mock with a matcher function" do 108 | provided [Polite.swear?(fn arg -> arg =~ ~r"moo") |> false] do 109 | Polite.swear?("its ok to moo really") |> falsey 110 | end 111 | end 112 | ``` 113 | 114 | #### Return values based on specific argument values 115 | ```elixir 116 | fact "mock with return based on argument" do 117 | provided [Polite.swear?(:pants) |> false, 118 | Polite.swear?(:bugger) |> true] do 119 | 120 | Funk.swear?(:pants) |> falsey 121 | Funk.swear?(:bugger) |> truthy 122 | end 123 | end 124 | ``` 125 | 126 | #### Polite Errors explaining when things went wrong 127 | 128 | ![Polite mock error message](http://s9.postimg.org/wjwdo9dun/Screen_Shot_2013_07_19_at_20_11_17.png) 129 | 130 | 131 | ## Checkers 132 | 133 | Amrita is also all about checker based testing! 134 | 135 | ```elixir 136 | Code.require_file "../test_helper.exs", __ENV__.file 137 | 138 | defmodule ExampleFacts do 139 | use Amrita.Sweet 140 | 141 | facts "about Amrita checkers" do 142 | 143 | fact "`equals` checks equality" do 144 | 1 - 10 |> equals -9 145 | 146 | # For convience the default checker is equals 147 | # So we can write the above as 148 | 1 - 10 |> -9 149 | 150 | # Pattern matching with tuples 151 | { 1, 2, { 3, 4 } } |> equals {1, _, { _, 4 } } 152 | 153 | # Which is the same as 154 | { 1, 2, { 3, 4 } } |> {1, _, { _, 4 } } 155 | end 156 | 157 | fact "contains checks if an element is in a collection" do 158 | [1, 2, 4, 5] |> contains 4 159 | 160 | {6, 7, 8, 9} |> contains 9 161 | 162 | [a: 1, :b 2] |> contains {:a, 1} 163 | end 164 | 165 | fact "! negates a checker" do 166 | [1, 2, 3, 4] |> !contains 9999 167 | 168 | # or you can add a space, like this. Whatever tickles your fancy. 169 | 170 | [1, 2, 3, 4] |> ! contains 9999 171 | 172 | 10 |> ! equal 11 173 | end 174 | 175 | fact "contains works with strings" do 176 | "mad hatters tea party" |> contains "hatters" 177 | 178 | "mad hatter tea party" |> contains ~r"h(\w+)er" 179 | end 180 | 181 | fact "has_prefix checks if the start of a collection matches" do 182 | [1, 2, 3, 4] |> has_prefix [1, 2] 183 | 184 | {1, 2, 3, 4} |> has_prefix {1, 2} 185 | 186 | "I cannot explain myself for I am not myself" |> has_prefix "I" 187 | end 188 | 189 | fact "has_prefix with a Set ignores the order" do 190 | {1, 2, 3, 4} |> has_prefix Set.new([{2, 1}]) 191 | end 192 | 193 | fact "has_suffix checks if the end of a collection matches" do 194 | [1, 2, 3, 4 ,5] |> has_suffix [4, 5] 195 | 196 | {1, 2, 3, 4} |> has_suffix {3, 4} 197 | 198 | "I cannot explain myself for I am not myself" |> has_suffix "myself" 199 | end 200 | 201 | fact "has_suffix with a Set ignores the order" do 202 | {1, 2, 3, 4} |> has_suffix Set.new([{4, 3}]) 203 | end 204 | 205 | fact "for_all checks if a predicate holds for all elements" do 206 | [2, 4, 6, 8] |> for_all even(&1) 207 | 208 | # or alternatively you could write 209 | 210 | [2, 4, 6, 8] |> Enum.all? even(&1) 211 | end 212 | 213 | fact "odd checks if a number is, well odd" do 214 | 1 |> odd 215 | end 216 | 217 | fact "even checks is a number if even" do 218 | 2 |> even 219 | end 220 | 221 | fact "roughly checks if a float within some +-delta matches" do 222 | 0.1001 |> roughly 0.1 223 | end 224 | 225 | fact "falsey checks if expression evalulates to false" do 226 | nil |> falsey 227 | end 228 | 229 | fact "truthy checks if expression evaulates to true" do 230 | "" |> truthy 231 | end 232 | 233 | defexception Boom, message: "Golly gosh" 234 | 235 | fact "raises checks if an exception was raised" do 236 | fn -> raise Boom end |> raises ExampleFacts.Boom 237 | end 238 | end 239 | 240 | future_fact "I'm not run yet, just printed as a reminder. Like a TODO" do 241 | # Never run 242 | false |> truthy 243 | end 244 | 245 | fact "a fact without a body is much like a TODO" 246 | 247 | # Backwards compatible with ExUnit 248 | test "arithmetic" do 249 | assert 1 + 1 == 2 250 | end 251 | 252 | end 253 | ``` 254 | 255 | ## Assertion Syntax with |> 256 | 257 | The syntax for assertions is as follows: 258 | 259 | ```elixir 260 | # Equality check 261 | ACTUAL |> [EXPECTED] 262 | # Not equal check 263 | ACTUAL |> ! [EXPECTED] 264 | 265 | # Using a checker function 266 | ACTUAL |> CHECKER [EXPECTED] 267 | # or negative form 268 | ACTUAL |> !CHECKER [EXPECTED] 269 | ``` 270 | 271 | 272 | ##Custom checkers 273 | 274 | Its simple to create your own checkers: 275 | 276 | ```elixir 277 | defchecker a_thousand(actual) do 278 | rem(actual, 1000) |> equals 0 279 | end 280 | 281 | fact "about 1000s" do 282 | 1000 |> a_thousand # true 283 | 1200 |> ! a_thousand # true 284 | end 285 | ``` 286 | 287 | ## Polite error messages: 288 | 289 | Amrita tries its best to be polite with its errors: 290 | 291 | ![Polite error message](http://s24.postimg.org/vlj6epnmt/Screen_Shot_2013_06_01_at_22_12_16.png) 292 | 293 | ## Amrita with Dynamo 294 | 295 | Checkout an example using Amrita with Dynamo: https://github.com/elixir-amrita/amrita_with_dynamo 296 | 297 | ### Plugins 298 | 299 | See the wiki for various IDE plugins for Amrita: https://github.com/josephwilk/amrita/wiki/Plugins 300 | 301 | ## Amrita Development 302 | 303 | Hacking on Amrita. 304 | 305 | ###Running tests 306 | 307 | Amrita runs tests against Elixir's latest stable release and against Elixir master. 308 | Make is your friend for running these tests: 309 | 310 | ``` 311 | # Run lastest stable and elixir master 312 | make ci 313 | 314 | # Run tests against your current Elixir install 315 | make 316 | ``` 317 | 318 | ### Docs 319 | 320 | http://josephwilk.github.io/amrita/docs 321 | 322 | ## Bloody good show 323 | 324 | Thanks for reading me, I appreciate it. 325 | 326 | Have a good day. 327 | 328 | Maybe drink some tea. 329 | 330 | Its good for the constitution. 331 | 332 | ![Tea](http://s15.postimg.org/9dqs4g0wr/tea.png) 333 | 334 | ##License 335 | (The MIT License) 336 | 337 | Copyright (c) 2014 Joseph Wilk 338 | 339 | Permission is hereby granted, free of charge, to any person obtaining 340 | a copy of this software and associated documentation files (the 341 | 'Software'), to deal in the Software without restriction, including 342 | without limitation the rights to use, copy, modify, merge, publish, 343 | distribute, sublicense, and/or sell copies of the Software, and to 344 | permit persons to whom the Software is furnished to do so, subject to 345 | the following conditions: 346 | 347 | The above copyright notice and this permission notice shall be 348 | included in all copies or substantial portions of the Software. 349 | 350 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 351 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 352 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 353 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 354 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 355 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 356 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 357 | --------------------------------------------------------------------------------