From 87c3bcf4140aab52d6734f484ca86ff9055b987b Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 20:17:59 +0900 Subject: [PATCH 1/8] test: guard in assert_match/2 --- test/assert_match_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/assert_match_test.exs b/test/assert_match_test.exs index a1fca08..52e70d7 100644 --- a/test/assert_match_test.exs +++ b/test/assert_match_test.exs @@ -27,6 +27,7 @@ defmodule AssertMatchTest do assert_match("prefix match", "prefix" <> _) assert_match(%{key: 1}, %{}) assert_match(%{key: 1}, %{key: _}) + assert_match(%{key: 1}, map when is_map_key(map, :key)) assert_match([1, 2], [_ | _]) end From abdc0ac99a946667999c799a3ba51d1dd182a6e1 Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 20:18:13 +0900 Subject: [PATCH 2/8] chore: use mise and direnv for development --- .envrc | 3 +++ mise.toml | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 .envrc create mode 100644 mise.toml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..600c121 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +direnv_load mise direnv exec diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..be3faf3 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +elixir = "1.17.3-otp-27" +erlang = "27.2.1" From 51381e8d31cbd962326b0039a304938ce2b84be7 Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 20:18:37 +0900 Subject: [PATCH 3/8] chore: Elixir 1.18 drops ~R// --- test/assert_match_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/assert_match_test.exs b/test/assert_match_test.exs index 52e70d7..1fd65d0 100644 --- a/test/assert_match_test.exs +++ b/test/assert_match_test.exs @@ -18,7 +18,7 @@ defmodule AssertMatchTest do end test "should work with regex" do - assert_match("prefix match", ~R/prefix/) + assert_match("prefix match", ~r/prefix/) assert_match("prefix match", ~r/#{"prefix"}/) end From 5364f8bd0f5ca27b0e0d58fa7a74bdd35fb4318b Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 20:19:29 +0900 Subject: [PATCH 4/8] ci: drop Erlang 23, support 27 --- .github/workflows/build.yml | 50 +++++++++++++++++------------------ .github/workflows/publish.yml | 4 +-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0082015..f1cfe86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,31 +10,6 @@ permissions: contents: read jobs: - build-otp-23: - name: Build and test on OTP 23 - runs-on: ubuntu-20.04 - strategy: - matrix: - elixir_version: - - "1.10" - - "1.11" - - "1.12" - - "1.13" - steps: - - uses: actions/checkout@v3 - - uses: erlef/setup-beam@v1 - with: - elixir-version: ${{ matrix.elixir_version }} - otp-version: "23" - - uses: actions/cache@v3 - with: - path: deps - key: 23-${{ matrix.elixir_version }}-${{ hashFiles('**/mix.lock') }} - restore-keys: | - 23-${{ matrix.elixir_version }}- - 23- - - run: mix deps.get - - run: mix test build-otp-24: name: Build and test on OTP 24 runs-on: ubuntu-20.04 @@ -92,6 +67,7 @@ jobs: elixir_version: - "1.14" - "1.15" + - "1.16" steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 @@ -107,3 +83,27 @@ jobs: 26- - run: mix deps.get - run: mix test + build-otp-27: + name: Build and test on OTP 27 + runs-on: ubuntu-22.04 + strategy: + matrix: + elixir_version: + - "1.16" + - "1.17" + - "1.18" + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir_version }} + otp-version: "27" + - uses: actions/cache@v3 + with: + path: deps + key: 27-${{ matrix.elixir_version }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + 27-${{ matrix.elixir_version }}- + 27- + - run: mix deps.get + - run: mix test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4dc35d8..e360284 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,8 +16,8 @@ jobs: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: - elixir-version: "1.14" - otp-version: "24" + elixir-version: "1.18" + otp-version: "27" - run: mix deps.get - run: mix test - run: mix hex.publish --yes From 5fbd302781d03d86fb7cedc932b92eb88a4fc30c Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 20:22:49 +0900 Subject: [PATCH 5/8] fix: Elixir 1.16 does not support Erlang 27 --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1cfe86..1135daa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,6 @@ jobs: strategy: matrix: elixir_version: - - "1.16" - "1.17" - "1.18" steps: From 2ca9a87fc4d40e6c8a937f51ea7f52fd579576c7 Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 21:07:52 +0900 Subject: [PATCH 6/8] feat: bump dev env to Elixir 1.18 --- mise.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mise.toml b/mise.toml index be3faf3..60d5dec 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,3 @@ [tools] -elixir = "1.17.3-otp-27" +elixir = "1.18.2-otp-27" erlang = "27.2.1" From ada0198d837444672f1751d64ca33abcbbd7ef79 Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 21:08:44 +0900 Subject: [PATCH 7/8] feat: support guard clauses in Elixir 1.18 --- lib/assert_match.ex | 52 +++++++++++++++++++++++++++++++++----- test/assert_match_test.exs | 8 +++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/lib/assert_match.ex b/lib/assert_match.ex index e271af8..2117615 100644 --- a/lib/assert_match.ex +++ b/lib/assert_match.ex @@ -6,11 +6,19 @@ defmodule AssertMatch do @doc """ Pipe-friendly equality/matching assertion. - Performs: + Performs `assert/1` with: - `=~` for Regex patterns + - `match?/2` for patterns with guards - `=` for other patterns + For the third variant, it can utilize bindings inside the pattern from outside! + + %{key: 1} + |> assert_match(%{key: value}) + + assert value == 1 + ## Extraction of pinned function calls Elixir's `^/1` (pin operator) usually does not allow runtime function calls, @@ -33,7 +41,27 @@ defmodule AssertMatch do }) You cannot nest pinned expressions. See test/assert_match_test.exs for more usages. + + ## Guards + + Guards are supported, but with limitations: if guards are used, bindings inside patterns cannot be used from outside. + + %{key: 1} + |> assert_match(map when is_map(map)) + # => Passes + + assert map == %{key: 1} + # => error: undefined variable "map" + + Related: bindings inside patterns that are NOT used in the guards are warned as unused. + + %{key: 1} + |> assert_match(%{key: value} = map when not is_map_key(map, :nonkey)) + # => warning: variable "value" is unused (if the variable is not meant to be used, prefix it with an underscore) + + This is a relatively new limitation introduced in Elixir 1.18, as a side-effect of [this change](https://github.com/elixir-lang/elixir/pull/13817). """ + @spec assert_match(any, Macro.t()) :: any defmacro assert_match(subject, pattern) do case pattern do falsy when falsy == nil or falsy == false -> @@ -55,11 +83,23 @@ defmodule AssertMatch do {matchable_ast, extracted_tmpvars} = extract_pinned_function_calls_to_variables(pattern) # Notice pattern is at left-hand side # In Elixir 1.10+, match assertions can display rich diffs for various subject-pattern combinations - quote do - right = unquote(subject) - unquote(tmpvar_definitions(extracted_tmpvars)) - ExUnit.Assertions.assert(unquote(matchable_ast) = right) - right + case matchable_ast do + {:when, _location, _branches} = ast_with_guard -> + # After Elixir 1.18, patterns with guards require special treatment. cf. https://github.com/elixir-lang/elixir/pull/13817 + quote do + right = unquote(subject) + unquote(tmpvar_definitions(extracted_tmpvars)) + ExUnit.Assertions.assert(match?(unquote(ast_with_guard), right)) + right + end + + _otherwise -> + quote do + right = unquote(subject) + unquote(tmpvar_definitions(extracted_tmpvars)) + ExUnit.Assertions.assert(unquote(matchable_ast) = right) + right + end end end end diff --git a/test/assert_match_test.exs b/test/assert_match_test.exs index 1fd65d0..0298fee 100644 --- a/test/assert_match_test.exs +++ b/test/assert_match_test.exs @@ -27,10 +27,16 @@ defmodule AssertMatchTest do assert_match("prefix match", "prefix" <> _) assert_match(%{key: 1}, %{}) assert_match(%{key: 1}, %{key: _}) - assert_match(%{key: 1}, map when is_map_key(map, :key)) assert_match([1, 2], [_ | _]) end + test "should work with guards" do + assert_match(%{key: 1}, map when is_map_key(map, :key)) + + # guard works with assert_match/2 but has limitation: bindings inside patterns cannot be used from outside + assert_match(%{key: 1}, %{key: _value} = map when not is_map_key(map, :nonkey)) + end + test "should work with incomplete struct patterns" do # As a value, %Date{year: 2018} raises since [:month, :day] are also enforced. assert_match(~D[2018-01-01], %Date{year: 2018}) From b36496ef4095852fd69aa0e17685ebea170c1a44 Mon Sep 17 00:00:00 2001 From: Yu Matsuzawa Date: Thu, 30 Jan 2025 21:10:06 +0900 Subject: [PATCH 8/8] feat: bump as minor version change due to behavior change --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 46ebb8a..009bc7d 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule AssertMatch.MixProject do app: :assert_match, description: "Leverages pattern matching & pipeline in Elixir tests by pipe-friendly `assert_match/2`", - version: "1.0.0", + version: "1.1.0", elixir: "~> 1.10", package: package(), deps: [{:ex_doc, "~> 0.27", only: :dev, runtime: false}]