├── .ebert.yml ├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── LICENSE ├── README.md ├── difference.png ├── head.gif ├── lib ├── power_assert.ex └── power_assert │ ├── assertion.ex │ ├── debug.ex │ └── renderer.ex ├── mix.exs ├── mix.lock └── test ├── ex_spec └── ex_spec_test.exs ├── power_assert └── debug_test.exs ├── power_assert_case_template_test.exs ├── power_assert_test.exs ├── power_assert_use_ex_unit_test.exs └── test_helper.exs /.ebert.yml: -------------------------------------------------------------------------------- 1 | # This configuration was used Ebert to review the ma2gedev/power_assert_ex repository 2 | # on 668343ba3734a20115b670d4a7ccabe5566ffcbd. 3 | # You can make this the default configuration for future reviews by moving this 4 | # file to your repository as `.ebert.yml` and pushing it to GitHub, and tweak 5 | # it as you wish - To know more on how to change this file to better review your 6 | # repository you can go to https://ebertapp.io/docs/config and see the configuration 7 | # details. 8 | --- 9 | styleguide: plataformatec/linters 10 | engines: 11 | credo: 12 | enabled: true 13 | fixme: 14 | enabled: true 15 | remark-lint: 16 | enabled: true 17 | exclude_paths: 18 | - config 19 | - test 20 | 21 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{mix,.formatter}.exs", 4 | "{config,lib}/**/*.{ex,exs}" 5 | ] 6 | ] 7 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | 11 | name: Build and test 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | matrix: 15 | include: 16 | - elixir: "1.10" 17 | otp: 22.3 18 | - elixir: "1.11" 19 | otp: 23.3 20 | - elixir: "1.12" 21 | otp: 23.3 22 | - elixir: "1.13" 23 | otp: 23.3 24 | - elixir: "1.13" 25 | otp: 24.3 26 | - elixir: "1.14" 27 | otp: 24.3 28 | - elixir: "1.14" 29 | otp: 25.1 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Set up Elixir 33 | uses: erlef/setup-beam@v1 34 | with: 35 | elixir-version: ${{ matrix.elixir }} # Define the elixir version [required] 36 | otp-version: ${{ matrix.otp }} # Define the OTP version [required] 37 | - name: Restore dependencies cache 38 | uses: actions/cache@v2 39 | with: 40 | path: deps 41 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }} 42 | restore-keys: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix- 43 | - name: Install dependencies 44 | run: mix deps.get 45 | - name: Run tests 46 | run: mix test 47 | 48 | format: 49 | name: Format 50 | runs-on: ubuntu-20.04 51 | strategy: 52 | matrix: 53 | include: 54 | - elixir: 1.14 55 | otp: 25.1 56 | steps: 57 | - uses: actions/checkout@v2 58 | - name: Set up Elixir 59 | uses: erlef/setup-beam@v1 60 | with: 61 | elixir-version: ${{ matrix.elixir }} # Define the elixir version [required] 62 | otp-version: ${{ matrix.otp }} # Define the OTP version [required] 63 | - name: Restore dependencies cache 64 | uses: actions/cache@v2 65 | with: 66 | path: deps 67 | key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }} 68 | restore-keys: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix- 69 | - name: Install dependencies 70 | run: mix deps.get 71 | - name: Check format 72 | run: mix format --check-formatted 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Takayuki Matsubara 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Power Assert 2 | 3 | [![hex.pm version](https://img.shields.io/hexpm/v/power_assert.svg)](https://hex.pm/packages/power_assert) [![hex.pm daily downloads](https://img.shields.io/hexpm/dd/power_assert.svg)](https://hex.pm/packages/power_assert) [![hex.pm weekly downloads](https://img.shields.io/hexpm/dw/power_assert.svg)](https://hex.pm/packages/power_assert) [![hex.pm downloads](https://img.shields.io/hexpm/dt/power_assert.svg)](https://hex.pm/packages/power_assert) [![Build Status](https://github.com/ma2gedev/power_assert_ex/workflows/Elixir%20CI/badge.svg?branch=master)](https://github.com/ma2gedev/power_assert_ex/actions?query=workflow%3A%22Elixir+CI%22) [![License](https://img.shields.io/hexpm/l/power_assert.svg)](http://www.apache.org/licenses/LICENSE-2.0) 4 | 5 | Power Assert makes test results easier to understand, without changing your ExUnit test code. 6 | 7 | ![Demo](https://github.com/ma2gedev/power_assert_ex/raw/master/head.gif) 8 | 9 | Example test is here: 10 | 11 | ```elixir 12 | test "Enum.at should return the element at the given index" do 13 | array = [1, 2, 3, 4, 5, 6]; index = 2; two = 2 14 | assert array |> Enum.at(index) == two 15 | end 16 | ``` 17 | 18 | Here is the difference between ExUnit and Power Assert results: 19 | 20 | ![Difference between ExUnit and Power Assert](https://github.com/ma2gedev/power_assert_ex/raw/master/difference.png) 21 | 22 | Enjoy :muscle: ! 23 | 24 | ## Installation 25 | 26 | Add Power Assert to your `mix.exs` dependencies: 27 | 28 | ```elixir 29 | defp deps do 30 | [{:power_assert, "~> 0.2.0", only: :test}] 31 | end 32 | ``` 33 | 34 | and fetch `$ mix deps.get`. 35 | 36 | ## Usage 37 | 38 | Replace `use ExUnit.Case` into `use PowerAssert` in your test code: 39 | 40 | ```elixir 41 | ## before(ExUnit) 42 | defmodule YourAwesomeTest do 43 | use ExUnit.Case # <-- **HERE** 44 | end 45 | 46 | ## after(PowerAssert) 47 | defmodule YourAwesomeTest do 48 | use PowerAssert # <-- **REPLACED** 49 | end 50 | ``` 51 | 52 | Done! You can run `$ mix test`. 53 | 54 | ### Use with ExUnit.CaseTemplate 55 | 56 | Insert `use PowerAssert` with `ExUnit.CaseTemplate.using/2` macro: 57 | 58 | ```elixir 59 | ## before(ExUnit.CaseTemplate) 60 | defmodule YourAwesomeTest do 61 | use ExUnit.CaseTemplate 62 | end 63 | 64 | ## after(PowerAssert) 65 | defmodule YourAwesomeTest do 66 | use ExUnit.CaseTemplate 67 | 68 | # add the following 69 | using do 70 | quote do 71 | use PowerAssert 72 | end 73 | end 74 | end 75 | ``` 76 | 77 | ### protip: useful command to replace `use ExUnit.Case` 78 | 79 | ```bash 80 | $ git grep -l 'use ExUnit\.Case' | xargs sed -i.bak -e 's/use ExUnit\.Case/use PowerAssert/g' 81 | ``` 82 | 83 | ## How to use with other framework depending on ExUnit such as ExSpec 84 | 85 | ### ExSpec 86 | 87 | Append `use PowerAssert` after `use ExSpec`: 88 | 89 | ```elixir 90 | defmodule ExSpecBasedTest do 91 | use ExSpec 92 | use PowerAssert # <-- append 93 | 94 | describe "describe" do 95 | it "it" do 96 | assert something == "hoge" 97 | end 98 | end 99 | end 100 | ``` 101 | 102 | See also: test/ex_spec/ex_spec_test.exs 103 | 104 | 105 | ## API 106 | 107 | Only provide `assert` macro: 108 | 109 | ```elixir 110 | assert(expression, message \\ nil) 111 | ``` 112 | 113 | ## Dependencies 114 | 115 | - ExUnit 116 | 117 | ## Limitation 118 | 119 | - NOT SUPPORTED 120 | - match expression ex: `assert List.first(x = [false])` 121 | - fn expression ex: `assert fn(x) -> x == 1 end.(2)` 122 | - :: expression ex: `<< x :: bitstring >>` 123 | - this means string interpolations also unsupported ex: `"#{x} hoge"` 124 | - sigil expression ex: `~w(hoge fuga)` 125 | - quote arguments ex: `assert quote(@opts, do: :hoge)` 126 | - case expression 127 | - get_and_update_in/2, put_in/2, update_in/2, for/1 128 | - <<>> expression includes attributes `<<@x, "y">>; <>` 129 | - `__MODULE__.Foo` 130 | - many macros maybe caught error... 131 | 132 | ## Resources 133 | 134 | - [Testing with Power Assert in Elixir projects](http://qiita.com/ma2ge/items/29115d0afbf97a092783) 135 | - [Power Assert Inside in Elixir](https://speakerdeck.com/ma2gedev/power-assert-inside-in-elixir) 136 | 137 | ## Author 138 | 139 | Takayuki Matsubara (@ma2ge on twitter) 140 | 141 | ## License 142 | 143 | Distributed under the Apache 2 License. 144 | 145 | Check [LICENSE](LICENSE) files for more information. 146 | 147 | -------------------------------------------------------------------------------- /difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ma2gedev/power_assert_ex/3cca46e4330bbdd89869070a665b34e7024793a8/difference.png -------------------------------------------------------------------------------- /head.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ma2gedev/power_assert_ex/3cca46e4330bbdd89869070a665b34e7024793a8/head.gif -------------------------------------------------------------------------------- /lib/power_assert.ex: -------------------------------------------------------------------------------- 1 | defmodule PowerAssert do 2 | defmacro __using__(opts) do 3 | use_ex_unit = Keyword.get(opts, :use_ex_unit, false) 4 | 5 | case use_ex_unit do 6 | false -> 7 | quote do 8 | use ExUnit.Case, unquote(opts) 9 | import ExUnit.Assertions, except: [assert: 1, assert: 2] 10 | import PowerAssert.Assertion 11 | end 12 | 13 | true -> 14 | quote do 15 | require PowerAssert.Assertion 16 | 17 | defmacro power_assert(ast, msg \\ nil) do 18 | quote do 19 | PowerAssert.Assertion.assert(unquote(ast), unquote(msg)) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/power_assert/assertion.ex: -------------------------------------------------------------------------------- 1 | defmodule PowerAssert.PositionAndValue do 2 | defstruct [:position, :value] 3 | end 4 | 5 | defmodule PowerAssert.Assertion do 6 | @moduledoc """ 7 | This module handles Power Assert main function 8 | """ 9 | 10 | @assign_string " = " 11 | # length of " = " 12 | @assign_len 3 13 | 14 | @equal_string " == " 15 | # length of " == " 16 | @equal_len 4 17 | 18 | @doc """ 19 | assert with descriptive messages 20 | 21 | array |> Enum.at(index) == two 22 | | | | | 23 | | | | 2 24 | | | 2 25 | | 3 26 | [1, 2, 3] 27 | """ 28 | defmacro assert(ast, msg \\ nil) 29 | 30 | defmacro assert({:=, _, [left, right]} = ast, msg) do 31 | # Almost the same code as ExUnit but rhs is displayed in detail 32 | code = Macro.escape(ast) 33 | [lhs_expr | t] = String.split(Macro.to_string(ast), @assign_string) 34 | rhs_expr = Enum.join(t, @assign_string) 35 | rhs_index = String.length(lhs_expr) + @assign_len 36 | injected_rhs_ast = __inject_store_code__(right, rhs_expr, rhs_index) 37 | message_ast = message_ast(msg) 38 | 39 | left = Macro.expand(left, __CALLER__) 40 | vars = collect_vars_from_pattern(left) 41 | 42 | return = 43 | no_warning( 44 | quote do 45 | if right do 46 | right 47 | else 48 | message = PowerAssert.Renderer.render(expr, position_and_values) 49 | unquote(message_ast) 50 | 51 | raise ExUnit.AssertionError, 52 | message: "Expected truthy, got #{inspect(right)}\n\n" <> message 53 | end 54 | end 55 | ) 56 | 57 | quote do 58 | unquote(injected_rhs_ast) 59 | right = result 60 | expr = unquote(code) 61 | 62 | unquote(vars) = 63 | case right do 64 | unquote(left) -> 65 | unquote(return) 66 | unquote(vars) 67 | 68 | _ -> 69 | message = PowerAssert.Renderer.render(expr, position_and_values) 70 | unquote(message_ast) 71 | 72 | raise ExUnit.AssertionError, 73 | message: message 74 | end 75 | 76 | right 77 | end 78 | end 79 | 80 | defmacro assert({:==, _, [left, right]} = ast, msg) do 81 | code = Macro.escape(ast) 82 | [lhs_expr | t] = String.split(Macro.to_string(ast), @equal_string) 83 | rhs_expr = Enum.join(t, @equal_string) 84 | injected_lhs_ast = __inject_store_code__(left, lhs_expr) 85 | rhs_index = String.length(lhs_expr) + @equal_len 86 | injected_rhs_ast = __inject_store_code__(right, rhs_expr, rhs_index) 87 | message_ast = message_ast(msg) 88 | 89 | quote do 90 | unquote(injected_lhs_ast) 91 | left = result 92 | left_position_and_values = position_and_values 93 | unquote(injected_rhs_ast) 94 | # wrap result for avoid warning: this check/guard will always yield the same result 95 | unless left == (fn x -> x end).(result) do 96 | message = 97 | PowerAssert.Renderer.render( 98 | unquote(code), 99 | left_position_and_values ++ position_and_values, 100 | left, 101 | result 102 | ) 103 | 104 | unquote(message_ast) 105 | 106 | raise ExUnit.AssertionError, 107 | message: message 108 | end 109 | 110 | result 111 | end 112 | end 113 | 114 | defmacro assert(ast, msg) do 115 | code = Macro.escape(ast) 116 | injected_ast = __inject_store_code__(ast, Macro.to_string(ast)) 117 | 118 | message_ast = message_ast(msg) 119 | 120 | quote do 121 | unquote(injected_ast) 122 | 123 | unless result do 124 | message = PowerAssert.Renderer.render(unquote(code), position_and_values) 125 | unquote(message_ast) 126 | 127 | raise ExUnit.AssertionError, 128 | message: message 129 | end 130 | 131 | result 132 | end 133 | end 134 | 135 | # avoid a warning that "this check/guard will always yield the same result" 136 | defp message_ast(msg) when is_binary(msg) do 137 | quote do 138 | message = "#{unquote(msg)}\n\n" <> message 139 | end 140 | end 141 | 142 | defp message_ast(_msg), do: nil 143 | 144 | # structs 145 | defmodule Detector do 146 | defstruct code: nil, positions: [], in_fn: 0 147 | end 148 | 149 | defmodule Injector do 150 | defstruct positions: nil, in_fn: 0 151 | end 152 | 153 | @doc false 154 | def __inject_store_code__(ast, expr, default_index \\ 0) do 155 | positions = detect_position(ast, expr, default_index) 156 | 157 | {injected_ast, _} = 158 | Macro.traverse( 159 | ast, 160 | %Injector{positions: Enum.reverse(positions)}, 161 | &pre_catcher/2, 162 | &catcher/2 163 | ) 164 | 165 | # IO.inspect injected_ast 166 | # IO.inspect Macro.to_string injected_ast 167 | quote do 168 | {:ok, buffer} = Agent.start_link(fn -> [] end) 169 | result = unquote(injected_ast) 170 | position_and_values = Agent.get(buffer, & &1) 171 | Agent.stop(buffer) 172 | end 173 | end 174 | 175 | ## detect positions 176 | defp detect_position(ast, expr, default_index) do 177 | {_ast, %Detector{positions: positions}} = 178 | Macro.traverse(ast, %Detector{code: expr}, &pre_collect_position/2, &collect_position/2) 179 | 180 | Enum.map(positions, fn [pos, _code] -> default_index + pos end) 181 | end 182 | 183 | @ignored_atoms [:fn, :&, :=, :"::", :@] 184 | defp pre_collect_position({atom, _, _args} = ast, detector) when atom in @ignored_atoms do 185 | {ast, %{detector | in_fn: detector.in_fn + 1}} 186 | end 187 | 188 | @unsupported_func [:quote, :<<>>, :case] 189 | defp pre_collect_position({func, _, _args} = ast, detector) when func in @unsupported_func do 190 | {ast, %{detector | in_fn: detector.in_fn + 1}} 191 | end 192 | 193 | @unsupported_func_arity2 [:put_in, :get_and_update_in, :update_in, :for, :match?] 194 | # get_and_update_in/2, put_in/2, update_in/2, for 195 | defp pre_collect_position({func, _, [_, _]} = ast, detector) 196 | when func in @unsupported_func_arity2 do 197 | {ast, %{detector | in_fn: detector.in_fn + 1}} 198 | end 199 | 200 | @ignore_ops [ 201 | :., 202 | :__aliases__, 203 | :|>, 204 | :==, 205 | :!=, 206 | :<, 207 | :>, 208 | :>=, 209 | :<=, 210 | :*, 211 | :||, 212 | :&&, 213 | :<>, 214 | :===, 215 | :!==, 216 | :and, 217 | :or, 218 | :=~, 219 | :%{}, 220 | :%, 221 | :->, 222 | :|, 223 | :{} 224 | ] 225 | defp pre_collect_position({func, _, args} = ast, detector) 226 | when func not in @ignore_ops and is_atom(func) and is_list(args) do 227 | case Atom.to_string(func) do 228 | <<"sigil_", _name>> -> 229 | {ast, %{detector | in_fn: detector.in_fn + 1}} 230 | 231 | _ -> 232 | {ast, detector} 233 | end 234 | end 235 | 236 | defp pre_collect_position(ast, detector) do 237 | {ast, detector} 238 | end 239 | 240 | # ex: 241 | # context[:key] 242 | # ^ 243 | defp collect_position( 244 | {{:., _, [Access, :get]}, _, [_l, _r]} = ast, 245 | %Detector{in_fn: in_fn} = detector 246 | ) 247 | when in_fn > 0, 248 | do: {ast, detector} 249 | 250 | defp collect_position({{:., _, [Access, :get]}, _, [_l, right]} = ast, detector) do 251 | func_call = Macro.to_string(ast) 252 | match_indexes = match_indexes_in_code(func_call, detector.code, :function_boundary) 253 | right_expr = Macro.to_string(right) 254 | # last value is needed. ex: keywords = [value: [value: "nya"]]; keywords[:value][:value] 255 | [[{r_pos, r_len}] | _t] = 256 | Regex.scan(~r/\[#{Regex.escape(right_expr)}\]/, func_call, return: :index) 257 | |> Enum.reverse() 258 | 259 | match_indexes = Enum.map(match_indexes, fn [{pos, _len}] -> [{pos + r_pos, r_len}] end) 260 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_call) 261 | {ast, %{detector | positions: positions}} 262 | end 263 | 264 | # ex: 265 | # map = %{value: "nya-"}; map.value 266 | # ^ 267 | # 268 | # List.first([1,2,3]) 269 | # ^ 270 | defp collect_position({{:., _, [_l, r_atom]}, _, _} = ast, %Detector{in_fn: in_fn} = detector) 271 | when is_atom(r_atom) and in_fn > 0, 272 | do: {ast, detector} 273 | 274 | defp collect_position({{:., _, [_l, r_atom]}, _, _} = ast, detector) when is_atom(r_atom) do 275 | func_call = Macro.to_string(ast) 276 | match_indexes = match_indexes_in_code(func_call, detector.code, :function_boundary) 277 | right_func_name = Atom.to_string(r_atom) 278 | # last value is needed. ex: map = %{value: %{value: "nya"}}; map.value.value 279 | [[{r_pos, r_len}] | _t] = 280 | Regex.scan(~r/(?!\.)#{Regex.escape(right_func_name)}/, func_call, return: :index) 281 | |> Enum.reverse() 282 | 283 | match_indexes = Enum.map(match_indexes, fn [{pos, _len}] -> [{pos + r_pos, r_len}] end) 284 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_call) 285 | {ast, %{detector | positions: positions}} 286 | end 287 | 288 | # ex: 289 | # func = fn () -> "nya-" end; func.() 290 | # ^ 291 | defp collect_position({{:., _, [_l]}, _, _} = ast, %Detector{in_fn: in_fn} = detector) 292 | when in_fn > 0, 293 | do: {ast, detector} 294 | 295 | defp collect_position({{:., _, [l]}, _, _} = ast, detector) do 296 | func_call = Macro.to_string(ast) 297 | match_indexes = match_indexes_in_code(func_call, detector.code, :function_boundary) 298 | length = function_expression(l) |> String.length() 299 | match_indexes = Enum.map(match_indexes, fn [{pos, len}] -> [{pos + length + 1, len}] end) 300 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_call) 301 | {ast, %{detector | positions: positions}} 302 | end 303 | 304 | # ex: 305 | # x + y 306 | # ^ 307 | @arithmetic_ops [:*, :+, :-, :/, :++, :--] 308 | defp collect_position({op, _, [_l, _r]} = ast, %Detector{in_fn: in_fn} = detector) 309 | when op in @arithmetic_ops and in_fn > 0, 310 | do: {ast, detector} 311 | 312 | defp collect_position({op, _, [l, _r]} = ast, detector) when op in @arithmetic_ops do 313 | func_call = Macro.to_string(ast) 314 | match_indexes = match_indexes_in_code(func_call, detector.code, :function_boundary) 315 | left_expr_len = Macro.to_string(l) |> String.length() 316 | op_len = Atom.to_string(op) |> String.length() 317 | 318 | match_indexes = 319 | Enum.map(match_indexes, fn [{pos, _len}] -> [{pos + left_expr_len + 1, op_len}] end) 320 | 321 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_call) 322 | {ast, %{detector | positions: positions}} 323 | end 324 | 325 | # ex: 326 | # fn(x) -> x == 1 end.(2) 327 | # @module_attribute 328 | defp collect_position({atom, _, _args} = ast, detector) when atom in @ignored_atoms do 329 | if atom == :@ and detector.in_fn == 1 do 330 | func_code = Macro.to_string(ast) 331 | match_indexes = match_indexes_in_code(func_code, detector.code, :function) 332 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_code) 333 | {ast, %{detector | positions: positions, in_fn: detector.in_fn - 1}} 334 | else 335 | {ast, %{detector | in_fn: detector.in_fn - 1}} 336 | end 337 | end 338 | 339 | # ex: 340 | # disregard inner ast 341 | # quote do: :hoge 342 | # ^ 343 | defp collect_position({func, _, _args} = ast, detector) when func in @unsupported_func do 344 | if detector.in_fn == 1 do 345 | func_code = Macro.to_string(ast) 346 | match_indexes = match_indexes_in_code(func_code, detector.code, :function) 347 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_code) 348 | {ast, %{detector | positions: positions, in_fn: detector.in_fn - 1}} 349 | else 350 | {ast, %{detector | in_fn: detector.in_fn - 1}} 351 | end 352 | end 353 | 354 | # ex: 355 | # get_and_update_in/2, put_in/2, update_in/2, for needs special format for first argument 356 | defp collect_position({func, _, [_, _]} = ast, detector) 357 | when func in @unsupported_func_arity2 do 358 | if detector.in_fn == 1 do 359 | func_code = Macro.to_string(ast) 360 | match_indexes = match_indexes_in_code(func_code, detector.code, :function) 361 | positions = insert_pos_unless_exist(detector.positions, match_indexes, func_code) 362 | {ast, %{detector | positions: positions, in_fn: detector.in_fn - 1}} 363 | else 364 | {ast, %{detector | in_fn: detector.in_fn - 1}} 365 | end 366 | end 367 | 368 | # ex: 369 | # import List 370 | # [1, 2] |> first() 371 | # ^ 372 | defp collect_position({func, _, args} = ast, %Detector{in_fn: in_fn} = detector) 373 | when func not in @ignore_ops and is_atom(func) and is_list(args) and in_fn > 0 do 374 | case Atom.to_string(func) do 375 | # not supported sigils 376 | <<"sigil_", _name>> -> 377 | {ast, %{detector | in_fn: detector.in_fn - 1}} 378 | 379 | _ -> 380 | {ast, detector} 381 | end 382 | end 383 | 384 | defp collect_position({func, _, args} = ast, detector) 385 | when func not in @ignore_ops and is_atom(func) and is_list(args) do 386 | func_code = Macro.to_string(ast) 387 | matches = Regex.scan(~r/(? 0, 400 | do: {ast, detector} 401 | 402 | defp collect_position({variable, _, el} = ast, detector) 403 | when is_atom(variable) and is_atom(el) do 404 | code_fragment = Macro.to_string(ast) 405 | match_indexes = match_indexes_in_code(code_fragment, detector.code, :variable) 406 | positions = insert_pos_unless_exist(detector.positions, match_indexes, code_fragment) 407 | {ast, %{detector | positions: positions}} 408 | end 409 | 410 | defp collect_position(ast, detector), do: {ast, detector} 411 | 412 | defp match_indexes_in_code(code_fragment, code, :function_boundary) do 413 | Regex.scan(~r/#{Regex.escape(code_fragment)}/, code, return: :index) 414 | end 415 | 416 | defp match_indexes_in_code(code_fragment, code, :function) do 417 | Regex.scan(~r/#{Regex.escape(code_fragment)}/, code, return: :index) 418 | end 419 | 420 | defp match_indexes_in_code(code_fragment, code, :variable) do 421 | Regex.scan(~r/(? p == pos end) do 433 | insert_pos_unless_exist(positions, tail, code) 434 | else 435 | List.insert_at(positions, 0, [pos, code]) 436 | end 437 | end 438 | 439 | defp function_expression({:fn, _, _args} = left_ast) do 440 | "(#{Macro.to_string(left_ast)})" 441 | end 442 | 443 | defp function_expression(left_ast) do 444 | Macro.to_string(left_ast) 445 | end 446 | 447 | ## injection 448 | defp inject_first_argument({:__block__, block_meta, [first, second, third]} = _ast) do 449 | ast_for_inject = {:l_value, [], PowerAssert.Assertion} 450 | {:=, meta, [v, {func_call, func_meta, func_args}]} = first 451 | 452 | injected_first_arg_ast = 453 | {:=, meta, [v, {func_call, func_meta, List.insert_at(func_args || [], 0, ast_for_inject)}]} 454 | 455 | {:inject, {:__block__, block_meta, [injected_first_arg_ast, second, third]}} 456 | end 457 | 458 | defp inject_first_argument(ast) do 459 | {:none, ast} 460 | end 461 | 462 | defp pre_catcher({atom, _, _args} = ast, injector) when atom in @ignored_atoms do 463 | {ast, %{injector | in_fn: injector.in_fn + 1}} 464 | end 465 | 466 | defp pre_catcher({func, _, _args} = ast, injector) when func in @unsupported_func do 467 | {ast, %{injector | in_fn: injector.in_fn + 1}} 468 | end 469 | 470 | defp pre_catcher({func, _, [_, _]} = ast, injector) when func in @unsupported_func_arity2 do 471 | {ast, %{injector | in_fn: injector.in_fn + 1}} 472 | end 473 | 474 | defp pre_catcher({func, _, args} = ast, injector) 475 | when func not in @ignore_ops and is_atom(func) and is_list(args) do 476 | case Atom.to_string(func) do 477 | <<"sigil_", _name>> -> 478 | {ast, %{injector | in_fn: injector.in_fn + 1}} 479 | 480 | _ -> 481 | {ast, injector} 482 | end 483 | end 484 | 485 | defp pre_catcher(ast, injector) do 486 | {ast, injector} 487 | end 488 | 489 | defp store_value_ast(ast, pos) do 490 | quote do 491 | v = unquote(ast) 492 | 493 | Agent.update( 494 | buffer, 495 | &[%PowerAssert.PositionAndValue{position: unquote(pos), value: v} | &1] 496 | ) 497 | 498 | v 499 | end 500 | end 501 | 502 | defp catcher({:|>, _meta, [_l, _r]} = ast, %Injector{in_fn: in_fn} = injector) when in_fn > 0, 503 | do: {ast, injector} 504 | 505 | defp catcher({:|>, _meta, [l, r]}, injector) do 506 | {res, r_ast} = inject_first_argument(r) 507 | 508 | ast = 509 | if res == :inject do 510 | quote do 511 | l_value = unquote(l) 512 | unquote(r_ast) 513 | end 514 | else 515 | quote do 516 | unquote(l) |> unquote(r_ast) 517 | end 518 | end 519 | 520 | {ast, injector} 521 | end 522 | 523 | defp catcher({{:., _, [Access, :get]}, _, [_l, _r]} = ast, %Injector{in_fn: in_fn} = injector) 524 | when in_fn > 0, 525 | do: {ast, injector} 526 | 527 | defp catcher( 528 | {{:., _, [Access, :get]}, _, [_l, _r]} = ast, 529 | %Injector{positions: [pos | t]} = injector 530 | ) do 531 | {store_value_ast(ast, pos), %{injector | positions: t}} 532 | end 533 | 534 | defp catcher({{:., _, [_l, r_atom]}, _meta, _} = ast, %Injector{in_fn: in_fn} = injector) 535 | when is_atom(r_atom) and in_fn > 0, 536 | do: {ast, injector} 537 | 538 | defp catcher( 539 | {{:., _, [_l, r_atom]}, _meta, _} = ast, 540 | %Injector{positions: [pos | t]} = injector 541 | ) 542 | when is_atom(r_atom) do 543 | {store_value_ast(ast, pos), %{injector | positions: t}} 544 | end 545 | 546 | defp catcher({{:., _, [_l]}, _, _} = ast, %Injector{in_fn: in_fn} = injector) when in_fn > 0, 547 | do: {ast, injector} 548 | 549 | defp catcher({{:., _, [_l]}, _, _} = ast, %Injector{positions: [pos | t]} = injector) do 550 | {store_value_ast(ast, pos), %{injector | positions: t}} 551 | end 552 | 553 | defp catcher({op, _, [_l, _r]} = ast, %Injector{in_fn: in_fn} = injector) 554 | when op in @arithmetic_ops and in_fn > 0, 555 | do: {ast, injector} 556 | 557 | defp catcher({op, _, [_l, _r]} = ast, %Injector{positions: [pos | t]} = injector) 558 | when op in @arithmetic_ops do 559 | {store_value_ast(ast, pos), %{injector | positions: t}} 560 | end 561 | 562 | defp catcher({atom, _, _args} = ast, %Injector{positions: [pos | t]} = injector) 563 | when atom in @ignored_atoms do 564 | if atom == :@ and injector.in_fn == 1 do 565 | {store_value_ast(ast, pos), %{injector | positions: t, in_fn: injector.in_fn - 1}} 566 | else 567 | {ast, %{injector | in_fn: injector.in_fn - 1}} 568 | end 569 | end 570 | 571 | defp catcher({func, _, _args} = ast, %Injector{positions: [pos | t]} = injector) 572 | when func in @unsupported_func do 573 | if injector.in_fn == 1 do 574 | {store_value_ast(ast, pos), %{injector | positions: t, in_fn: injector.in_fn - 1}} 575 | else 576 | {ast, %{injector | in_fn: injector.in_fn - 1}} 577 | end 578 | end 579 | 580 | defp catcher({func, _, [_, _]} = ast, %Injector{positions: [pos | t]} = injector) 581 | when func in @unsupported_func_arity2 do 582 | if injector.in_fn == 1 do 583 | {store_value_ast(ast, pos), %{injector | positions: t, in_fn: injector.in_fn - 1}} 584 | else 585 | {ast, %{injector | in_fn: injector.in_fn - 1}} 586 | end 587 | end 588 | 589 | defp catcher({func, _, args} = ast, %Injector{positions: [_h | _t], in_fn: in_fn} = injector) 590 | when func not in @ignore_ops and is_atom(func) and is_list(args) and in_fn > 0 do 591 | case Atom.to_string(func) do 592 | <<"sigil_", _name>> -> 593 | {ast, %{injector | in_fn: injector.in_fn - 1}} 594 | 595 | _ -> 596 | {ast, injector} 597 | end 598 | end 599 | 600 | defp catcher({func, _, args} = ast, %Injector{positions: [pos | t]} = injector) 601 | when func not in @ignore_ops and is_atom(func) and is_list(args) do 602 | {store_value_ast(ast, pos), %{injector | positions: t}} 603 | end 604 | 605 | defp catcher({variable, _, el} = ast, %Injector{in_fn: in_fn} = injector) 606 | when is_atom(variable) and is_atom(el) and in_fn > 0, 607 | do: {ast, injector} 608 | 609 | defp catcher({variable, _, el} = ast, %Injector{positions: [pos | t]} = injector) 610 | when is_atom(variable) and is_atom(el) do 611 | {store_value_ast(ast, pos), %{injector | positions: t}} 612 | end 613 | 614 | defp catcher(ast, injector), do: {ast, injector} 615 | 616 | defp collect_vars_from_pattern(expr) do 617 | {_, vars} = 618 | Macro.prewalk(expr, [], fn 619 | {:"::", _, [left, _]}, acc -> 620 | {[left], acc} 621 | 622 | {skip, _, [_]}, acc when skip in [:^, :@] -> 623 | {:ok, acc} 624 | 625 | {:_, _, context}, acc when is_atom(context) -> 626 | {:ok, acc} 627 | 628 | {name, meta, context}, acc when is_atom(name) and is_atom(context) -> 629 | {:ok, [{name, [generated: true] ++ meta, context} | acc]} 630 | 631 | node, acc -> 632 | {node, acc} 633 | end) 634 | 635 | Enum.uniq(vars) 636 | end 637 | 638 | defp no_warning({name, meta, args}) do 639 | {name, [generated: true] ++ meta, args} 640 | end 641 | end 642 | -------------------------------------------------------------------------------- /lib/power_assert/debug.ex: -------------------------------------------------------------------------------- 1 | defmodule PowerAssert.Debug do 2 | @moduledoc """ 3 | This module provides debug utilities 4 | """ 5 | 6 | @doc """ 7 | execute code with inspected prints 8 | useful for debug 9 | 10 | iex> puts_expr(x |> Enum.at(y)) 11 | x |> Enum.at(y) 12 | | | | 13 | | 3 2 14 | [1, 2, 3, 4] 15 | 3 16 | """ 17 | defmacro puts_expr(ast) do 18 | code = Macro.escape(ast) 19 | injected_ast = PowerAssert.Assertion.__inject_store_code__(ast, Macro.to_string(ast)) 20 | 21 | quote do 22 | unquote(injected_ast) 23 | 24 | IO.puts( 25 | PowerAssert.Renderer.render( 26 | unquote(code), 27 | var!(position_and_values, PowerAssert.Assertion) 28 | ) 29 | ) 30 | 31 | var!(result, PowerAssert.Assertion) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/power_assert/renderer.ex: -------------------------------------------------------------------------------- 1 | defmodule PowerAssert.Renderer do 2 | @moduledoc false 3 | 4 | alias PowerAssert.PositionAndValue 5 | 6 | @doc """ 7 | renders test result 8 | """ 9 | def render(code_ast, position_and_values, lhs_result \\ nil, rhs_result \\ nil) 10 | 11 | def render(code_ast, [], lhs_result, rhs_result) do 12 | Macro.to_string(code_ast) <> extra_information(lhs_result, rhs_result) 13 | end 14 | 15 | def render(code_ast, position_and_values, lhs_result, rhs_result) do 16 | code_str = Macro.to_string(code_ast) 17 | 18 | position_and_values = 19 | Enum.sort(position_and_values, fn %PositionAndValue{position: x_pos}, 20 | %PositionAndValue{position: y_pos} -> 21 | x_pos > y_pos 22 | end) 23 | 24 | %PositionAndValue{position: max_pos} = 25 | Enum.max_by(position_and_values, fn %PositionAndValue{position: pos} -> pos end) 26 | 27 | first_line = String.duplicate(" ", max_pos + 1) |> replace_with_bar(position_and_values) 28 | lines = make_lines([], Enum.count(position_and_values), position_and_values, -1) 29 | Enum.join([code_str, first_line] ++ lines, "\n") <> extra_information(lhs_result, rhs_result) 30 | end 31 | 32 | defp make_lines(lines, 0, _, _latest_pos) do 33 | lines 34 | end 35 | 36 | defp make_lines(lines, times, position_and_values, latest_pos) do 37 | [%PositionAndValue{position: pos, value: value} | t] = position_and_values 38 | value = inspect(value) 39 | value_len = String.length(value) 40 | 41 | lines = 42 | if latest_pos != -1 && latest_pos - (pos + value_len) > 0 do 43 | [last_line | tail_lines] = Enum.reverse(lines) 44 | {before_str, after_str} = String.split_at(last_line, pos) 45 | {_removed_str, after_str} = String.split_at(after_str, value_len) 46 | line = before_str <> value <> after_str 47 | Enum.reverse([line | tail_lines]) 48 | else 49 | line = String.duplicate(" ", pos + 1) 50 | line = replace_with_bar(line, position_and_values) 51 | line = String.replace(line, ~r/\|$/, value) 52 | lines ++ [line] 53 | end 54 | 55 | make_lines(lines, times - 1, t, pos) 56 | end 57 | 58 | defp replace_with_bar(line, position_and_values) do 59 | Enum.reduce(position_and_values, line, fn %PositionAndValue{position: pos}, line -> 60 | {front, back} = String.split_at(line, pos + 1) 61 | String.replace(front, ~r/ $/, "|") <> back 62 | end) 63 | end 64 | 65 | defp extra_information(lhs_result, rhs_result) 66 | when is_list(lhs_result) and is_list(rhs_result) do 67 | [ 68 | "\n\nonly in lhs: " <> ((lhs_result -- rhs_result) |> inspect), 69 | "only in rhs: " <> ((rhs_result -- lhs_result) |> inspect) 70 | ] 71 | |> Enum.join("\n") 72 | end 73 | 74 | defp extra_information(lhs_result, rhs_result) when is_map(lhs_result) and is_map(rhs_result) do 75 | lhs_result = Map.delete(lhs_result, :__struct__) 76 | rhs_result = Map.delete(rhs_result, :__struct__) 77 | in_left = Map.split(lhs_result, Map.keys(rhs_result)) |> elem(1) 78 | in_right = Map.split(rhs_result, Map.keys(lhs_result)) |> elem(1) 79 | str = "\n" 80 | 81 | str = 82 | if map_size(in_left) != 0 do 83 | str <> "\nonly in lhs: " <> inspect(in_left) 84 | else 85 | str 86 | end 87 | 88 | str = 89 | if map_size(in_right) != 0 do 90 | str <> "\nonly in rhs: " <> inspect(in_right) 91 | else 92 | str 93 | end 94 | 95 | diff = collect_map_diff(lhs_result, rhs_result) 96 | 97 | str = 98 | case Enum.empty?(diff) do 99 | true -> str 100 | false -> str <> "\ndifference:\n" <> Enum.join(diff, "\n") 101 | end 102 | 103 | str 104 | end 105 | 106 | defp extra_information(lhs_result, rhs_result) do 107 | if String.valid?(lhs_result) && String.valid?(rhs_result) do 108 | extra_information_for_string(lhs_result, rhs_result) 109 | else 110 | "" 111 | end 112 | end 113 | 114 | defp extra_information_for_string(lhs_result, rhs_result) do 115 | "\n\ndifference:" <> "\n" <> lhs_result <> "\n" <> rhs_result 116 | end 117 | 118 | defp collect_map_diff(map1, map2) do 119 | Enum.reduce(map2, [], fn {k, v}, acc -> 120 | case Map.fetch(map1, k) do 121 | {:ok, ^v} -> 122 | acc 123 | 124 | {:ok, map1_value} -> 125 | acc ++ ["key #{inspect(k)} => {#{inspect(map1_value)}, #{inspect(v)}}"] 126 | 127 | _ -> 128 | acc 129 | end 130 | end) 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PowerAssert.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :power_assert, 7 | version: "0.3.0", 8 | elixir: "~> 1.9", 9 | description: "Power Assert in Elixir. Shows evaluation results each expression.", 10 | package: [ 11 | maintainers: ["Takayuki Matsubara"], 12 | licenses: ["Apache-2.0"], 13 | links: %{"GitHub" => "https://github.com/ma2gedev/power_assert_ex"} 14 | ], 15 | build_embedded: Mix.env() == :prod, 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application 22 | # 23 | # Type `mix help compile.app` for more information 24 | def application do 25 | [extra_applications: [:logger]] 26 | end 27 | 28 | # Dependencies can be Hex packages: 29 | # 30 | # {:mydep, "~> 0.3.0"} 31 | # 32 | # Or git/path repositories: 33 | # 34 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 35 | # 36 | # Type `mix help deps` for more examples and options 37 | defp deps do 38 | [ 39 | {:ex_spec, ">= 2.0.0", only: :test}, 40 | {:ex_doc, ">= 0.0.0", only: :dev} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 3 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 4 | "ex_spec": {:hex, :ex_spec, "2.0.1", "8bdbd6fa85995fbf836ed799571d44be6f9ebbcace075209fd0ad06372c111cf", [:mix], [], "hexpm", "b44fe5054497411a58341ece5bf7756c219d9d6c1303b5ac467f557a0a4c31ac"}, 5 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/ex_spec/ex_spec_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExSpecTest do 2 | use ExSpec 3 | use PowerAssert 4 | 5 | describe "in describe" do 6 | context "in context" do 7 | it "success" do 8 | assert 1 + 2 == 3 9 | end 10 | end 11 | 12 | context "power assert error message" do 13 | it "descriptive message" do 14 | try do 15 | assert [1,2,3] |> Enum.take(1) |> Enum.empty?() 16 | rescue 17 | error -> 18 | msg = """ 19 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 20 | | | 21 | [1] false 22 | """ 23 | ExUnit.Assertions.assert error.message <> "\n" == msg 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/power_assert/debug_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PowerAssert.DebugTest do 2 | use ExUnit.Case 3 | import ExUnit.CaptureIO 4 | 5 | import PowerAssert.Debug 6 | 7 | test "puts_expr" do 8 | expect = """ 9 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 10 | | | 11 | [1] false 12 | """ 13 | assert capture_io(fn -> 14 | puts_expr [1,2,3] |> Enum.take(1) |> Enum.empty?() 15 | end) == expect 16 | 17 | expect = """ 18 | :hoge == :fuga 19 | """ 20 | assert capture_io(fn -> 21 | puts_expr :hoge == :fuga 22 | end) == expect 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/power_assert_case_template_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyCase do 2 | use ExUnit.CaseTemplate 3 | using do 4 | quote do 5 | use PowerAssert 6 | end 7 | end 8 | 9 | setup do 10 | {:ok, executed: "setup func"} 11 | end 12 | end 13 | 14 | defmodule MyTest do 15 | use MyCase, async: true 16 | 17 | test "executed setup function", context do 18 | assert context[:executed] == "setup func" 19 | end 20 | 21 | test "raise", context do 22 | assert context[:executed] == "setup func" 23 | try do 24 | assert [1,2,3] |> Enum.take(1) |> Enum.empty?() 25 | rescue 26 | error -> 27 | msg = """ 28 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 29 | | | 30 | [1] false 31 | """ 32 | ExUnit.Assertions.assert error.message <> "\n" == msg 33 | end 34 | end 35 | end 36 | 37 | defmodule MyCaseUsing do 38 | use ExUnit.CaseTemplate 39 | 40 | using do 41 | quote do 42 | use PowerAssert 43 | import MyCaseUsing 44 | end 45 | end 46 | 47 | def my_function(msg) do 48 | msg 49 | end 50 | 51 | setup do 52 | {:ok, executed: "setup func"} 53 | end 54 | end 55 | 56 | defmodule MyTestUsing do 57 | use MyCaseUsing, async: true 58 | 59 | test "using function is available" do 60 | assert my_function("using") == "using" 61 | end 62 | 63 | test "executed setup function", context do 64 | assert context[:executed] == "setup func" 65 | end 66 | 67 | test "raise", context do 68 | assert context[:executed] == "setup func" 69 | try do 70 | assert [1,2,3] |> Enum.take(1) |> Enum.empty?() 71 | rescue 72 | error -> 73 | msg = """ 74 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 75 | | | 76 | [1] false 77 | """ 78 | ExUnit.Assertions.assert error.message <> "\n" == msg 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/power_assert_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PowerAssertTest do 2 | use PowerAssert 3 | 4 | test "expr" do 5 | import List 6 | assert ~w(hoge fuga) == ["hoge", "fuga"] 7 | x = "fuga" 8 | assert "hoge#{x}fuga" == "hogefugafuga" 9 | _one = "aiueo" 10 | two = 2 11 | assert [_one] = [two] 12 | assert match?(_x, "fuga") 13 | keywords = [value: [value: "hoge"]] 14 | assert keywords[:value][:value] == "hoge" 15 | assert fn(x) -> x == 1 end.(1) 16 | assert __ENV__.aliases |> Kernel.==([]) 17 | assert [1,2] |> first() |> Kernel.==(1) 18 | assert self() |> Kernel.==(self()) 19 | assert [1,2,3] |> Enum.take(1) |> List.delete(1) |> Enum.empty? 20 | end 21 | 22 | test "raise" do 23 | try do 24 | assert [1,2,3] |> Enum.take(1) |> Enum.empty?() 25 | ExUnit.Assertions.assert false, "should not reach" 26 | rescue 27 | error -> 28 | msg = """ 29 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 30 | | | 31 | [1] false 32 | """ 33 | 34 | if error.message <> "\n" != msg do 35 | value = false 36 | ExUnit.Assertions.assert value 37 | end 38 | end 39 | end 40 | 41 | defmacrop assert_ok(arg) do 42 | quote do 43 | assert {:ok, val} = {:ok, unquote(arg)} 44 | end 45 | end 46 | 47 | test "assert inside macro" do 48 | assert_ok 42 49 | end 50 | end 51 | 52 | defmodule PowerAssertAssertionTest do 53 | use ExUnit.Case 54 | 55 | require PowerAssert.Assertion 56 | alias PowerAssert.Assertion 57 | 58 | test "rendering" do 59 | expect = """ 60 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 61 | | | 62 | [1] false 63 | """ 64 | assert_helper(expect, fn () -> 65 | Assertion.assert [1,2,3] |> Enum.take(1) |> Enum.empty?() 66 | end) 67 | end 68 | 69 | test "assignment success" do 70 | Assertion.assert x = 1 71 | assert x == 1 72 | Assertion.assert %{ hoge: x } = %{ hoge: "hoge" } 73 | assert x == "hoge" 74 | Assertion.assert x = %{ hoge: "hoge" } 75 | assert x == %{ hoge: "hoge" } 76 | end 77 | 78 | test "assignment failed" do 79 | expect = """ 80 | [false] = [x] 81 | | 82 | "hoge" 83 | """ 84 | assert_helper(expect, fn () -> 85 | x = "hoge" 86 | Assertion.assert [false] = [x] 87 | end) 88 | 89 | expect = """ 90 | %{fuga: _} = x 91 | | 92 | %{hoge: "fuga"} 93 | """ 94 | assert_helper(expect, fn () -> 95 | x = %{ hoge: "fuga" } 96 | Assertion.assert %{ fuga: _ } = x 97 | end) 98 | 99 | expect = """ 100 | "hell" = ["hello", "hoge"] |> Enum.at(0) 101 | | 102 | "hello" 103 | """ 104 | assert_helper(expect, fn () -> 105 | Assertion.assert "hell" = ["hello", "hoge"] |> Enum.at(0) 106 | end) 107 | 108 | expect = """ 109 | Expected truthy, got nil 110 | 111 | nil = [nil, "hoge"] |> Enum.at(0) 112 | | 113 | nil 114 | """ 115 | assert_helper(expect, fn () -> 116 | Assertion.assert nil = [nil, "hoge"] |> Enum.at(0) 117 | end) 118 | end 119 | 120 | test "with message" do 121 | expect = """ 122 | failed with message 123 | 124 | [false] |> List.first() 125 | | 126 | false 127 | """ 128 | assert_helper(expect, fn () -> 129 | Assertion.assert [false] |> List.first(), "failed with message" 130 | end) 131 | end 132 | 133 | test "tuple expr" do 134 | expect = """ 135 | {x, :hoge} == {"x", :hoge} 136 | | 137 | "hoge" 138 | """ 139 | assert_helper(expect, fn () -> 140 | x = "hoge" 141 | Assertion.assert {x, :hoge} == {"x", :hoge} 142 | end) 143 | 144 | expect = """ 145 | {x, :hoge, :fuga} == {"x", :hoge, :fuga} 146 | | 147 | "hoge" 148 | """ 149 | assert_helper(expect, fn () -> 150 | x = "hoge" 151 | Assertion.assert {x, :hoge, :fuga} == {"x", :hoge, :fuga} 152 | end) 153 | end 154 | 155 | test "div, rem expr" do 156 | expect = """ 157 | rem(x, y) != 1 158 | | | | 159 | 1 5 2 160 | """ 161 | assert_helper(expect, fn () -> 162 | x = 5 163 | y = 2 164 | Assertion.assert rem(x, y) != 1 165 | end) 166 | 167 | expect = """ 168 | div(x, y) != 2 169 | | | | 170 | 2 5 2 171 | """ 172 | assert_helper(expect, fn () -> 173 | x = 5 174 | y = 2 175 | Assertion.assert div(x, y) != 2 176 | end) 177 | end 178 | 179 | test "string == string" do 180 | expect = """ 181 | hoge == fuga 182 | | | 183 | "hoge" "fuga" 184 | 185 | difference: 186 | hoge 187 | fuga 188 | """ 189 | assert_helper(expect, fn () -> 190 | hoge = "hoge" 191 | fuga = "fuga" 192 | Assertion.assert hoge == fuga 193 | end) 194 | end 195 | 196 | test "string == number" do 197 | expect = """ 198 | hoge == piyo 199 | | | 200 | "hoge" 4 201 | """ 202 | assert_helper(expect, fn () -> 203 | hoge = "hoge" 204 | piyo = 4 205 | Assertion.assert hoge == piyo 206 | end) 207 | end 208 | 209 | test "number" do 210 | expect = """ 211 | 3 == piyo 212 | | 213 | 4 214 | """ 215 | assert_helper(expect, fn () -> 216 | piyo = 4 217 | Assertion.assert 3 == piyo 218 | end) 219 | end 220 | 221 | test "!= expr" do 222 | expect = """ 223 | hoge != piyo 224 | | | 225 | 4 4 226 | """ 227 | assert_helper(expect, fn () -> 228 | hoge = 4 229 | piyo = 4 230 | Assertion.assert hoge != piyo 231 | end) 232 | end 233 | 234 | test "array expr" do 235 | expect = """ 236 | ary1 == ary2 237 | | | 238 | | ["hoge"] 239 | ["hoge", "fuga"] 240 | 241 | only in lhs: ["fuga"] 242 | only in rhs: [] 243 | """ 244 | assert_helper(expect, fn () -> 245 | ary1 = ["hoge", "fuga"] 246 | ary2 = ["hoge"] 247 | Assertion.assert ary1 == ary2 248 | end) 249 | 250 | expect = """ 251 | [1, 2, 3] == [2, 3, 4] 252 | 253 | only in lhs: [1] 254 | only in rhs: [4] 255 | """ 256 | assert_helper(expect, fn () -> 257 | Assertion.assert [1, 2, 3] == [2, 3, 4] 258 | end) 259 | end 260 | 261 | test "array with pipe expr" do 262 | expect = """ 263 | ary1 |> Enum.count() == ary2 |> Enum.count() 264 | | | | | 265 | | 2 ["hoge"] 1 266 | ["hoge", "fuga"] 267 | """ 268 | assert_helper(expect, fn() -> 269 | ary1 = ["hoge", "fuga"] 270 | ary2 = ["hoge"] 271 | Assertion.assert ary1 |> Enum.count() == ary2 |> Enum.count() 272 | end) 273 | end 274 | 275 | test "&& expr" do 276 | num = :rand.uniform(3) + 13 # avoid "this check/guard will always yield the same result" 277 | expect = """ 278 | 5 < num && num < 13 279 | | | 280 | #{num} #{num} 281 | """ 282 | assert_helper(expect, fn () -> 283 | Assertion.assert 5 < num && num < 13 284 | end) 285 | end 286 | 287 | test "&& expr first" do 288 | expect = """ 289 | 5 < num && num < 13 290 | | 291 | 4 292 | """ 293 | assert_helper(expect, fn () -> 294 | num = 4 295 | Assertion.assert 5 < num && num < 13 296 | end) 297 | end 298 | 299 | test "|| expr" do 300 | expect = """ 301 | num < 5 || 13 < num 302 | | | 303 | 10 10 304 | """ 305 | assert_helper(expect, fn () -> 306 | num = 10 307 | Assertion.assert num < 5 || 13 < num 308 | end) 309 | end 310 | 311 | test "map expr" do 312 | expect = expectation_by_version("1.10.0", %{ 313 | earlier: """ 314 | map.value() 315 | | | 316 | | false 317 | %{value: false} 318 | """, 319 | later: """ 320 | map.value 321 | | | 322 | | false 323 | %{value: false} 324 | """ 325 | }) 326 | assert_helper(expect, fn () -> 327 | map = %{value: false} 328 | Assertion.assert map.value 329 | end) 330 | 331 | expect = """ 332 | map == %{value: "hoge"} 333 | | 334 | %{value: "fuga"} 335 | 336 | difference: 337 | key :value => {"fuga", "hoge"} 338 | """ 339 | assert_helper(expect, fn () -> 340 | map = %{value: "fuga"} 341 | Assertion.assert map == %{value: "hoge"} 342 | end) 343 | end 344 | 345 | test "nested map expr" do 346 | expect = expectation_by_version("1.10.0", %{ 347 | earlier: """ 348 | map.value().value() 349 | | | | 350 | | | false 351 | | %{value: false} 352 | %{value: %{value: false}} 353 | """, 354 | later: """ 355 | map.value.value 356 | | | | 357 | | | false 358 | | %{value: false} 359 | %{value: %{value: false}} 360 | """ 361 | }) 362 | assert_helper(expect, fn () -> 363 | map = %{value: %{value: false}} 364 | Assertion.assert map.value.value 365 | end) 366 | end 367 | 368 | test "keywords expr" do 369 | expect = """ 370 | keywords[:value] 371 | | | 372 | | false 373 | [value: false] 374 | """ 375 | assert_helper(expect, fn () -> 376 | keywords = [value: false] 377 | Assertion.assert keywords[:value] 378 | end) 379 | 380 | expect = """ 381 | keywords == [value: "hoge"] 382 | | 383 | [value: "fuga"] 384 | 385 | only in lhs: [value: "fuga"] 386 | only in rhs: [value: "hoge"] 387 | """ 388 | assert_helper(expect, fn () -> 389 | keywords = [value: "fuga"] 390 | Assertion.assert keywords == [value: "hoge"] 391 | end) 392 | end 393 | 394 | test "| operator" do 395 | expect = """ 396 | %{map | hoge: x} == %{hoge: "hoge", fuga: "fuga"} 397 | | | 398 | | "x" 399 | %{fuga: "fuga", hoge: "hoge"} 400 | 401 | difference: 402 | key :hoge => {"x", "hoge"} 403 | """ 404 | assert_helper(expect, fn () -> 405 | x = "x" 406 | map = %{hoge: "hoge", fuga: "fuga"} 407 | Assertion.assert %{map | hoge: x} == %{hoge: "hoge", fuga: "fuga"} 408 | end) 409 | 410 | expect = """ 411 | [h | t] == [1, 2, 3, 4] 412 | | | 413 | 1 [2, 3] 414 | 415 | only in lhs: [] 416 | only in rhs: [4] 417 | """ 418 | assert_helper(expect, fn () -> 419 | h = 1 420 | t = [2, 3] 421 | Assertion.assert [h|t] == [1,2,3,4] 422 | end) 423 | end 424 | 425 | test "nested keywords expr" do 426 | expect = """ 427 | keywords[:value][:value] 428 | | | | 429 | | | false 430 | | [value: false] 431 | [value: [value: false]] 432 | """ 433 | assert_helper(expect, fn () -> 434 | keywords = [value: [value: false]] 435 | Assertion.assert keywords[:value][:value] 436 | end) 437 | end 438 | 439 | test "! expr" do 440 | expect = """ 441 | !truth 442 | || 443 | |true 444 | false 445 | """ 446 | assert_helper(expect, fn () -> 447 | truth = true 448 | Assertion.assert !truth 449 | end) 450 | end 451 | 452 | test "only literal expr" do 453 | expect = """ 454 | false 455 | """ 456 | assert_helper(expect, fn () -> 457 | Assertion.assert false 458 | end) 459 | end 460 | 461 | test "func expr" do 462 | expect_str = """ 463 | func.() 464 | | | 465 | | false 466 | #Function< 467 | """ |> String.trim 468 | expect = ~r/#{Regex.escape(expect_str)}/ 469 | assert_helper(expect, fn () -> 470 | func = fn () -> false end 471 | Assertion.assert func.() 472 | end) 473 | end 474 | 475 | test "func with an one argument expr" do 476 | expect = """ 477 | func.(value) 478 | | || 479 | | |false 480 | | false 481 | #Function< 482 | """ |> String.trim 483 | expect = ~r/#{Regex.escape(expect)}/ 484 | assert_helper(expect, fn () -> 485 | value = false 486 | func = fn (v) -> v end 487 | Assertion.assert func.(value) 488 | end) 489 | end 490 | 491 | test "func with arguments expr" do 492 | expect = """ 493 | func.(value1, value2) 494 | | || | 495 | | |"hoge" "fuga" 496 | | false 497 | #Function< 498 | """ |> String.trim 499 | expect = ~r/#{Regex.escape(expect)}/ 500 | assert_helper(expect, fn () -> 501 | value1 = "hoge" 502 | value2 = "fuga" 503 | func = fn (v1, v2) -> v1 == v2 end 504 | Assertion.assert func.(value1, value2) 505 | end) 506 | end 507 | 508 | test "compare funcs expr" do 509 | expect1 = """ 510 | sum.(one, two) == sum.(three, one) 511 | | || | | || | 512 | | || | | |3 1 513 | | || | | 4 514 | | |1 2 #Function< 515 | """ |> String.trim 516 | expect2 = """ 517 | > 518 | | 3 519 | #Function< 520 | """ |> String.trim 521 | expect = ~r/#{Regex.escape(expect1)}.*#{Regex.escape(expect2)}/ 522 | assert_helper(expect, fn () -> 523 | sum = fn (x, y) -> x + y end 524 | one = 1 525 | two = 2 526 | three = 3 527 | Assertion.assert sum.(one, two) == sum.(three, one) 528 | end) 529 | end 530 | 531 | test "* expr" do 532 | expect = """ 533 | one * two * three == 7 534 | | | | | | 535 | 1 2 2 6 3 536 | """ 537 | assert_helper(expect, fn () -> 538 | one = 1 539 | two = 2 540 | three = 3 541 | Assertion.assert one * two * three == 7 542 | end) 543 | end 544 | 545 | test "imported function expr" do 546 | expect = """ 547 | first([false, 2, 3]) 548 | | 549 | false 550 | """ 551 | assert_helper(expect, fn () -> 552 | import List 553 | Assertion.assert first([false,2,3]) 554 | end) 555 | end 556 | 557 | test "imported function with pipe expr" do 558 | expect = """ 559 | [false, 2] |> first() 560 | | 561 | false 562 | """ 563 | assert_helper(expect, fn () -> 564 | import List 565 | Assertion.assert [false, 2] |> first() 566 | end) 567 | end 568 | 569 | test "imported function without parentheses with pipe expr" do 570 | expect = """ 571 | [false, 2] |> first 572 | | 573 | false 574 | """ 575 | assert_helper(expect, fn () -> 576 | import List 577 | Assertion.assert [false, 2] |> first 578 | end) 579 | end 580 | 581 | test "operators expr" do 582 | expect = """ 583 | x > y 584 | | | 585 | 1 2 586 | """ 587 | assert_helper(expect, fn () -> 588 | x = 1 589 | y = 2 590 | Assertion.assert x > y 591 | end) 592 | 593 | expect = """ 594 | x < y 595 | | | 596 | 2 1 597 | """ 598 | assert_helper(expect, fn () -> 599 | x = 2 600 | y = 1 601 | Assertion.assert x < y 602 | end) 603 | 604 | expect = """ 605 | x >= y 606 | | | 607 | 1 2 608 | """ 609 | assert_helper(expect, fn () -> 610 | x = 1 611 | y = 2 612 | Assertion.assert x >= y 613 | end) 614 | 615 | expect = """ 616 | x <= y 617 | | | 618 | 2 1 619 | """ 620 | assert_helper(expect, fn () -> 621 | x = 2 622 | y = 1 623 | Assertion.assert x <= y 624 | end) 625 | 626 | expect = """ 627 | x == y 628 | | | 629 | 2 1 630 | """ 631 | assert_helper(expect, fn () -> 632 | x = 2 633 | y = 1 634 | Assertion.assert x == y 635 | end) 636 | 637 | expect = """ 638 | x != x 639 | | | 640 | 2 2 641 | """ 642 | assert_helper(expect, fn () -> 643 | x = 2 644 | Assertion.assert x != x 645 | end) 646 | 647 | expect = """ 648 | x || y 649 | | | 650 | | false 651 | false 652 | """ 653 | assert_helper(expect, fn () -> 654 | x = false 655 | y = false 656 | Assertion.assert x || y 657 | end) 658 | 659 | expect = """ 660 | x && y 661 | | | 662 | true false 663 | """ 664 | assert_helper(expect, fn () -> 665 | # avoid "this check/guard will always yield the same result" 666 | x = !!:rand.uniform(1) 667 | y = !:rand.uniform(1) 668 | Assertion.assert x && y 669 | end) 670 | 671 | expect = """ 672 | x <> y == "hoge" 673 | | | 674 | "fu" "ga" 675 | 676 | difference: 677 | fuga 678 | hoge 679 | """ 680 | assert_helper(expect, fn () -> 681 | x = "fu" 682 | y = "ga" 683 | Assertion.assert x <> y == "hoge" 684 | end) 685 | 686 | expect = """ 687 | x === y 688 | | | 689 | 1 1.0 690 | """ 691 | assert_helper(expect, fn () -> 692 | x = 1 693 | y = 1.0 694 | Assertion.assert x === y 695 | end) 696 | 697 | expect = """ 698 | x !== y 699 | | | 700 | 1 1 701 | """ 702 | assert_helper(expect, fn () -> 703 | x = 1 704 | y = 1 705 | Assertion.assert x !== y 706 | end) 707 | 708 | expect = """ 709 | x and y 710 | | | 711 | true false 712 | """ 713 | assert_helper(expect, fn () -> 714 | x = true 715 | y = false 716 | Assertion.assert x and y 717 | end) 718 | 719 | expect = """ 720 | x or y 721 | | | 722 | | false 723 | false 724 | """ 725 | assert_helper(expect, fn () -> 726 | x = false 727 | y = false 728 | Assertion.assert x or y 729 | end) 730 | 731 | expect = """ 732 | x =~ y 733 | | | 734 | | ~r/e/ 735 | "abcd" 736 | """ 737 | assert_helper(expect, fn () -> 738 | x = "abcd" 739 | y = ~r/e/ 740 | Assertion.assert x =~ y 741 | end) 742 | 743 | end 744 | 745 | test "arithmetic ops expr" do 746 | expect = """ 747 | x * y == a + b 748 | | | | | | | 749 | 2 6 3 2 5 3 750 | """ 751 | assert_helper(expect, fn () -> 752 | x = 2 753 | y = 3 754 | a = 2 755 | b = 3 756 | Assertion.assert x * y == a + b 757 | end) 758 | 759 | expect = """ 760 | x / y == a - b 761 | | | | | | | 762 | | | 2 6 4 2 763 | 6 3.0 764 | """ 765 | assert_helper(expect, fn () -> 766 | x = 6 767 | y = 2 768 | a = 6 769 | b = 2 770 | Assertion.assert x / y == a - b 771 | end) 772 | 773 | expect = """ 774 | x ++ y == a -- b 775 | | | | | | | 776 | | | | | | [1] 777 | | | | | [2, 3] 778 | | | [4] [1, 2, 3] 779 | | [1, 2, 3, 4] 780 | [1, 2, 3] 781 | 782 | only in lhs: [1, 4] 783 | only in rhs: [] 784 | """ 785 | assert_helper(expect, fn () -> 786 | x = [1, 2, 3] 787 | y = [4] 788 | a = [1, 2, 3] 789 | b = [1] 790 | Assertion.assert x ++ y == a -- b 791 | end) 792 | end 793 | 794 | test "unary ops expr" do 795 | expect = """ 796 | -x == +y 797 | || || 798 | || |-1 799 | |-1 -1 800 | 1 801 | """ 802 | assert_helper(expect, fn () -> 803 | x = -1 804 | y = -1 805 | Assertion.assert -x == +y 806 | end) 807 | 808 | expect = expectation_by_version("1.13.0", %{ 809 | earlier: """ 810 | not(x) 811 | | | 812 | | true 813 | false 814 | """, 815 | later: """ 816 | not x 817 | | | 818 | | true 819 | false 820 | """ 821 | }) 822 | assert_helper(expect, fn () -> 823 | x = true 824 | Assertion.assert not x 825 | end) 826 | 827 | expect = """ 828 | !x 829 | || 830 | |true 831 | false 832 | """ 833 | assert_helper(expect, fn () -> 834 | x = true 835 | Assertion.assert !x 836 | end) 837 | end 838 | 839 | defmodule TestStruct do 840 | defstruct value: "hoge" 841 | end 842 | 843 | test "struct expr" do 844 | expect = """ 845 | x == %TestStruct{value: "fuga"} 846 | | 847 | %PowerAssertAssertionTest.TestStruct{value: "ho"} 848 | 849 | difference: 850 | key :value => {"ho", "fuga"} 851 | """ 852 | assert_helper(expect, fn () -> 853 | x = %TestStruct{value: "ho"} 854 | Assertion.assert x == %TestStruct{value: "fuga"} 855 | end) 856 | end 857 | 858 | test "block expr" do 859 | expect = """ 860 | true == (x == y) 861 | | | 862 | true false 863 | """ 864 | assert_helper(expect, fn () -> 865 | x = true; y = false 866 | Assertion.assert true == (x == y) 867 | end) 868 | end 869 | 870 | @test_module_attr [1, 2, 3] 871 | test "module attribute expr" do 872 | expect = """ 873 | @test_module_attr |> Enum.at(2) == x 874 | | | | 875 | [1, 2, 3] 3 5 876 | """ 877 | assert_helper(expect, fn () -> 878 | x = 5 879 | Assertion.assert @test_module_attr |> Enum.at(2) == x 880 | end) 881 | end 882 | 883 | test "fn expr not supported" do 884 | expect = """ 885 | (fn x -> x == 1 end).(y) 886 | || 887 | |2 888 | false 889 | """ 890 | assert_helper(expect, fn () -> 891 | y = 2 892 | Assertion.assert fn(x) -> x == 1 end.(y) 893 | end) 894 | 895 | expect = """ 896 | Enum.map(array, fn x -> x == 1 end) |> List.first() 897 | | | | 898 | | [2, 3] false 899 | [false, false] 900 | """ 901 | assert_helper(expect, fn () -> 902 | array = [2, 3] 903 | Assertion.assert Enum.map(array, fn(x) -> x == 1 end) |> List.first() 904 | end) 905 | 906 | # partials 907 | expect = """ 908 | Enum.map(array, &(&1 == 1)) |> List.first() 909 | | | | 910 | | [2, 3] false 911 | [false, false] 912 | """ 913 | assert_helper(expect, fn () -> 914 | array = [2, 3] 915 | Assertion.assert Enum.map(array, &(&1 == 1)) |> List.first() 916 | end) 917 | end 918 | 919 | test "= expr not supported" do 920 | expect = """ 921 | List.first(_x = array) 922 | | 923 | false 924 | """ 925 | assert_helper(expect, fn () -> 926 | array = [false, true] 927 | Assertion.assert List.first(_x = array) 928 | end) 929 | end 930 | 931 | test "string interpolation not supported" do 932 | expect = """ 933 | "hoge" == "f\#{x}a" 934 | | 935 | "fuga" 936 | 937 | difference: 938 | hoge 939 | fuga 940 | """ 941 | assert_helper(expect, fn () -> 942 | x = "ug" 943 | Assertion.assert "hoge" == "f#{x}a" 944 | end) 945 | end 946 | 947 | test ":: expr not supported" do 948 | expect = """ 949 | "hoge" == <<"f", Kernel.to_string(x)::binary, "a">> 950 | | 951 | "fuga" 952 | 953 | difference: 954 | hoge 955 | fuga 956 | """ 957 | assert_helper(expect, fn () -> 958 | x = "ug" 959 | Assertion.assert "hoge" == <<"f", Kernel.to_string(x)::binary, "a">> 960 | end) 961 | end 962 | 963 | test "sigil expr not supported" do 964 | expect = expectation_by_version("1.10.0", %{ 965 | earlier: """ 966 | ~w"hoge fuga \#{x}" == y 967 | | 968 | ["hoge", "fuga"] 969 | 970 | only in lhs: ["nya"] 971 | only in rhs: [] 972 | """, 973 | later: """ 974 | ~w(hoge fuga \#{x}) == y 975 | | 976 | ["hoge", "fuga"] 977 | 978 | only in lhs: ["nya"] 979 | only in rhs: [] 980 | """ 981 | }) 982 | assert_helper(expect, fn () -> 983 | x = "nya" 984 | y = ["hoge", "fuga"] 985 | Assertion.assert ~w(hoge fuga #{x}) == y 986 | end) 987 | end 988 | 989 | @opts [context: Elixir] 990 | test "quote expr not supported" do 991 | expect = expectation_by_version("1.13.0", %{ 992 | earlier: """ 993 | quote(@opts) do 994 | :hoge 995 | end == :fuga 996 | | 997 | :hoge 998 | """, 999 | later: """ 1000 | quote @opts do 1001 | :hoge 1002 | end == :fuga 1003 | | 1004 | :hoge 1005 | """ 1006 | }) 1007 | assert_helper(expect, fn () -> 1008 | Assertion.assert quote(@opts, do: :hoge) == :fuga 1009 | end) 1010 | 1011 | expect = """ 1012 | quote do 1013 | unquote(x) 1014 | end == :fuga 1015 | | 1016 | :hoge 1017 | """ 1018 | assert_helper(expect, fn () -> 1019 | x = :hoge 1020 | Assertion.assert quote(do: unquote(x)) == :fuga 1021 | end) 1022 | end 1023 | 1024 | test "get_and_update_in/2, put_in/2 and update_in/2 expr are not supported" do 1025 | expect = """ 1026 | put_in(users["john"][:age], 28) == %{"john" => %{age: 27}} 1027 | | 1028 | %{"john" => %{age: 28}} 1029 | 1030 | difference: 1031 | key "john" => {%{age: 28}, %{age: 27}} 1032 | """ 1033 | assert_helper(expect, fn () -> 1034 | users = %{"john" => %{age: 27}} 1035 | Assertion.assert put_in(users["john"][:age], 28) == %{"john" => %{age: 27}} 1036 | end) 1037 | 1038 | expect = """ 1039 | update_in(users["john"][:age], &(&1 + 1)) == %{"john" => %{age: 27}} 1040 | | 1041 | %{"john" => %{age: 28}} 1042 | 1043 | difference: 1044 | key "john" => {%{age: 28}, %{age: 27}} 1045 | """ 1046 | assert_helper(expect, fn () -> 1047 | users = %{"john" => %{age: 27}} 1048 | Assertion.assert update_in(users["john"][:age], &(&1 + 1)) == %{"john" => %{age: 27}} 1049 | end) 1050 | 1051 | expect = expectation_by_version("1.13.0", %{ 1052 | earlier: """ 1053 | get_and_update_in(users["john"].age(), &({&1, &1 + 1})) == {27, %{"john" => %{age: 27}}} 1054 | | 1055 | {27, %{"john" => %{age: 28}}} 1056 | """, 1057 | later: """ 1058 | get_and_update_in(users["john"].age(), &{&1, &1 + 1}) == {27, %{"john" => %{age: 27}}} 1059 | | 1060 | {27, %{"john" => %{age: 28}}} 1061 | """ 1062 | }) 1063 | assert_helper(expect, fn () -> 1064 | users = %{"john" => %{age: 27}} 1065 | Assertion.assert get_and_update_in(users["john"].age(), &{&1, &1 + 1}) == {27, %{"john" => %{age: 27}}} 1066 | end) 1067 | end 1068 | 1069 | test "for expr not supported" do 1070 | expect = expectation_by_version("1.13.0", %{ 1071 | earlier: """ 1072 | for(x <- enum) do 1073 | x * 2 1074 | end == [2, 4, 6] 1075 | | 1076 | [2, 4, 8] 1077 | 1078 | only in lhs: '\\b' 1079 | only in rhs: [6] 1080 | """, 1081 | later: """ 1082 | for x <- enum do 1083 | x * 2 1084 | end == [2, 4, 6] 1085 | | 1086 | [2, 4, 8] 1087 | 1088 | only in lhs: '\\b' 1089 | only in rhs: [6] 1090 | """ 1091 | }) 1092 | assert_helper(expect, fn () -> 1093 | enum = [1,2,4] 1094 | Assertion.assert for(x <- enum, do: x * 2) == [2, 4, 6] 1095 | end) 1096 | end 1097 | 1098 | @hello "hello" 1099 | test ":<<>> expr includes module attribute not supported" do 1100 | expect = """ 1101 | <<@hello, " ", "world">> == "hello world!" 1102 | | 1103 | "hello world" 1104 | 1105 | difference: 1106 | hello world 1107 | hello world! 1108 | """ 1109 | assert_helper(expect, fn () -> 1110 | Assertion.assert <<@hello, " ", "world">> == "hello world!" 1111 | end) 1112 | end 1113 | 1114 | test "case expr not supported" do 1115 | expect = expectation_by_version("1.13.0", %{ 1116 | earlier: """ 1117 | case(x) do 1118 | {:ok, right} -> 1119 | right 1120 | {_left, right} -> 1121 | case(right) do 1122 | {:ok, right} -> 1123 | right 1124 | end 1125 | end == :doing 1126 | | 1127 | :done 1128 | """, 1129 | later: """ 1130 | case x do 1131 | {:ok, right} -> 1132 | right 1133 | 1134 | {_left, right} -> 1135 | case right do 1136 | {:ok, right} -> right 1137 | end 1138 | end == :doing 1139 | | 1140 | :done 1141 | """ 1142 | }) 1143 | assert_helper(expect, fn () -> 1144 | x = {:error, {:ok, :done}} 1145 | Assertion.assert (case x do 1146 | {:ok, right} -> 1147 | right 1148 | {_left, right} -> 1149 | case right do 1150 | {:ok, right} -> right 1151 | end 1152 | end) == :doing 1153 | end) 1154 | end 1155 | 1156 | test "__VAR__ expr" do 1157 | expect = """ 1158 | module != __MODULE__ 1159 | | | 1160 | | PowerAssertAssertionTest 1161 | PowerAssertAssertionTest 1162 | """ 1163 | assert_helper(expect, fn () -> 1164 | module = __ENV__.module 1165 | Assertion.assert module != __MODULE__ 1166 | end) 1167 | end 1168 | 1169 | test "big map" do 1170 | expect = expectation_by_version("1.13.0", %{ 1171 | earlier: """ 1172 | big_map == %{:hoge => "value", "value" => "hoge", ["fuga"] => [], %{hoge: :hoge} => %{}, :big => "big", :middle => "middle", :small => "small"} 1173 | | 1174 | %{:big => "big", :hoge => "value", :moga => "moga", :small => "small", %{hoge: :hoge} => %{}, ["fuga"] => [], "value" => "hoe"} 1175 | 1176 | only in lhs: %{moga: "moga"} 1177 | only in rhs: %{middle: "middle"} 1178 | difference: 1179 | key "value" => {"hoe", "hoge"} 1180 | """, 1181 | later: """ 1182 | big_map == %{ 1183 | :hoge => "value", 1184 | "value" => "hoge", 1185 | ["fuga"] => [], 1186 | %{hoge: :hoge} => %{}, 1187 | big: "big", 1188 | middle: "middle", 1189 | small: "small" 1190 | } 1191 | | 1192 | %{:big => "big", :hoge => "value", :moga => "moga", :small => "small", %{hoge: :hoge} => %{}, ["fuga"] => [], "value" => "hoe"} 1193 | 1194 | only in lhs: %{moga: "moga"} 1195 | only in rhs: %{middle: "middle"} 1196 | difference: 1197 | key "value" => {"hoe", "hoge"} 1198 | """ 1199 | }) 1200 | assert_helper(expect, fn () -> 1201 | big_map = %{:hoge => "value", "value" => "hoe", ["fuga"] => [], %{hoge: :hoge} => %{}, :big => "big", :small => "small", moga: "moga"} 1202 | Assertion.assert big_map == %{:hoge => "value", "value" => "hoge", ["fuga"] => [], %{hoge: :hoge} => %{}, :big => "big", :middle => "middle", :small => "small"} 1203 | end) 1204 | end 1205 | 1206 | def assert_helper(expect, func) when is_binary(expect) do 1207 | expect = String.trim(expect) 1208 | try do 1209 | func.() 1210 | assert false, "should be failed test #{expect}" 1211 | rescue 1212 | error -> 1213 | assert expect == error.message 1214 | end 1215 | end 1216 | def assert_helper(expect, func) do 1217 | try do 1218 | func.() 1219 | assert false, "should be failed test #{expect}" 1220 | rescue 1221 | error -> 1222 | assert Regex.match?(expect, error.message) 1223 | end 1224 | end 1225 | 1226 | def expectation_by_version(version, %{earlier: expect_earlier, later: expect_later}) do 1227 | case Version.compare(System.version(), version) do 1228 | :lt -> expect_earlier 1229 | _ -> expect_later 1230 | end 1231 | end 1232 | 1233 | end 1234 | -------------------------------------------------------------------------------- /test/power_assert_use_ex_unit_test.exs: -------------------------------------------------------------------------------- 1 | # when already `use ExUnit.Case` 2 | # for instance other framework depending on ExUnit such as `ExSpec` 3 | defmodule PowerAssertUseExUnitTest do 4 | use ExUnit.Case 5 | use PowerAssert, use_ex_unit: true 6 | 7 | test "expr" do 8 | import List 9 | power_assert ~w(hoge fuga) == ["hoge", "fuga"] 10 | x = "fuga" 11 | power_assert "hoge#{x}fuga" == "hogefugafuga" 12 | _one = "aiueo" 13 | two = 2 14 | power_assert [_one] = [two] 15 | power_assert match?(_x, "fuga") 16 | keywords = [value: [value: "hoge"]] 17 | power_assert keywords[:value][:value] == "hoge" 18 | power_assert fn(x) -> x == 1 end.(1) 19 | power_assert __ENV__.aliases |> Kernel.==([]) 20 | power_assert [1,2] |> first() |> Kernel.==(1) 21 | power_assert self() |> Kernel.==(self()) 22 | power_assert [1,2,3] |> Enum.take(1) |> List.delete(1) |> Enum.empty? 23 | end 24 | 25 | test "raise" do 26 | try do 27 | power_assert [1,2,3] |> Enum.take(1) |> Enum.empty?() 28 | assert false, "should not reach" 29 | rescue 30 | error -> 31 | msg = """ 32 | [1, 2, 3] |> Enum.take(1) |> Enum.empty?() 33 | | | 34 | [1] false 35 | """ 36 | 37 | if error.message <> "\n" != msg do 38 | value = false 39 | assert value 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------