Skip to content

Commit 66d31d8

Browse files
committed
Fix OTP 26 CI via json_polyfill, add credo and live integration tests with CI postgres
1 parent 7f6da17 commit 66d31d8

7 files changed

Lines changed: 223 additions & 1 deletion

File tree

.credo.exs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Credo configuration for Localize Ecto.
2+
#
3+
# Policy decisions follow the localize project (July 2026):
4+
#
5+
# * `Design.AliasUsage` is disabled. The localize family deliberately
6+
# fully qualifies many calls because module names such as
7+
# `Localize.List`, `Localize.Date` and `Localize.String` shadow the
8+
# standard library when aliased. The preferred style is to alias
9+
# submodules opportunistically when the trailing segment does NOT
10+
# clash with the stdlib, and never as a bulk conversion.
11+
#
12+
# * `Refactor.Nesting` stays at the default maximum depth of 2:
13+
# multi-clause helper functions with pattern matching are preferred
14+
# over nested case/cond/if.
15+
#
16+
# * `Refactor.CyclomaticComplexity` stays at the default of 9;
17+
# naturally-branchy functions carry inline `credo:disable`
18+
# annotations with a one-line justification instead of a raised
19+
# global limit.
20+
%{
21+
configs: [
22+
%{
23+
name: "default",
24+
strict: true,
25+
files: %{
26+
included: ["lib/", "test/"],
27+
excluded: []
28+
},
29+
checks: %{
30+
disabled: [
31+
{Credo.Check.Design.AliasUsage, []}
32+
]
33+
}
34+
}
35+
]
36+
}

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ on:
88

99
env:
1010
MIX_ENV: test
11+
PGHOST: localhost
12+
PGUSER: postgres
13+
PGPASSWORD: postgres
1114

1215
permissions:
1316
contents: read
@@ -17,6 +20,19 @@ jobs:
1720
name: "Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }}${{ matrix.lint && ' (lint)' || '' }}"
1821
runs-on: ubuntu-latest
1922

23+
services:
24+
postgres:
25+
image: postgres:17
26+
env:
27+
POSTGRES_PASSWORD: postgres
28+
ports:
29+
- 5432:5432
30+
options: >-
31+
--health-cmd pg_isready
32+
--health-interval 10s
33+
--health-timeout 5s
34+
--health-retries 5
35+
2036
strategy:
2137
fail-fast: false
2238
matrix:
@@ -77,6 +93,10 @@ jobs:
7793
if: matrix.lint
7894
run: mix format --check-formatted
7995

96+
- name: Run credo
97+
if: matrix.lint
98+
run: mix credo --strict
99+
80100
- name: Compile (warnings as errors)
81101
run: mix compile --warnings-as-errors
82102

mix.exs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule LocalizeEcto.MixProject do
1111
name: "Localize Ecto",
1212
source_url: @source_url,
1313
elixir: "~> 1.17",
14+
elixirc_paths: elixirc_paths(Mix.env()),
1415
start_permanent: Mix.env() == :prod,
1516
deps: deps(),
1617
description: description(),
@@ -68,14 +69,32 @@ defmodule LocalizeEcto.MixProject do
6869
]
6970
end
7071

72+
defp elixirc_paths(:test), do: ["lib", "test/support"]
73+
defp elixirc_paths(_), do: ["lib"]
74+
7175
defp deps do
7276
[
7377
{:ecto, "~> 3.12"},
7478
{:localize, "~> 0.50"},
7579
{:ecto_sql, "~> 3.12", optional: true},
7680
{:postgrex, "~> 0.20", only: [:dev, :test]},
81+
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
7782
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
7883
{:ex_doc, "~> 0.34", only: [:dev, :release], runtime: false}
79-
]
84+
] ++ maybe_json_polyfill()
85+
end
86+
87+
# json_polyfill (the EEP 68 :json module for OTP 26) is provided for
88+
# this project's own dev/test/CI only — `only:` dependencies never
89+
# enter the hex package requirements. OTP 26 consumers add
90+
# {:json_polyfill, "~> 0.2 or ~> 1.0"} to their own deps, as the
91+
# localize README documents. The conditional avoids fetching it on
92+
# OTP >= 27, where :json is built in.
93+
defp maybe_json_polyfill do
94+
if Code.ensure_loaded?(:json) do
95+
[]
96+
else
97+
[{:json_polyfill, "~> 0.2 or ~> 1.0", only: [:dev, :test]}]
98+
end
8099
end
81100
end

