Current section
Files
Jump to
Current section
Files
src/dream_test@reporters@progress.erl
-module(dream_test@reporters@progress).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_test/reporters/progress.gleam").
-export([new/0, render/2, handle_event/2]).
-export_type([progress_reporter/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(
" Live progress bar reporter.\n"
"\n"
" This module renders a single-line progress bar that updates in-place using\n"
" carriage returns, and adapts to the current terminal width.\n"
"\n"
" Because it uses `\\r` (carriage return) to rewrite the current line, it is\n"
" best suited for interactive terminals. In CI logs (or when other output is\n"
" printed concurrently), the output may be less readable.\n"
"\n"
" ## Terminal width\n"
"\n"
" Width is detected via Erlang `io:columns/0` with fallbacks:\n"
"\n"
" - if `io:columns/0` fails, it reads the `COLUMNS` environment variable\n"
" - if that is missing/invalid, it defaults to 80 columns\n"
"\n"
" It is designed to be driven by `dream_test/reporters/types.ReporterEvent`,\n"
" but most users should not call it directly. Prefer attaching it via\n"
" `runner.progress_reporter(progress.new())` and letting the runner drive events.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" pub fn main() {\n"
" runner.new([tests()])\n"
" |> runner.progress_reporter(progress.new())\n"
" |> runner.exit_on_failure()\n"
" |> runner.run()\n"
" }\n"
" ```\n"
).
-opaque progress_reporter() :: progress_reporter.
-file("src/dream_test/reporters/progress.gleam", 48).
?DOC(" Create a new progress reporter.\n").
-spec new() -> progress_reporter().
new() ->
progress_reporter.
-file("src/dream_test/reporters/progress.gleam", 211).
-spec pad_or_truncate(binary(), integer()) -> binary().
pad_or_truncate(Text, Width) ->
Graphemes = gleam@string:to_graphemes(Text),
Len = erlang:length(Graphemes),
case Len =:= Width of
true ->
Text;
false ->
case Len < Width of
true ->
<<Text/binary,
(gleam@string:repeat(<<" "/utf8>>, Width - Len))/binary>>;
false ->
gleam@string:join(
gleam@list:take(Graphemes, Width),
<<""/utf8>>
)
end
end.
-file("src/dream_test/reporters/progress.gleam", 231).
-spec clamp_range(integer(), integer(), integer()) -> integer().
clamp_range(N, Min, Max) ->
case N < Min of
true ->
Min;
false ->
case N > Max of
true ->
Max;
false ->
N
end
end.
-file("src/dream_test/reporters/progress.gleam", 196).
-spec render_bar(integer(), integer()) -> binary().
render_bar(Width, Percent) ->
Filled = (Width * clamp_range(Percent, 0, 100)) div 100,
Empty = Width - Filled,
<<(gleam@string:repeat(<<"█"/utf8>>, Filled))/binary,
(gleam@string:repeat(<<"░"/utf8>>, Empty))/binary>>.
-file("src/dream_test/reporters/progress.gleam", 163).
-spec render_line(integer(), integer(), integer(), binary()) -> binary().
render_line(Columns, Completed, Total, Label) ->
Safe_total = case Total =< 0 of
true ->
1;
false ->
Total
end,
Safe_completed = clamp_range(Completed, 0, Safe_total),
Percent = case Safe_total of
0 -> 0;
Gleam@denominator -> Safe_completed * 100 div Gleam@denominator
end,
Counter = <<<<(erlang:integer_to_binary(Safe_completed))/binary, "/"/utf8>>/binary,
(erlang:integer_to_binary(Safe_total))/binary>>,
Percent_text = <<(erlang:integer_to_binary(Percent))/binary, "%"/utf8>>,
Prefix = <<Counter/binary, " "/utf8>>,
Suffix = case Label of
<<""/utf8>> ->
<<" "/utf8, Percent_text/binary>>;
_ ->
<<<<<<" "/utf8, Percent_text/binary>>/binary, " "/utf8>>/binary,
Label/binary>>
end,
Fixed = (string:length(Prefix) + 3) + string:length(Suffix),
Bar_width = clamp_range(Columns - Fixed, 10, Columns),
Bar = <<<<"["/utf8, (render_bar(Bar_width, Percent))/binary>>/binary,
"]"/utf8>>,
Raw = <<<<Prefix/binary, Bar/binary>>/binary, Suffix/binary>>,
pad_or_truncate(Raw, Columns).
-file("src/dream_test/reporters/progress.gleam", 202).
-spec format_result_name(dream_test@types:test_result()) -> binary().
format_result_name(Result) ->
case lists:reverse(erlang:element(3, Result)) of
[] ->
erlang:element(2, Result);
[Leaf] ->
Leaf;
[Leaf@1, Parent | _] ->
<<<<Parent/binary, " › "/utf8>>/binary, Leaf@1/binary>>
end.
-file("src/dream_test/reporters/progress.gleam", 224).
-spec clamp_min(integer(), integer()) -> integer().
clamp_min(N, Min) ->
case N < Min of
true ->
Min;
false ->
N
end.
-file("src/dream_test/reporters/progress.gleam", 138).
?DOC(
" Render a progress bar line for a given terminal width.\n"
"\n"
" This is pure and is intended for testing and for custom reporter work.\n"
"\n"
" Width is measured in **graphemes** (user-visible characters), so Unicode\n"
" stays aligned. The width is clamped to a minimum of 20 so the bar remains\n"
" readable.\n"
"\n"
" Note: The `columns` input is typically a *terminal column count* (from\n"
" `io:columns/0`), but rendering is done by grapheme count so we can safely\n"
" pad/truncate Unicode.\n"
"\n"
" ## Parameters\n"
"\n"
" - `columns`: terminal width (in columns)\n"
" - `event`: event to render\n"
"\n"
" ## Returns\n"
"\n"
" A single line of text **exactly** `columns` graphemes wide (after clamping).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" progress.render(30, reporter_types.RunStarted(total: 10))\n"
" |> should\n"
" |> match_snapshot(\"./test/snapshots/progress_render_run_started.snap\")\n"
" |> or_fail_with(\"expected render output snapshot match\")\n"
" ```\n"
).
-spec render(integer(), dream_test@reporters@types:reporter_event()) -> binary().
render(Columns, Event) ->
Cols = clamp_min(Columns, 20),
case Event of
{run_started, Total} ->
render_line(Cols, 0, Total, <<""/utf8>>);
{test_finished, Completed, Total@1, Result} ->
render_line(Cols, Completed, Total@1, format_result_name(Result));
{run_finished, Completed@1, Total@2, _} ->
render_line(Cols, Completed@1, Total@2, <<"done"/utf8>>);
_ ->
render_line(Cols, 0, 1, <<""/utf8>>)
end.
-file("src/dream_test/reporters/progress.gleam", 93).
?DOC(
" Handle a single reporter event by writing an in-place progress bar line.\n"
"\n"
" - For `RunStarted`, prints an initial 0% bar.\n"
" - For `TestFinished`, prints an updated bar using the included counts.\n"
" - For `RunFinished`, prints a final 100% bar and a newline.\n"
"\n"
" This function ignores hook events (`HookStarted` / `HookFinished`) so hook\n"
" chatter doesn’t scramble the single-line UI.\n"
"\n"
" ## Parameters\n"
"\n"
" - `event`: a `ReporterEvent` emitted by the runner\n"
" - `write`: an output sink (typically `io.print`). For best results this\n"
" should write **without adding extra newlines**.\n"
"\n"
" ## When should I use this?\n"
"\n"
" Usually you shouldn’t call it directly—prefer attaching it via\n"
" `runner.progress_reporter(progress.new())` and letting the runner drive events.\n"
"\n"
" You may call it directly only if you are building your own reporter/driver\n"
" and you are already receiving `ReporterEvent`s.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" progress.handle_event(\n"
" reporter_types.RunStarted(total: 10),\n"
" write_progress_line_to_file,\n"
" )\n"
"\n"
" use text <- result.try(\n"
" file.read(\"test/tmp/progress_handle_event.txt\")\n"
" |> result.map_error(file.error_to_string),\n"
" )\n"
"\n"
" text\n"
" |> should\n"
" |> match_snapshot(\"./test/snapshots/progress_handle_event_run_started.snap\")\n"
" |> or_fail_with(\"expected handle_event output snapshot match\")\n"
" ```\n"
).
-spec handle_event(
progress_reporter(),
dream_test@reporters@types:reporter_event()
) -> gleam@option:option(binary()).
handle_event(Reporter, Event) ->
_ = Reporter,
Cols = dream_test_reporter_progress_ffi:columns(),
Line = render(Cols, Event),
case Event of
{hook_started, _, _, _} ->
none;
{hook_finished, _, _, _, _} ->
none;
{run_finished, _, _, _} ->
{some, <<<<"\r"/utf8, Line/binary>>/binary, "\n"/utf8>>};
_ ->
{some, <<"\r"/utf8, Line/binary>>}
end.