-module(dream_test@reporters@json). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream_test/reporters/json.gleam"). -export([new/0, pretty/1, format/1, format_pretty/1, render/2, report/2, report_pretty/2]). -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( " JSON test reporter for dream_test.\n" "\n" " This reporter outputs test results as JSON for CI/CD integration,\n" " test aggregation, and tooling.\n" "\n" " The JSON object includes:\n" "\n" " - `version`: schema version\n" " - `timestamp_ms`: when the report was created\n" " - `duration_ms`: total duration (sum of test durations)\n" " - `system`: `os`, `otp_version`, `gleam_version`\n" " - `summary`: counts by status\n" " - `tests`: per-test details (name, full_name, status, duration_ms, tags, kind, failures)\n" "\n" " ## Usage\n" "\n" " ```gleam\n" " import dream_test/matchers.{succeed}\n" " import dream_test/reporters/json\n" " import dream_test/reporters/progress\n" " import dream_test/runner\n" " import dream_test/unit.{describe, it}\n" "\n" " pub fn tests() {\n" " describe(\"JSON Reporter\", [\n" " it(\"outputs JSON format\", fn() {\n" " // The json reporter prints machine-readable JSON at the end of the run.\n" " Ok(succeed())\n" " }),\n" " it(\"includes test metadata\", fn() {\n" " // JSON output includes name, full_name, status, duration, tags\n" " Ok(succeed())\n" " }),\n" " ])\n" " }\n" "\n" " pub fn main() {\n" " runner.new([tests()])\n" " |> runner.progress_reporter(progress.new())\n" " |> runner.results_reporters([json.new()])\n" " |> runner.exit_on_failure()\n" " |> runner.run()\n" " }\n" " ```\n" ). -file("src/dream_test/reporters/json.gleam", 64). ?DOC(" Create a JSON results reporter (printed at the end of the run).\n"). -spec new() -> dream_test@reporters@types:results_reporter(). new() -> {json, {json_reporter_config, false}}. -file("src/dream_test/reporters/json.gleam", 69). ?DOC(" Enable pretty-printed JSON output.\n"). -spec pretty(dream_test@reporters@types:results_reporter()) -> dream_test@reporters@types:results_reporter(). pretty(Reporter) -> case Reporter of {json, _} -> {json, {json_reporter_config, true}}; Other -> Other end. -file("src/dream_test/reporters/json.gleam", 317). -spec build_payload_fields( gleam@option:option(dream_test@types:failure_payload()) ) -> list({binary(), gleam@json:json()}). build_payload_fields(Payload) -> case Payload of none -> []; {some, {equality_failure, Actual, Expected}} -> [{<<"expected"/utf8>>, gleam@json:string(Expected)}, {<<"actual"/utf8>>, gleam@json:string(Actual)}]; {some, {boolean_failure, Actual@1, Expected@1}} -> [{<<"expected"/utf8>>, gleam@json:bool(Expected@1)}, {<<"actual"/utf8>>, gleam@json:bool(Actual@1)}]; {some, {option_failure, Actual@2, Expected_some}} -> [{<<"expected_some"/utf8>>, gleam@json:bool(Expected_some)}, {<<"actual"/utf8>>, gleam@json:string(Actual@2)}]; {some, {result_failure, Actual@3, Expected_ok}} -> [{<<"expected_ok"/utf8>>, gleam@json:bool(Expected_ok)}, {<<"actual"/utf8>>, gleam@json:string(Actual@3)}]; {some, {collection_failure, Actual@4, Expected@2, Operation}} -> [{<<"expected"/utf8>>, gleam@json:string(Expected@2)}, {<<"actual"/utf8>>, gleam@json:string(Actual@4)}, {<<"operation"/utf8>>, gleam@json:string(Operation)}]; {some, {comparison_failure, Actual@5, Expected@3, Operator}} -> [{<<"expected"/utf8>>, gleam@json:string(Expected@3)}, {<<"actual"/utf8>>, gleam@json:string(Actual@5)}, {<<"comparison_operator"/utf8>>, gleam@json:string(Operator)}]; {some, {string_match_failure, Actual@6, Pattern, Operation@1}} -> [{<<"pattern"/utf8>>, gleam@json:string(Pattern)}, {<<"actual"/utf8>>, gleam@json:string(Actual@6)}, {<<"operation"/utf8>>, gleam@json:string(Operation@1)}]; {some, {snapshot_failure, Actual@7, Expected@4, Snapshot_path, Is_missing}} -> [{<<"expected"/utf8>>, gleam@json:string(Expected@4)}, {<<"actual"/utf8>>, gleam@json:string(Actual@7)}, {<<"snapshot_path"/utf8>>, gleam@json:string(Snapshot_path)}, {<<"is_missing"/utf8>>, gleam@json:bool(Is_missing)}]; {some, {custom_matcher_failure, Actual@8, Description}} -> [{<<"actual"/utf8>>, gleam@json:string(Actual@8)}, {<<"description"/utf8>>, gleam@json:string(Description)}] end. -file("src/dream_test/reporters/json.gleam", 307). -spec build_failure_json(dream_test@types:assertion_failure()) -> gleam@json:json(). build_failure_json(Failure) -> Base_fields = [{<<"operator"/utf8>>, gleam@json:string(erlang:element(2, Failure))}, {<<"message"/utf8>>, gleam@json:string(erlang:element(3, Failure))}], Payload_fields = build_payload_fields(erlang:element(4, Failure)), gleam@json:object(lists:append(Base_fields, Payload_fields)). -file("src/dream_test/reporters/json.gleam", 370). -spec status_to_string(dream_test@types:status()) -> binary(). status_to_string(Status) -> case Status of passed -> <<"passed"/utf8>>; failed -> <<"failed"/utf8>>; skipped -> <<"skipped"/utf8>>; pending -> <<"pending"/utf8>>; timed_out -> <<"timed_out"/utf8>>; setup_failed -> <<"setup_failed"/utf8>> end. -file("src/dream_test/reporters/json.gleam", 381). -spec kind_to_string(dream_test@types:test_kind()) -> binary(). kind_to_string(Kind) -> case Kind of unit -> <<"unit"/utf8>>; integration -> <<"integration"/utf8>>; {gherkin_scenario, Id} -> <<"gherkin:"/utf8, Id/binary>> end. -file("src/dream_test/reporters/json.gleam", 295). -spec build_test_json(dream_test@types:test_result()) -> gleam@json:json(). build_test_json(Result) -> gleam@json:object( [{<<"name"/utf8>>, gleam@json:string(erlang:element(2, Result))}, {<<"full_name"/utf8>>, gleam@json:array( erlang:element(3, Result), fun gleam@json:string/1 )}, {<<"status"/utf8>>, gleam@json:string(status_to_string(erlang:element(4, Result)))}, {<<"duration_ms"/utf8>>, gleam@json:int(erlang:element(5, Result))}, {<<"tags"/utf8>>, gleam@json:array( erlang:element(6, Result), fun gleam@json:string/1 )}, {<<"kind"/utf8>>, gleam@json:string(kind_to_string(erlang:element(8, Result)))}, {<<"failures"/utf8>>, gleam@json:array( erlang:element(7, Result), fun build_failure_json/1 )}] ). -file("src/dream_test/reporters/json.gleam", 393). -spec count_by_status_loop( list(dream_test@types:test_result()), dream_test@types:status(), integer() ) -> integer(). count_by_status_loop(Results, Wanted, Count) -> case Results of [] -> Count; [Result | Rest] -> case erlang:element(4, Result) =:= Wanted of true -> count_by_status_loop(Rest, Wanted, Count + 1); false -> count_by_status_loop(Rest, Wanted, Count) end end. -file("src/dream_test/reporters/json.gleam", 389). -spec count_by_status( list(dream_test@types:test_result()), dream_test@types:status() ) -> integer(). count_by_status(Results, Wanted) -> count_by_status_loop(Results, Wanted, 0). -file("src/dream_test/reporters/json.gleam", 275). -spec build_summary_json(list(dream_test@types:test_result())) -> gleam@json:json(). build_summary_json(Results) -> Total = erlang:length(Results), Passed = count_by_status(Results, passed), Failed = count_by_status(Results, failed), Skipped = count_by_status(Results, skipped), Pending = count_by_status(Results, pending), Timed_out = count_by_status(Results, timed_out), Setup_failed = count_by_status(Results, setup_failed), gleam@json:object( [{<<"total"/utf8>>, gleam@json:int(Total)}, {<<"passed"/utf8>>, gleam@json:int(Passed)}, {<<"failed"/utf8>>, gleam@json:int(Failed)}, {<<"skipped"/utf8>>, gleam@json:int(Skipped)}, {<<"pending"/utf8>>, gleam@json:int(Pending)}, {<<"timed_out"/utf8>>, gleam@json:int(Timed_out)}, {<<"setup_failed"/utf8>>, gleam@json:int(Setup_failed)}] ). -file("src/dream_test/reporters/json.gleam", 412). -spec calculate_total_duration_loop( list(dream_test@types:test_result()), integer() ) -> integer(). calculate_total_duration_loop(Results, Total) -> case Results of [] -> Total; [Result | Rest] -> calculate_total_duration_loop( Rest, Total + erlang:element(5, Result) ) end. -file("src/dream_test/reporters/json.gleam", 408). -spec calculate_total_duration(list(dream_test@types:test_result())) -> integer(). calculate_total_duration(Results) -> calculate_total_duration_loop(Results, 0). -file("src/dream_test/reporters/json.gleam", 485). -spec spaces(integer()) -> binary(). spaces(Count) -> gleam@string:repeat(<<" "/utf8>>, Count). -file("src/dream_test/reporters/json.gleam", 428). -spec prettify_chars(list(binary()), integer(), boolean(), binary()) -> binary(). prettify_chars(Chars, Indent, In_string, Acc) -> case Chars of [] -> Acc; [<<"\\"/utf8>>, Escaped | Rest] when In_string -> prettify_chars( Rest, Indent, true, <<<>/binary, Escaped/binary>> ); [<<"\""/utf8>> | Rest@1] -> prettify_chars( Rest@1, Indent, not In_string, <> ); [Char | Rest@2] when In_string -> prettify_chars(Rest@2, Indent, true, <>); [<<"{"/utf8>> | Rest@3] -> New_indent = Indent + 2, prettify_chars( Rest@3, New_indent, false, <<<>/binary, (spaces(New_indent))/binary>> ); [<<"}"/utf8>> | Rest@4] -> New_indent@1 = Indent - 2, prettify_chars( Rest@4, New_indent@1, false, <<<<<>/binary, (spaces(New_indent@1))/binary>>/binary, "}"/utf8>> ); [<<"["/utf8>> | Rest@5] -> New_indent@2 = Indent + 2, prettify_chars( Rest@5, New_indent@2, false, <<<>/binary, (spaces(New_indent@2))/binary>> ); [<<"]"/utf8>> | Rest@6] -> New_indent@3 = Indent - 2, prettify_chars( Rest@6, New_indent@3, false, <<<<<>/binary, (spaces(New_indent@3))/binary>>/binary, "]"/utf8>> ); [<<","/utf8>> | Rest@7] -> prettify_chars( Rest@7, Indent, false, <<<>/binary, (spaces(Indent))/binary>> ); [<<":"/utf8>> | Rest@8] -> prettify_chars(Rest@8, Indent, false, <>); [<<" "/utf8>> | Rest@9] -> prettify_chars(Rest@9, Indent, false, Acc); [Char@1 | Rest@10] -> prettify_chars( Rest@10, Indent, false, <> ) end. -file("src/dream_test/reporters/json.gleam", 424). -spec prettify_json(binary()) -> binary(). prettify_json(Input) -> prettify_chars(gleam@string:to_graphemes(Input), 0, false, <<""/utf8>>). -file("src/dream_test/reporters/json.gleam", 267). -spec build_system_json() -> gleam@json:json(). build_system_json() -> gleam@json:object( [{<<"os"/utf8>>, gleam@json:string(dream_test_reporter_json_ffi:get_os())}, {<<"otp_version"/utf8>>, gleam@json:string( dream_test_reporter_json_ffi:get_otp_version() )}, {<<"gleam_version"/utf8>>, gleam@json:string( dream_test_reporter_json_ffi:get_gleam_version() )}] ). -file("src/dream_test/reporters/json.gleam", 254). -spec build_report_json(list(dream_test@types:test_result())) -> gleam@json:json(). build_report_json(Results) -> Total_duration = calculate_total_duration(Results), gleam@json:object( [{<<"version"/utf8>>, gleam@json:string(<<"1.0"/utf8>>)}, {<<"timestamp_ms"/utf8>>, gleam@json:int(dream_test_reporter_json_ffi:get_timestamp_ms())}, {<<"duration_ms"/utf8>>, gleam@json:int(Total_duration)}, {<<"system"/utf8>>, build_system_json()}, {<<"summary"/utf8>>, build_summary_json(Results)}, {<<"tests"/utf8>>, gleam@json:array(Results, fun build_test_json/1)}] ). -file("src/dream_test/reporters/json.gleam", 123). ?DOC( " Format test results as a compact JSON string.\n" "\n" " Returns a single-line JSON string suitable for machine parsing.\n" "\n" " ## Parameters\n" "\n" " - `results`: The test results to encode into the JSON report\n" "\n" " ## Returns\n" "\n" " A compact (single-line) JSON string.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import dream_test/matchers.{succeed}\n" " import dream_test/reporters/json\n" " import dream_test/runner\n" " import dream_test/unit.{describe, it}\n" "\n" " fn example_suite() {\n" " describe(\"Example Suite\", [\n" " it(\"passes\", fn() { Ok(succeed()) }),\n" " ])\n" " }\n" "\n" " pub fn main() {\n" " let results = runner.new([example_suite()]) |> runner.run()\n" " json.format(results)\n" " }\n" " ```\n" ). -spec format(list(dream_test@types:test_result())) -> binary(). format(Results) -> _pipe = build_report_json(Results), gleam@json:to_string(_pipe). -file("src/dream_test/reporters/json.gleam", 160). ?DOC( " Format test results as pretty-printed JSON.\n" "\n" " Returns an indented, human-readable JSON string with 2-space indentation.\n" "\n" " ## Parameters\n" "\n" " - `results`: The test results to encode into the JSON report\n" "\n" " ## Returns\n" "\n" " A pretty-printed JSON string with 2-space indentation.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import dream_test/matchers.{succeed}\n" " import dream_test/reporters/json\n" " import dream_test/runner\n" " import dream_test/unit.{describe, it}\n" "\n" " fn example_suite() {\n" " describe(\"Example Suite\", [\n" " it(\"passes\", fn() { Ok(succeed()) }),\n" " ])\n" " }\n" "\n" " pub fn main() {\n" " let results = runner.new([example_suite()]) |> runner.run()\n" " json.format_pretty(results)\n" " }\n" " ```\n" ). -spec format_pretty(list(dream_test@types:test_result())) -> binary(). format_pretty(Results) -> _pipe = build_report_json(Results), _pipe@1 = gleam@json:to_string(_pipe), prettify_json(_pipe@1). -file("src/dream_test/reporters/json.gleam", 80). ?DOC(" Render JSON output for a completed run.\n"). -spec render( dream_test@reporters@types:json_reporter_config(), list(dream_test@types:test_result()) ) -> binary(). render(Config, Results) -> {json_reporter_config, Pretty} = Config, case Pretty of true -> format_pretty(Results); false -> format(Results) end. -file("src/dream_test/reporters/json.gleam", 200). ?DOC( " Print test results as JSON using a provided writer function.\n" "\n" " This is the main entry point for JSON reporting. The writer function\n" " receives the JSON string and can print it, log it, or write it to a file.\n" "\n" " ## Parameters\n" "\n" " - `results`: The test results to encode and write\n" " - `write`: Output sink for the JSON string (for example `io.print`)\n" "\n" " ## Returns\n" "\n" " Returns the input `results` unchanged, enabling pipeline composition.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import dream_test/matchers.{succeed}\n" " import dream_test/reporters/json\n" " import dream_test/runner\n" " import dream_test/unit.{describe, it}\n" " import gleam/io\n" "\n" " fn example_suite() {\n" " describe(\"Example Suite\", [\n" " it(\"passes\", fn() { Ok(succeed()) }),\n" " ])\n" " }\n" "\n" " pub fn main() {\n" " let results = runner.new([example_suite()]) |> runner.run()\n" " results |> json.report(io.print)\n" " }\n" " ```\n" ). -spec report(list(dream_test@types:test_result()), fun((binary()) -> nil)) -> list(dream_test@types:test_result()). report(Results, Write) -> Write(format(Results)), Results. -file("src/dream_test/reporters/json.gleam", 242). ?DOC( " Print test results as pretty-printed JSON using a provided writer function.\n" "\n" " Same as `report` but with indented, human-readable output.\n" "\n" " ## Parameters\n" "\n" " - `results`: The test results to encode and write\n" " - `write`: Output sink for the JSON string (for example `io.print`)\n" "\n" " ## Returns\n" "\n" " Returns the input `results` unchanged, enabling pipeline composition.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import dream_test/matchers.{succeed}\n" " import dream_test/reporters/json\n" " import dream_test/runner\n" " import dream_test/unit.{describe, it}\n" " import gleam/io\n" "\n" " fn example_suite() {\n" " describe(\"Example Suite\", [\n" " it(\"passes\", fn() { Ok(succeed()) }),\n" " ])\n" " }\n" "\n" " pub fn main() {\n" " let results = runner.new([example_suite()]) |> runner.run()\n" " results |> json.report_pretty(io.print)\n" " }\n" " ```\n" ). -spec report_pretty( list(dream_test@types:test_result()), fun((binary()) -> nil) ) -> list(dream_test@types:test_result()). report_pretty(Results, Write) -> Write(format_pretty(Results)), Results.