mix.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
%{
2+
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3+
"credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"},
24
"db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
35
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
46
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
@@ -8,7 +10,9 @@
810
"erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"},
911
"ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"},
1012
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
13+
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
1114
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
15+
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
1216
"localize": {:hex, :localize, "0.50.0", "adf1dfa06aa3e23596389a52bd5a819c24415eb937abc5e511fc49f77c8984b6", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: true]}, {:gettext, "~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ac5621c8b762470e5a6d24d0e7d8565491220f85fa9e09697c61b33013bb2e0d"},
1317
"makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"},
1418
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
defmodule Localize.Ecto.IntegrationTest do
2+
# Migrations and DDL are global to the test database, so this suite
3+
# is not async.
4+
use ExUnit.Case, async: false
5+
6+
import Ecto.Query
7+
import Localize.Ecto
8+
9+
alias Localize.Ecto.TestRepo
10+
11+
defmodule AddPhonebookCollation do
12+
use Ecto.Migration
13+
14+
import Localize.Ecto.Migration
15+
16+
def change do
17+
create_collation("de-u-co-phonebk", if_not_exists: true)
18+
create_collation("de-u-co-phonebk", name: "german_phonebook", if_not_exists: true)
19+
end
20+
end
21+
22+
defmodule DropPhonebookCollation do
23+
use Ecto.Migration
24+
25+
import Localize.Ecto.Migration
26+
27+
def change do
28+
drop_collation("de-u-co-phonebk", name: "german_phonebook", if_exists: true)
29+
end
30+
end
31+
32+
setup_all do
33+
TestRepo.query!("create table if not exists words (id serial primary key, word text)")
34+
TestRepo.query!("truncate words")
35+
36+
for word <- ["Muller", "Müller", "Mueller", "Mahler"] do
37+
TestRepo.query!("insert into words (word) values ($1)", [word])
38+
end
39+
40+
:ok
41+
end
42+
43+
defp migrate(:up, migration, version) do
44+
Ecto.Migrator.up(TestRepo, version, migration, log: false)
45+
end
46+
47+
defp migrate(:down, migration, version) do
48+
Ecto.Migrator.down(TestRepo, version, migration, log: false)
49+
end
50+
51+
defp collation_names do
52+
"select collname from pg_collation where collname like '%phonebk%' or collname like '%phonebook%'"
53+
|> TestRepo.query!()
54+
|> Map.fetch!(:rows)
55+
|> List.flatten()
56+
end
57+
58+
defp words_ordered_by(locale_or_options) do
59+
query = from w in "words", order_by: collate(w.word, ^locale_or_options), select: w.word
60+
TestRepo.all(query)
61+
end
62+
63+
describe "create_collation/2 and drop_collation/2 in a migration" do
64+
test "creating, using, rolling back and dropping collations" do
65+
assert :ok = migrate(:up, AddPhonebookCollation, 20_260_717_000_001)
66+
assert "de-u-co-phonebk-x-icu" in collation_names()
67+
assert "german_phonebook" in collation_names()
68+
69+
# Standard German sorts ü after u; phonebook order treats ü as ue.
70+
assert words_ordered_by("de") == ["Mahler", "Mueller", "Muller", "Müller"]
71+
assert words_ordered_by("de-u-co-phonebk") == ["Mahler", "Mueller", "Müller", "Muller"]
72+
73+
assert words_ordered_by(collation: "german_phonebook") ==
74+
["Mahler", "Mueller", "Müller", "Muller"]
75+
76+
# A collated comparison in a where clause.
77+
comparison =
78+
from w in "words",
79+
where: collate(w.word < "Muller", "de-u-co-phonebk"),
80+
order_by: w.word,
81+
select: w.word
82+
83+
assert TestRepo.all(comparison) == ["Mahler", "Mueller", "Müller"]
84+
85+
# drop_collation/2 is reversible too: run its migration up, then
86+
# roll both migrations back and confirm the collations are gone.
87+
assert :ok = migrate(:up, DropPhonebookCollation, 20_260_717_000_002)
88+
refute "german_phonebook" in collation_names()
89+
90+
assert :ok = migrate(:down, DropPhonebookCollation, 20_260_717_000_002)
91+
assert "german_phonebook" in collation_names()
92+
93+
assert :ok = migrate(:down, AddPhonebookCollation, 20_260_717_000_001)
94+
refute "de-u-co-phonebk-x-icu" in collation_names()
95+
refute "german_phonebook" in collation_names()
96+
end
97+
end
98+
99+
describe "collate/1,2 against a live server" do
100+
test "orders by locale-specific collations" do
101+
assert words_ordered_by("de") == ["Mahler", "Mueller", "Muller", "Müller"]
102+
assert words_ordered_by("sv") == ["Mahler", "Mueller", "Muller", "Müller"]
103+
end
104+
105+
test "orders by the current locale" do
106+
{:ok, _} = Localize.put_locale("de")
107+
query = from w in "words", order_by: collate(w.word), select: w.word
108+
109+
assert TestRepo.all(query) == ["Mahler", "Mueller", "Muller", "Müller"]
110+
after
111+
Localize.put_locale("en")
112+
end
113+
end
114+
end

test/support/test_repo.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
defmodule Localize.Ecto.TestRepo do
2+
@moduledoc false
3+
4+
use Ecto.Repo, otp_app: :localize_ecto, adapter: Ecto.Adapters.Postgres
5+
end

test/test_helper.exs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,25 @@
1+
# Integration tests run against a real PostgreSQL server. Connection
2+
# settings follow the libpq environment variables so CI (service
3+
# container with a password) and local development (trust auth) both
4+
# work without configuration changes.
5+
Application.put_env(:localize_ecto, Localize.Ecto.TestRepo,
6+
hostname: System.get_env("PGHOST", "localhost"),
7+
port: String.to_integer(System.get_env("PGPORT", "5432")),
8+
username: System.get_env("PGUSER", System.get_env("USER")),
9+
password: System.get_env("PGPASSWORD"),
10+
database: "localize_ecto_test",
11+
pool_size: 2,
12+
log: false
13+
)
14+
15+
repo_config = Application.get_env(:localize_ecto, Localize.Ecto.TestRepo)
16+
17+
case Ecto.Adapters.Postgres.storage_up(repo_config) do
18+
:ok -> :ok
19+
{:error, :already_up} -> :ok
20+
{:error, reason} -> raise "could not create test database: #{inspect(reason)}"
21+
end
22+
23+
{:ok, _} = Localize.Ecto.TestRepo.start_link()
24+
125
ExUnit.start()

0 commit comments

Comments
 (0)