src/lightspeed@testing.erl

-module(lightspeed@testing).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/testing.gleam").
-export([trace/1, opcodes/1, run_with_log/1, run/1, instructions/1, replay/1, mounted_route/1, rendered_views/1, patches/1, pushed_events/1, subscriptions/1, navigation/1, shutdown_reason/1, telemetry/1, errors/1]).
-export_type([trace/0, log_entry/0, patch_record/0, event_record/0, simulation_state/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(" Helpers for deterministic runtime tests.\n").

-type trace() :: {trace, list(binary())}.

-type log_entry() :: {log_entry,
        integer(),
        lightspeed@agent@isa:instruction(),
        binary(),
        binary()}.

-type patch_record() :: {patch_record, binary(), binary()}.

-type event_record() :: {event_record, binary(), binary()}.

-type simulation_state() :: {simulation_state,
        gleam@option:option(binary()),
        list(binary()),
        list(patch_record()),
        list(event_record()),
        list(binary()),
        gleam@option:option(binary()),
        gleam@option:option(binary()),
        list(binary()),
        list(binary())}.

-file("src/lightspeed/testing.gleam", 48).
?DOC(" Build a trace from an instruction list.\n").
-spec trace(list(lightspeed@agent@isa:instruction())) -> trace().
trace(Instructions) ->
    _pipe = Instructions,
    _pipe@1 = gleam@list:map(_pipe, fun lightspeed@agent@isa:opcode/1),
    {trace, _pipe@1}.

-file("src/lightspeed/testing.gleam", 55).
?DOC(" Extract opcodes from a trace.\n").
-spec opcodes(trace()) -> list(binary()).
opcodes(Trace) ->
    erlang:element(2, Trace).

-file("src/lightspeed/testing.gleam", 130).
-spec new_state() -> simulation_state().
new_state() ->
    {simulation_state, none, [], [], [], [], none, none, [], []}.

-file("src/lightspeed/testing.gleam", 250).
-spec remove_topic(binary(), list(binary())) -> list(binary()).
remove_topic(Topic, Topics) ->
    case Topics of
        [] ->
            [];

        [Entry | Rest] ->
            case Entry =:= Topic of
                true ->
                    remove_topic(Topic, Rest);

                false ->
                    [Entry | remove_topic(Topic, Rest)]
            end
    end.

-file("src/lightspeed/testing.gleam", 239).
-spec contains_topic(binary(), list(binary())) -> boolean().
contains_topic(Topic, Topics) ->
    case Topics of
        [] ->
            false;

        [Entry | Rest] ->
            case Entry =:= Topic of
                true ->
                    true;

                false ->
                    contains_topic(Topic, Rest)
            end
    end.

-file("src/lightspeed/testing.gleam", 235).
-spec append_error(simulation_state(), binary()) -> simulation_state().
append_error(State, Message) ->
    {simulation_state,
        erlang:element(2, State),
        erlang:element(3, State),
        erlang:element(4, State),
        erlang:element(5, State),
        erlang:element(6, State),
        erlang:element(7, State),
        erlang:element(8, State),
        erlang:element(9, State),
        [Message | erlang:element(10, State)]}.

-file("src/lightspeed/testing.gleam", 228).
-spec append_telemetry(simulation_state(), binary()) -> simulation_state().
append_telemetry(State, Message) ->
    {simulation_state,
        erlang:element(2, State),
        erlang:element(3, State),
        erlang:element(4, State),
        erlang:element(5, State),
        erlang:element(6, State),
        erlang:element(7, State),
        erlang:element(8, State),
        [Message | erlang:element(9, State)],
        erlang:element(10, State)}.

-file("src/lightspeed/testing.gleam", 166).
-spec apply_instruction(simulation_state(), lightspeed@agent@isa:instruction()) -> simulation_state().
apply_instruction(State, Instruction) ->
    State@1 = append_telemetry(
        State,
        lightspeed@agent@isa:describe(Instruction)
    ),
    case Instruction of
        {mount, Route, _} ->
            case erlang:element(2, State@1) of
                none ->
                    {simulation_state,
                        {some, Route},
                        erlang:element(3, State@1),
                        erlang:element(4, State@1),
                        erlang:element(5, State@1),
                        erlang:element(6, State@1),
                        erlang:element(7, State@1),
                        erlang:element(8, State@1),
                        erlang:element(9, State@1),
                        erlang:element(10, State@1)};

                {some, _} ->
                    append_error(State@1, <<"duplicate mount"/utf8>>)
            end;

        {render, View_id} ->
            case erlang:element(2, State@1) of
                none ->
                    append_error(State@1, <<"render before mount"/utf8>>);

                {some, _} ->
                    {simulation_state,
                        erlang:element(2, State@1),
                        [View_id | erlang:element(3, State@1)],
                        erlang:element(4, State@1),
                        erlang:element(5, State@1),
                        erlang:element(6, State@1),
                        erlang:element(7, State@1),
                        erlang:element(8, State@1),
                        erlang:element(9, State@1),
                        erlang:element(10, State@1)}
            end;

        {patch, Target, Html} ->
            case erlang:element(2, State@1) of
                none ->
                    append_error(State@1, <<"patch before mount"/utf8>>);

                {some, _} ->
                    Patch = {patch_record, Target, Html},
                    {simulation_state,
                        erlang:element(2, State@1),
                        erlang:element(3, State@1),
                        [Patch | erlang:element(4, State@1)],
                        erlang:element(5, State@1),
                        erlang:element(6, State@1),
                        erlang:element(7, State@1),
                        erlang:element(8, State@1),
                        erlang:element(9, State@1),
                        erlang:element(10, State@1)}
            end;

        {push_event, Name, Payload} ->
            Event = {event_record, Name, Payload},
            {simulation_state,
                erlang:element(2, State@1),
                erlang:element(3, State@1),
                erlang:element(4, State@1),
                [Event | erlang:element(5, State@1)],
                erlang:element(6, State@1),
                erlang:element(7, State@1),
                erlang:element(8, State@1),
                erlang:element(9, State@1),
                erlang:element(10, State@1)};

        {navigate, To} ->
            {simulation_state,
                erlang:element(2, State@1),
                erlang:element(3, State@1),
                erlang:element(4, State@1),
                erlang:element(5, State@1),
                erlang:element(6, State@1),
                {some, To},
                erlang:element(8, State@1),
                erlang:element(9, State@1),
                erlang:element(10, State@1)};

        {subscribe, Topic} ->
            case contains_topic(Topic, erlang:element(6, State@1)) of
                true ->
                    State@1;

                false ->
                    {simulation_state,
                        erlang:element(2, State@1),
                        erlang:element(3, State@1),
                        erlang:element(4, State@1),
                        erlang:element(5, State@1),
                        [Topic | erlang:element(6, State@1)],
                        erlang:element(7, State@1),
                        erlang:element(8, State@1),
                        erlang:element(9, State@1),
                        erlang:element(10, State@1)}
            end;

        {unsubscribe, Topic@1} ->
            Subscriptions = remove_topic(Topic@1, erlang:element(6, State@1)),
            {simulation_state,
                erlang:element(2, State@1),
                erlang:element(3, State@1),
                erlang:element(4, State@1),
                erlang:element(5, State@1),
                Subscriptions,
                erlang:element(7, State@1),
                erlang:element(8, State@1),
                erlang:element(9, State@1),
                erlang:element(10, State@1)};

        {shutdown, Reason} ->
            {simulation_state,
                erlang:element(2, State@1),
                erlang:element(3, State@1),
                erlang:element(4, State@1),
                erlang:element(5, State@1),
                erlang:element(6, State@1),
                erlang:element(7, State@1),
                {some, Reason},
                erlang:element(9, State@1),
                erlang:element(10, State@1)}
    end.

-file("src/lightspeed/testing.gleam", 144).
-spec run_loop(
    list(lightspeed@agent@isa:instruction()),
    simulation_state(),
    list(log_entry()),
    integer()
) -> {simulation_state(), list(log_entry())}.
run_loop(Program, State, Log_rev, Step) ->
    case Program of
        [] ->
            {State, lists:reverse(Log_rev)};

        [Instruction | Rest] ->
            State@1 = apply_instruction(State, Instruction),
            Entry = {log_entry,
                Step,
                Instruction,
                lightspeed@agent@isa:opcode(Instruction),
                lightspeed@agent@isa:describe(Instruction)},
            run_loop(Rest, State@1, [Entry | Log_rev], Step + 1)
    end.

-file("src/lightspeed/testing.gleam", 66).
?DOC(" Run an ISA program and return a replayable instruction log.\n").
-spec run_with_log(list(lightspeed@agent@isa:instruction())) -> {simulation_state(),
    list(log_entry())}.
run_with_log(Program) ->
    run_loop(Program, new_state(), [], 0).

-file("src/lightspeed/testing.gleam", 60).
?DOC(" Run an ISA program through the deterministic test interpreter.\n").
-spec run(list(lightspeed@agent@isa:instruction())) -> simulation_state().
run(Program) ->
    {State, _} = run_with_log(Program),
    State.

-file("src/lightspeed/testing.gleam", 80).
?DOC(" Extract instructions from a log.\n").
-spec instructions(list(log_entry())) -> list(lightspeed@agent@isa:instruction()).
instructions(Log) ->
    _pipe = Log,
    gleam@list:map(_pipe, fun(Entry) -> erlang:element(3, Entry) end).

-file("src/lightspeed/testing.gleam", 73).
?DOC(" Replay a prior log into a deterministic interpreter state.\n").
-spec replay(list(log_entry())) -> simulation_state().
replay(Log) ->
    _pipe = Log,
    _pipe@1 = instructions(_pipe),
    run(_pipe@1).

-file("src/lightspeed/testing.gleam", 86).
?DOC(" Route from the latest mount instruction, when mounted.\n").
-spec mounted_route(simulation_state()) -> gleam@option:option(binary()).
mounted_route(State) ->
    erlang:element(2, State).

-file("src/lightspeed/testing.gleam", 91).
?DOC(" Rendered view ids in execution order.\n").
-spec rendered_views(simulation_state()) -> list(binary()).
rendered_views(State) ->
    lists:reverse(erlang:element(3, State)).

-file("src/lightspeed/testing.gleam", 96).
?DOC(" Patches in execution order.\n").
-spec patches(simulation_state()) -> list(patch_record()).
patches(State) ->
    lists:reverse(erlang:element(4, State)).

-file("src/lightspeed/testing.gleam", 101).
?DOC(" Pushed events in execution order.\n").
-spec pushed_events(simulation_state()) -> list(event_record()).
pushed_events(State) ->
    lists:reverse(erlang:element(5, State)).

-file("src/lightspeed/testing.gleam", 106).
?DOC(" Subscriptions currently active at end of simulation.\n").
-spec subscriptions(simulation_state()) -> list(binary()).
subscriptions(State) ->
    lists:reverse(erlang:element(6, State)).

-file("src/lightspeed/testing.gleam", 111).
?DOC(" Latest navigation target, when present.\n").
-spec navigation(simulation_state()) -> gleam@option:option(binary()).
navigation(State) ->
    erlang:element(7, State).

-file("src/lightspeed/testing.gleam", 116).
?DOC(" Shutdown reason, when a shutdown instruction was executed.\n").
-spec shutdown_reason(simulation_state()) -> gleam@option:option(binary()).
shutdown_reason(State) ->
    erlang:element(8, State).

-file("src/lightspeed/testing.gleam", 121).
?DOC(" Telemetry messages in execution order.\n").
-spec telemetry(simulation_state()) -> list(binary()).
telemetry(State) ->
    lists:reverse(erlang:element(9, State)).

-file("src/lightspeed/testing.gleam", 126).
?DOC(" Validation errors in execution order.\n").
-spec errors(simulation_state()) -> list(binary()).
errors(State) ->
    lists:reverse(erlang:element(10, State)).