From 910d97ed5b850cdbe986ebb86c02045c13bb69a0 Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Sun, 24 Sep 2023 21:07:20 -1000 Subject: [PATCH] Fix tests on Elixir 1.15 (#335) Starts running tests on Elixir 1.15 with OTP 26 The map ordering change in OTP 26 caused many tests to break because they were implicitly relying on a not guaranteed order. Adds machete to add a sorted_list test helper (instead of changing many assertions to be split across two statements) --- .github/workflows/ci.yml | 2 + mix.exs | 3 +- mix.lock | 1 + .../component/input/text_field_test.exs | 9 +- test/scenic/scene_test.exs | 16 ++- test/scenic/view_port/input_test.exs | 98 +++++++++---------- test/support/assertions.ex | 61 ++++++++++++ test/support/data_case.ex | 11 +++ 8 files changed, 139 insertions(+), 62 deletions(-) create mode 100644 test/support/assertions.ex create mode 100644 test/support/data_case.ex diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e81e2e32..766f3c61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: include: - elixir: '1.11.4' otp: '24.2' + - elixir: '1.15.5' + otp: '26.0' steps: - uses: actions/checkout@v3 diff --git a/mix.exs b/mix.exs index 68165e70..e6ef58c8 100644 --- a/mix.exs +++ b/mix.exs @@ -57,7 +57,8 @@ defmodule Scenic.Mixfile do {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:excoveralls, ">= 0.0.0", only: :test, runtime: false}, {:inch_ex, "~> 2.0", only: [:dev, :docs], runtime: false}, - {:dialyxir, "~> 1.1", only: :dev, runtime: false} + {:dialyxir, "~> 1.1", only: :dev, runtime: false}, + {:machete, "~> 0.2.8", only: :test} ] end diff --git a/mix.lock b/mix.lock index 95a911f0..16d7e9ec 100644 --- a/mix.lock +++ b/mix.lock @@ -16,6 +16,7 @@ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "machete": {:hex, :machete, "0.2.8", "ca7be4d2d65f05f4ab5eac0f5fd339cea08d4d75f775ac6c22991df2baed5d80", [:mix], [], "hexpm", "09bf5b306385d01c877453ead94914a595beecba3f2525c7364ee3ba0630a224"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, diff --git a/test/scenic/component/input/text_field_test.exs b/test/scenic/component/input/text_field_test.exs index 1c96335c..c67605e3 100644 --- a/test/scenic/component/input/text_field_test.exs +++ b/test/scenic/component/input/text_field_test.exs @@ -5,7 +5,9 @@ # defmodule Scenic.Component.Input.TextFieldTest do - use ExUnit.Case, async: false + use Scenic.Test.DataCase, async: false + use Machete + import Scenic.Test.SortedListMatcher doctest Scenic.Component.Input.TextField alias Scenic.Graph @@ -96,7 +98,7 @@ defmodule Scenic.Component.Input.TextFieldTest do assert Input.fetch_captures!(vp) == {:ok, []} Input.send(vp, @press_in) force_sync(vp.pid, pid) - assert Input.fetch_captures!(vp) == {:ok, [:codepoint, :cursor_button, :key]} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([:codepoint, :cursor_button, :key])} Input.send(vp, @cp_k) assert_receive({:fwd_event, {:value_changed, :text_field, "kInitial value"}}, 200) @@ -105,7 +107,8 @@ defmodule Scenic.Component.Input.TextFieldTest do test "press_out releases and ends editing", %{vp: vp, pid: pid} do Input.send(vp, @press_in) force_sync(vp.pid, pid) - assert Input.fetch_captures!(vp) == {:ok, [:codepoint, :cursor_button, :key]} + + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([:codepoint, :cursor_button, :key])} Input.send(vp, @press_out) force_sync(vp.pid, pid) diff --git a/test/scenic/scene_test.exs b/test/scenic/scene_test.exs index 6c8b7dcd..9ef3158b 100644 --- a/test/scenic/scene_test.exs +++ b/test/scenic/scene_test.exs @@ -5,7 +5,7 @@ # defmodule Scenic.SceneTest do - use ExUnit.Case, async: false + use Scenic.Test.DataCase, async: false doctest Scenic.Scene alias Scenic.ViewPort @@ -382,34 +382,32 @@ defmodule Scenic.SceneTest do test "fetch_requests works", %{scene: scene} do Scenic.Scene.request_input(scene, :cursor_button) - assert Scene.fetch_requests(scene) == {:ok, [:cursor_button, :codepoint]} + assert Scene.fetch_requests(scene) ~> {:ok, sorted_list([:cursor_button, :codepoint])} # request input from outside the scene :ok = ViewPort.Input.request(scene.viewport, :cursor_pos) # should only show input form the scene even though this is the caller - assert Scene.fetch_requests(scene) == {:ok, [:cursor_button, :codepoint]} + assert Scene.fetch_requests(scene) ~> {:ok, sorted_list([:cursor_button, :codepoint])} end test "request_input works", %{scene: scene} do Scenic.Scene.request_input(scene, :cursor_button) - assert Scene.fetch_requests(scene) == - {:ok, [:cursor_button, :codepoint]} + assert Scene.fetch_requests(scene) ~> {:ok, sorted_list([:cursor_button, :codepoint])} :ok = Scene.request_input(scene, :cursor_pos) - assert Scene.fetch_requests(scene) == - {:ok, [:cursor_pos, :cursor_button, :codepoint]} + assert Scene.fetch_requests(scene) ~> {:ok, sorted_list([:cursor_pos, :cursor_button, :codepoint])} end test "unrequest_input works", %{scene: scene} do Scenic.Scene.request_input(scene, :cursor_button) - assert Scene.fetch_requests(scene) == {:ok, [:cursor_button, :codepoint]} + assert Scene.fetch_requests(scene) ~> {:ok, sorted_list([:cursor_button, :codepoint])} :ok = Scene.unrequest_input(scene, :codepoint) - assert Scene.fetch_requests(scene) == {:ok, [:cursor_button]} + assert Scene.fetch_requests(scene) ~> {:ok, sorted_list([:cursor_button])} end test "fetch_captures works", %{scene: scene} do diff --git a/test/scenic/view_port/input_test.exs b/test/scenic/view_port/input_test.exs index c9522a94..6bcca099 100644 --- a/test/scenic/view_port/input_test.exs +++ b/test/scenic/view_port/input_test.exs @@ -4,7 +4,7 @@ # defmodule Scenic.ViewPort.InputTest do - use ExUnit.Case, async: false + use Scenic.Test.DataCase, async: false doctest Scenic.ViewPort.Input alias Scenic.ViewPort @@ -46,86 +46,86 @@ defmodule Scenic.ViewPort.InputTest do end test "Test that capture/release/list_captures work", %{vp: vp} do - assert Input.fetch_captures(vp) == {:ok, []} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} :ok = Input.capture(vp, :cursor_pos) - assert Input.fetch_captures(vp) == {:ok, [:cursor_pos]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([:cursor_pos])} :ok = Input.capture(vp, [:key, :codepoint]) - assert Input.fetch_captures(vp) == {:ok, [:key, :cursor_pos, :codepoint]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([:key, :cursor_pos, :codepoint])} :ok = Input.release(vp, :key) - assert Input.fetch_captures(vp) == {:ok, [:cursor_pos, :codepoint]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([:cursor_pos, :codepoint])} :ok = Input.release(vp, :all) - assert Input.fetch_captures(vp) == {:ok, []} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} end test "list_captures and list_captures! work", %{vp: vp} do - assert Input.fetch_captures(vp) == {:ok, []} - assert Input.fetch_captures!(vp) == {:ok, []} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([])} Agent.start(fn -> :ok = Input.capture(vp, [:codepoint]) end) - assert Input.fetch_captures(vp) == {:ok, []} - assert Input.fetch_captures!(vp) == {:ok, [:codepoint]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([:codepoint])} :ok = Input.capture(vp, :cursor_pos) - assert Input.fetch_captures(vp) == {:ok, [:cursor_pos]} - assert Input.fetch_captures!(vp) == {:ok, [:codepoint, :cursor_pos]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([:cursor_pos])} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([:codepoint, :cursor_pos])} end test "captures are cleaned up when the owning process stops", %{vp: vp} do # set up a capture :ok = Input.capture(vp, [:codepoint]) - assert Input.fetch_captures!(vp) == {:ok, [:codepoint]} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([:codepoint])} # fake indicate this process went down Process.send(vp.pid, {:DOWN, make_ref(), :process, self(), :test}, []) - assert Input.fetch_captures!(vp) == {:ok, []} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([])} end test "Test that request/unrequest/list_requests work", %{vp: vp} do - assert Input.fetch_requests(vp) == {:ok, []} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} :ok = Input.request(vp, :cursor_pos) - assert Input.fetch_requests(vp) == {:ok, [:cursor_pos]} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([:cursor_pos])} :ok = Input.request(vp, [:key, :codepoint]) - assert Input.fetch_requests(vp) == {:ok, [:key, :cursor_pos, :codepoint]} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([:key, :cursor_pos, :codepoint])} :ok = Input.unrequest(vp, :key) - assert Input.fetch_requests(vp) == {:ok, [:cursor_pos, :codepoint]} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([:cursor_pos, :codepoint])} :ok = Input.unrequest(vp, :all) - assert Input.fetch_requests(vp) == {:ok, []} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} end test "fetch_requests and fetch_requests! work", %{vp: vp} do - assert Input.fetch_requests(vp) == {:ok, []} - assert Input.fetch_requests!(vp) == {:ok, []} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([])} Agent.start(fn -> :ok = Input.request(vp, [:codepoint]) end) - assert Input.fetch_captures(vp) == {:ok, []} - assert Input.fetch_requests!(vp) == {:ok, [:codepoint]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:codepoint])} :ok = Input.request(vp, :cursor_pos) - assert Input.fetch_requests(vp) == {:ok, [:cursor_pos]} - assert Input.fetch_requests!(vp) == {:ok, [:codepoint, :cursor_pos]} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([:cursor_pos])} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:codepoint, :cursor_pos])} end test "requests are cleaned up with the owning process stops", %{vp: vp, scene: scene} do :ok = Input.request(vp, :cursor_pos) Scenic.Scene.request_input(scene, :codepoint) - assert Input.fetch_requests!(vp) == {:ok, [:codepoint, :cursor_pos]} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:codepoint, :cursor_pos])} Scenic.Scene.stop(scene) - assert Input.fetch_requests!(vp) == {:ok, [:cursor_pos]} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:cursor_pos])} end # ---------------- @@ -135,7 +135,7 @@ defmodule Scenic.ViewPort.InputTest do %{vp: vp, scene: scene} do # make like a driver GenServer.cast(vp.pid, {:register_driver, self()}) - assert Input.fetch_requests!(vp) == {:ok, []} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([])} graph = Scenic.Graph.build() @@ -162,7 +162,7 @@ defmodule Scenic.ViewPort.InputTest do } do # make like a driver GenServer.cast(vp.pid, {:register_driver, self()}) - assert Input.fetch_requests!(vp) == {:ok, []} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([])} graph = Scenic.Graph.build() @@ -179,7 +179,7 @@ defmodule Scenic.ViewPort.InputTest do } do # make like a driver GenServer.cast(vp.pid, {:register_driver, self()}) - assert Input.fetch_requests!(vp) == {:ok, []} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([])} graph = Scenic.Graph.build() @@ -193,7 +193,7 @@ defmodule Scenic.ViewPort.InputTest do test ":cursor_pos only the input listed in a input style is requested", %{vp: vp, scene: scene} do # make like a driver GenServer.cast(vp.pid, {:register_driver, self()}) - assert Input.fetch_requests!(vp) == {:ok, []} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([])} graph = Scenic.Graph.build() @@ -333,11 +333,11 @@ defmodule Scenic.ViewPort.InputTest do # specific input types test "cursor_scroll request works", %{vp: vp} do - assert Input.fetch_captures!(vp) == {:ok, []} - assert Input.fetch_requests(vp) == {:ok, []} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} :ok = Input.request(vp, :cursor_scroll) - assert Input.fetch_requests(vp) == {:ok, [:cursor_scroll]} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([:cursor_scroll])} assert Input.send(vp, {:cursor_scroll, {{1, 2}, {3, 4}}}) == :ok @@ -348,11 +348,11 @@ defmodule Scenic.ViewPort.InputTest do end test "cursor_scroll capture works", %{vp: vp} do - assert Input.fetch_captures(vp) == {:ok, []} - assert Input.fetch_requests(vp) == {:ok, []} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} :ok = Input.capture(vp, :cursor_scroll) - assert Input.fetch_captures(vp) == {:ok, [:cursor_scroll]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([:cursor_scroll])} assert Input.send(vp, {:cursor_scroll, {{1, 2}, {3, 4}}}) == :ok @@ -363,11 +363,11 @@ defmodule Scenic.ViewPort.InputTest do end test "cursor_pos request works", %{vp: vp} do - assert Input.fetch_captures!(vp) == {:ok, []} - assert Input.fetch_requests(vp) == {:ok, []} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} :ok = Input.request(vp, :cursor_pos) - assert Input.fetch_requests(vp) == {:ok, [:cursor_pos]} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([:cursor_pos])} assert Input.send(vp, {:cursor_pos, {1, 2}}) == :ok @@ -378,11 +378,11 @@ defmodule Scenic.ViewPort.InputTest do end test "cursor_pos capture works", %{vp: vp} do - assert Input.fetch_captures(vp) == {:ok, []} - assert Input.fetch_requests(vp) == {:ok, []} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests(vp) ~> {:ok, sorted_list([])} :ok = Input.capture(vp, :cursor_pos) - assert Input.fetch_captures(vp) == {:ok, [:cursor_pos]} + assert Input.fetch_captures(vp) ~> {:ok, sorted_list([:cursor_pos])} assert Input.send(vp, {:cursor_pos, {1, 2}}) == :ok @@ -396,7 +396,7 @@ defmodule Scenic.ViewPort.InputTest do # drivers are sent input updates test "drivers are sent requested input updates", %{vp: vp} do - assert Input.fetch_requests!(vp) == {:ok, []} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([])} :ok = Input.request(vp, :cursor_button) GenServer.cast(vp.pid, {:register_driver, self()}) @@ -418,7 +418,7 @@ defmodule Scenic.ViewPort.InputTest do test "drivers are sent requested input updates when a scene goes down", %{vp: vp, scene: scene} do Scenic.Scene.request_input(scene, :cursor_button) - assert Input.fetch_requests!(vp) == {:ok, [:cursor_button]} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:cursor_button])} GenServer.cast(vp.pid, {:register_driver, self()}) assert_receive({:_request_input_, [:cursor_button]}, 100) @@ -432,8 +432,8 @@ defmodule Scenic.ViewPort.InputTest do test "drivers are sent captured input updates", %{vp: vp, scene: scene} do Scenic.Scene.request_input(scene, :cursor_button) - assert Input.fetch_captures!(vp) == {:ok, []} - assert Input.fetch_requests!(vp) == {:ok, [:cursor_button]} + assert Input.fetch_captures!(vp) ~> {:ok, sorted_list([])} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:cursor_button])} GenServer.cast(vp.pid, {:register_driver, self()}) assert_receive({:_request_input_, [:cursor_button]}, 100) @@ -454,7 +454,7 @@ defmodule Scenic.ViewPort.InputTest do test "drivers are sent captured input updates when a scene goes down", %{vp: vp, scene: scene} do Scenic.Scene.request_input(scene, :cursor_button) - assert Input.fetch_requests!(vp) == {:ok, [:cursor_button]} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:cursor_button])} self = self() # have to have an agent do the capture so that it comes from a different pid than this # test, which is pretending to be a driver... @@ -479,7 +479,7 @@ defmodule Scenic.ViewPort.InputTest do # should get an update when the owning pid goes down # calling fetch_requests! makes sure the vp has processed the agent DOWN message - assert Input.fetch_requests!(vp) == {:ok, [:cursor_button]} + assert Input.fetch_requests!(vp) ~> {:ok, sorted_list([:cursor_button])} assert_receive({:_request_input_, [:cursor_button]}, 100) end diff --git a/test/support/assertions.ex b/test/support/assertions.ex new file mode 100644 index 00000000..eb2d150e --- /dev/null +++ b/test/support/assertions.ex @@ -0,0 +1,61 @@ +defmodule Scenic.Test.Assertions do + import ExUnit.Assertions + + def assert_match_list(list1, list2) do + assert Enum.sort(list1) == Enum.sort(list2) + end +end + +defmodule Scenic.Test.SortedListMatcher do + @moduledoc """ + Defines a matcher that matches falsy values + """ + + import Machete.Mismatch + + defstruct [:values] + + @typedoc """ + Describes an instance of this matcher + """ + @opaque t :: %__MODULE__{} + + @typedoc """ + Describes the arguments that can be passed to this matcher + """ + @type opts :: [] + + @doc """ + Matches against [falsy values](https://hexdocs.pm/elixir/1.12/Kernel.html#module-truthy-and-falsy-values) + + Takes no arguments + + Examples: + + iex> assert false ~> falsy() + true + + iex> assert nil ~> falsy() + true + + iex> refute true ~> falsy() + false + """ + @spec sorted_list(list()) :: t() + def sorted_list(list \\ []) do + struct!(__MODULE__, values: list) + end + + defimpl Machete.Matchable do + def mismatches(%@for{} = a, b) do + list_a = Enum.sort(a.values) + list_b = Enum.sort(b) + + if list_a == list_b do + nil + else + mismatch("#{inspect(list_b)} does not match #{inspect(list_a)}") + end + end + end +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 00000000..461d8e20 --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,11 @@ +defmodule Scenic.Test.DataCase do + use ExUnit.CaseTemplate + + using do + quote do + use Machete + import Scenic.Test.SortedListMatcher + import Scenic.Test.Assertions + end + end +end