Skip to content

Commit 2f14f97

Browse files
authored
Fixes flaky tests (#14)
* Fixes flaky tests
1 parent 4b63191 commit 2f14f97

File tree

7 files changed

+798
-48
lines changed

7 files changed

+798
-48
lines changed

lib/predicator/functions/registry.ex

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,21 @@ defmodule Predicator.Functions.Registry do
5353
"""
5454
@spec start_registry :: :ok
5555
def start_registry do
56-
:ets.new(@registry_name, [:set, :public, :named_table])
57-
:ok
56+
case :ets.whereis(@registry_name) do
57+
:undefined ->
58+
try do
59+
:ets.new(@registry_name, [:set, :public, :named_table])
60+
:ok
61+
rescue
62+
ArgumentError ->
63+
# Another process created the table between our check and creation
64+
# This is fine, just return :ok
65+
:ok
66+
end
67+
68+
_table_exists ->
69+
:ok
70+
end
5871
end
5972

6073
@doc """
@@ -89,8 +102,18 @@ defmodule Predicator.Functions.Registry do
89102
impl: impl
90103
}
91104

92-
:ets.insert(@registry_name, {name, function_info})
93-
:ok
105+
# Handle potential race condition where table could be deleted between
106+
# ensure_registry_exists() and this insert operation
107+
try do
108+
:ets.insert(@registry_name, {name, function_info})
109+
:ok
110+
rescue
111+
ArgumentError ->
112+
# Table was deleted between our check and insert, recreate and retry
113+
ensure_registry_exists()
114+
:ets.insert(@registry_name, {name, function_info})
115+
:ok
116+
end
94117
end
95118

96119
@doc """
@@ -112,21 +135,13 @@ defmodule Predicator.Functions.Registry do
112135
def call(name, args, context) when is_binary(name) and is_list(args) and is_map(context) do
113136
ensure_registry_exists()
114137

115-
case :ets.lookup(@registry_name, name) do
116-
[{_name, %{arity: arity, impl: impl}}] ->
117-
if length(args) == arity do
118-
try do
119-
impl.(args, context)
120-
rescue
121-
error ->
122-
{:error, "Function #{name}() failed: #{Exception.message(error)}"}
123-
end
124-
else
125-
{:error, "Function #{name}() expects #{arity} arguments, got #{length(args)}"}
126-
end
127-
128-
[] ->
129-
{:error, "Unknown function: #{name}"}
138+
try do
139+
do_lookup_and_call(name, args, context)
140+
rescue
141+
ArgumentError ->
142+
# Table was deleted between our check and lookup, recreate and retry
143+
ensure_registry_exists()
144+
do_lookup_and_call(name, args, context)
130145
end
131146
end
132147

@@ -139,9 +154,19 @@ defmodule Predicator.Functions.Registry do
139154
def list_functions do
140155
ensure_registry_exists()
141156

142-
:ets.tab2list(@registry_name)
143-
|> Enum.map(fn {_key, function_info} -> function_info end)
144-
|> Enum.sort_by(& &1.name)
157+
try do
158+
:ets.tab2list(@registry_name)
159+
|> Enum.map(fn {_key, function_info} -> function_info end)
160+
|> Enum.sort_by(& &1.name)
161+
rescue
162+
ArgumentError ->
163+
# Table was deleted between our check and tab2list, recreate and retry
164+
ensure_registry_exists()
165+
166+
:ets.tab2list(@registry_name)
167+
|> Enum.map(fn {_key, function_info} -> function_info end)
168+
|> Enum.sort_by(& &1.name)
169+
end
145170
end
146171

147172
@doc """
@@ -158,7 +183,15 @@ defmodule Predicator.Functions.Registry do
158183
@spec function_registered?(binary()) :: boolean()
159184
def function_registered?(name) when is_binary(name) do
160185
ensure_registry_exists()
161-
:ets.member(@registry_name, name)
186+
187+
try do
188+
:ets.member(@registry_name, name)
189+
rescue
190+
ArgumentError ->
191+
# Table was deleted between our check and member, recreate and retry
192+
ensure_registry_exists()
193+
:ets.member(@registry_name, name)
194+
end
162195
end
163196

164197
@doc """
@@ -169,8 +202,17 @@ defmodule Predicator.Functions.Registry do
169202
@spec clear_registry() :: :ok
170203
def clear_registry do
171204
ensure_registry_exists()
172-
:ets.delete_all_objects(@registry_name)
173-
:ok
205+
206+
try do
207+
:ets.delete_all_objects(@registry_name)
208+
:ok
209+
rescue
210+
ArgumentError ->
211+
# Table was deleted between our check and delete_all_objects, recreate and retry
212+
ensure_registry_exists()
213+
:ets.delete_all_objects(@registry_name)
214+
:ok
215+
end
174216
end
175217

176218
# Ensure the ETS table exists
@@ -180,4 +222,24 @@ defmodule Predicator.Functions.Registry do
180222
_table_exists -> :ok
181223
end
182224
end
225+
226+
# Helper function to lookup and call a function (extracted to eliminate duplicate code)
227+
defp do_lookup_and_call(name, args, context) do
228+
case :ets.lookup(@registry_name, name) do
229+
[{_name, %{arity: arity, impl: impl}}] ->
230+
if length(args) == arity do
231+
try do
232+
impl.(args, context)
233+
rescue
234+
error ->
235+
{:error, "Function #{name}() failed: #{Exception.message(error)}"}
236+
end
237+
else
238+
{:error, "Function #{name}() expects #{arity} arguments, got #{length(args)}"}
239+
end
240+
241+
[] ->
242+
{:error, "Unknown function: #{name}"}
243+
end
244+
end
183245
end

test/custom_functions_integration_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule CustomFunctionsIntegrationTest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case, async: false
33

44
import Predicator
55

test/function_calls_integration_test.exs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
defmodule FunctionCallsIntegrationTest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case, async: false
33

44
import Predicator
5-
alias Predicator.Functions.{Registry, SystemFunctions}
65

76
setup do
8-
# Ensure system functions are available
9-
Registry.clear_registry()
10-
SystemFunctions.register_all()
7+
# Clear custom functions but preserve system functions
8+
clear_custom_functions()
119
:ok
1210
end
1311

test/predicator/evaluator_edge_cases_test.exs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
defmodule Predicator.EvaluatorEdgeCasesTest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case, async: false
33

44
alias Predicator.Evaluator
5-
alias Predicator.Functions.{Registry, SystemFunctions}
65

76
setup do
8-
# Ensure built-in functions are available
9-
Registry.clear_registry()
10-
SystemFunctions.register_all()
7+
# Clear custom functions but preserve system functions
8+
Predicator.clear_custom_functions()
119
:ok
1210
end
1311

@@ -94,7 +92,7 @@ defmodule Predicator.EvaluatorEdgeCasesTest do
9492

9593
test "handles function call that returns error" do
9694
# Register a function that always returns an error
97-
Registry.register_function("error_func", 1, fn [_arg], _context ->
95+
Predicator.register_function("error_func", 1, fn [_arg], _context ->
9896
{:error, "Function intentionally failed"}
9997
end)
10098

@@ -284,7 +282,7 @@ defmodule Predicator.EvaluatorEdgeCasesTest do
284282

285283
test "preserves error message in exception" do
286284
# Register function that returns error
287-
Registry.register_function("fail_func", 0, fn [], _context ->
285+
Predicator.register_function("fail_func", 0, fn [], _context ->
288286
{:error, "Custom failure message"}
289287
end)
290288

0 commit comments

Comments
 (0)