okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.peterl: replace deprecated catch expressions with try for OTP 2931ff834

main
7.1 KiB168 linesraw
1-module(albedo_runtime_test_support).
2-export([temporary_database/0, cleanup/1, temporary_workspace/0, cleanup_workspace/1,
3         kernel_memory/1, catalog_does_not_block/3, catalog_forget_while_blocked/3,
4         hold_preparation/2, capture_during_preparation/3]).
5temporary_workspace() ->
6    Path = filename:join(os:getenv("TMPDIR", "/tmp"),
7                         "runtime-workspace-" ++ binary_to_list(albedo_native:new_id())),
8    ok = file:make_dir(Path),
9    unicode:characters_to_binary(Path).
10cleanup_workspace(Path) ->
11    _ = file:del_dir_r(Path),
12    nil.
13temporary_database() ->
14    unicode:characters_to_binary(filename:join(os:getenv("TMPDIR", "/tmp"), "albedo-" ++ binary_to_list(albedo_native:new_id()) ++ ".sqlite" )).
15cleanup(Path) ->
16    lists:foreach(fun(Suffix) -> file:delete(<<Path/binary, Suffix/binary>>) end,
17                  [<<>>, <<"-wal">>, <<"-shm">>]),
18    _ = file:del_dir_r(<<Path/binary, ".backup">>),
19    nil.
20
21%% A barrier followed by GC measures retained ownership, not transient boot garbage.
22kernel_memory({session, _, _, Kernel, _, _, _, _, _}) ->
23    _ = albedo_python:events(Kernel),
24    true = erlang:garbage_collect(Kernel),
25    {memory, Bytes} = erlang:process_info(Kernel, memory),
26    Bytes.
27
28%% A catalog worker waiting on the real ledger must not own the runtime loop.
29catalog_does_not_block(Runtime, Home, Id) ->
30    Store = 'albedo@harness@runtime':ledger(Runtime),
31    Owner = 'albedo@daemon@store':owner(Store),
32    erlang:suspend_process(Owner),
33    Parent = self(),
34    Worker = spawn_link(fun() ->
35        Result = 'albedo@harness@runtime':observe_catalog(Runtime, Home, Id),
36        Parent ! {catalog_complete, self(), Result}
37    end),
38    try
39        await_store_query(Owner, erlang:monotonic_time(millisecond) + 3000),
40        Reader = spawn_link(fun() ->
41            Loaded = 'albedo@harness@runtime':loaded_sessions(Runtime),
42            Parent ! {loaded_complete, self(), Loaded}
43        end),
44        Responsive = receive
45            {loaded_complete, Reader, _} -> true
46        after 1000 -> false
47        end,
48        erlang:resume_process(Owner),
49        Completed = receive
50            {catalog_complete, Worker, {ok, _}} -> true;
51            {catalog_complete, Worker, {error, _}} -> false
52        after 10000 -> false
53        end,
54        {Responsive, Completed}
55    after
56        resume(Owner)
57    end.
58
59%% The catalog began against a prepared composition that is removed while its
60%% discovery is held. Its eventual response must observe that newer ownership.
61catalog_forget_while_blocked(Runtime, Home, Id) ->
62    Store = 'albedo@harness@runtime':ledger(Runtime),
63    Owner = 'albedo@daemon@store':owner(Store),
64    erlang:suspend_process(Owner),
65    Parent = self(),
66    Worker = spawn_link(fun() ->
67        Result = 'albedo@harness@runtime':observe_catalog(Runtime, Home, Id),
68        Parent ! {catalog_complete, self(), Result}
69    end),
70    try
71        await_store_query(Owner, erlang:monotonic_time(millisecond) + 3000),
72        'albedo@harness@runtime':forget_session(Runtime, Id),
73        erlang:resume_process(Owner),
74        receive
75            {catalog_complete, Worker, Result} -> Result
76        after 10000 -> error(catalog_did_not_complete)
77        end
78    after
79        resume(Owner)
80    end.
81
82%% A body that already resumed Owner leaves nothing to resume.
83resume(Owner) ->
84    try erlang:resume_process(Owner) catch error:badarg -> false end.
85
86await_store_query(Owner, Deadline) ->
87    case process_info(Owner, message_queue_len) of
88        {message_queue_len, N} when N > 0 -> ok;
89        _ ->
90            case erlang:monotonic_time(millisecond) < Deadline of
91                true -> receive after 10 -> ok end, await_store_query(Owner, Deadline);
92                false -> error(catalog_did_not_query_store)
93            end
94    end.
95
96%% The real composition worker calls this context loader. Only an explicit
97%% release lets it finish; no process state or runtime messages are injected.
98hold_preparation(Parent, Workspace) ->
99    Parent ! {preparation_held, self(), Workspace},
100    receive
101        release_preparation -> {ok, <<>>}
102    after 20000 -> {error, <<"preparation release was not received">>}
103    end.
104
105capture_during_preparation(Runtime, Session, Requests) ->
106    Parent = self(),
107    %% Fixture startup follows peek_commands' deadline, not a latency assertion.
108    Deadline = erlang:monotonic_time(millisecond) + 15000,
109    %% Inspect only the public runtime handle's subject, never actor State.
110    {ok, Owner} = 'gleam@erlang@process':subject_owner(element(2, Runtime)),
111    Callers = [spawn(fun() ->
112        erlang:trace(self(), true, [send, {tracer, Parent}]),
113        Result = try 'albedo@harness@runtime':peek_commands(Runtime, Id, Workspace)
114                 catch Class:Reason -> {error, {preparation_caller_failed, Class, Reason}}
115                 end,
116        erlang:trace(self(), false, [send]),
117        Parent ! {preparation_complete, self(), Result}
118    end) || {Id, Workspace} <- Requests],
119    try
120        await_preparation_requests(Callers, Owner, Deadline),
121        %% All actual request sends precede this public owner roundtrip.
122        _ = 'albedo@harness@runtime':loaded_sessions(Runtime),
123        await_preparation_hold(Deadline),
124        Captured = 'albedo@daemon@session':capture(Session),
125        Completed = release_preparations(Callers, true),
126        {Captured, Completed}
127    catch Class:Reason:Stack ->
128        %% Failure during setup must also release callbacks and drain callers.
129        _ = release_preparations(Callers, true),
130        erlang:raise(Class, Reason, Stack)
131    end.
132
133await_preparation_requests([], _Owner, _Deadline) -> ok;
134await_preparation_requests(Callers, Owner, Deadline) ->
135    receive
136        {trace, Caller, send, _, Owner} ->
137            await_preparation_requests(lists:delete(Caller, Callers), Owner, Deadline);
138        {preparation_complete, _, _} = Completion ->
139            self() ! Completion,
140            error({preparation_completed_before_admission, Completion})
141    after max(0, Deadline - erlang:monotonic_time(millisecond)) ->
142        error(preparation_request_was_not_sent)
143    end.
144
145await_preparation_hold(Deadline) ->
146    receive
147        {preparation_held, _, _} = Held ->
148            %% Retain the hold so cleanup releases it after synchronous capture.
149            self() ! Held;
150        {preparation_complete, _, _} = Completion ->
151            self() ! Completion,
152            error({preparation_completed_before_context_hold, Completion})
153    after max(0, Deadline - erlang:monotonic_time(millisecond)) ->
154        error(preparation_context_did_not_start_before_call_deadline)
155    end.
156
157release_preparations([], Completed) -> Completed;
158release_preparations(Callers, Completed) ->
159    receive
160        {preparation_held, Worker, _} ->
161            Worker ! release_preparation,
162            release_preparations(Callers, Completed);
163        {preparation_complete, Caller, Result} ->
164            Successful = case Result of {ok, _} -> true; _ -> false end,
165            release_preparations(lists:delete(Caller, Callers), Completed andalso Successful);
166        {trace, _, send, _, _} -> release_preparations(Callers, Completed)
167    after 10000 -> false
168    end.