Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

direnv_load mise direnv exec
49 changes: 24 additions & 25 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -92,6 +67,7 @@ jobs:
elixir_version:
- "1.14"
- "1.15"
- "1.16"
steps:
- uses: actions/checkout@v3
- uses: erlef/setup-beam@v1
Expand All @@ -107,3 +83,26 @@ 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.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
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 46 additions & 6 deletions lib/assert_match.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ->
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tools]
elixir = "1.18.2-otp-27"
erlang = "27.2.1"
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}]
Expand Down
9 changes: 8 additions & 1 deletion test/assert_match_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,6 +30,13 @@ defmodule AssertMatchTest do
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
Copy link
Contributor

@KyosukeUryu KyosukeUryu Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

= match演算子ではassertionと同時に束縛できたがmatch?/2を利用するmatchだと束縛はできないのは確かに制限事項となりますね 📝

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})
Expand Down