diff --git a/t/unit/App/Yath/Renderer.t b/t/unit/App/Yath/Renderer.t index 22278e5d9..df1ad3df8 100644 --- a/t/unit/App/Yath/Renderer.t +++ b/t/unit/App/Yath/Renderer.t @@ -1,5 +1,63 @@ use Test2::V0 -target => 'App::Yath::Renderer'; -skip_all "write me"; +# Minimal subclass used to test the base class contract +package App::Yath::Renderer::TestSub; +use parent 'App::Yath::Renderer'; +sub render_event {} + +package main; + +my $settings = bless({}, 'MockSettings'); + +# --- Construction --- + +like( + dies { $CLASS->new() }, + qr/'settings' is required/, + "init() croaks without settings", +); + +ok(my $r = $CLASS->new(settings => $settings), "can construct with settings"); + +# --- Inheritance & interface --- + +can_ok($CLASS, qw/ + init render_event + start step signal finish exit_hook + weight end_of_events +/); + +# --- render_event must be overridden --- + +like( + dies { $r->render_event({}) }, + qr/forgot to override 'render_event\(\)'/, + "base render_event() croaks — subclass must override", +); + +# --- Default weight --- + +is($r->weight, 0, "weight() returns 0 by default"); + +# --- Lifecycle hooks are no-ops --- + +ok(lives { $r->start() }, "start() does not die"); +ok(lives { $r->step() }, "step() does not die"); +ok(lives { $r->signal('INT') }, "signal() does not die"); +ok(lives { $r->finish() }, "finish() does not die"); +ok(lives { $r->exit_hook() }, "exit_hook() does not die"); +ok(lives { $r->end_of_events() }, "end_of_events() does not die"); + +# --- Subclass satisfies the contract --- + +ok( + my $sub = App::Yath::Renderer::TestSub->new(settings => $settings), + "subclass with render_event can be constructed", +); +ok(lives { $sub->render_event({}) }, "subclass render_event() does not die"); + +# --- Attributes are accessible --- + +is($r->settings, $settings, "settings attribute is stored"); done_testing; diff --git a/t/unit/App/Yath/Renderer/DB.t b/t/unit/App/Yath/Renderer/DB.t index 46e64f18f..d857ff9f9 100644 --- a/t/unit/App/Yath/Renderer/DB.t +++ b/t/unit/App/Yath/Renderer/DB.t @@ -1,5 +1,44 @@ -use Test2::V0 -target => 'App::Yath::Renderer::DB'; +use Test2::V0; -skip_all "write me"; +# App::Yath::Renderer::DB requires several optional modules (Consumer::NonBlock, +# App::Yath::Schema::RunProcessor, etc.). Skip gracefully when they are absent. +eval { require App::Yath::Renderer::DB; 1 } + or skip_all "App::Yath::Renderer::DB requires optional dependencies: $@"; + +our $CLASS = 'App::Yath::Renderer::DB'; + +# DB's init() calls $settings->yath->project which must return a truthy value. +package MockYath { + sub new { bless {project => 'test-project'}, shift } + sub project { $_[0]->{project} } +} +package MockSettings { + sub new { bless {}, shift } + sub yath { MockYath->new() } +} + +my $settings = MockSettings->new(); + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/init start render_event finish/); + +# --- Construction --- + +ok(my $r = $CLASS->new(settings => $settings), "can construct"); + +# --- Tracking attributes --- + +ok(!defined($r->pid), "pid starts undef"); +ok(!defined($r->writer), "writer starts undef"); +ok(!defined($r->stopped), "stopped starts undef"); + +# --- render_event is a no-op (data flows to the writer process) --- + +ok(lives { $r->render_event({}) }, "render_event() does not die without a writer"); done_testing; diff --git a/t/unit/App/Yath/Renderer/Default.t b/t/unit/App/Yath/Renderer/Default.t index 161792221..54bd9231c 100644 --- a/t/unit/App/Yath/Renderer/Default.t +++ b/t/unit/App/Yath/Renderer/Default.t @@ -1,5 +1,71 @@ -use Test2::V0 -target => 'App::Yath::Renderer::Default'; +use Test2::V0; -skip_all "write me"; +eval { require App::Yath::Renderer::Default; 1 } + or skip_all "App::Yath::Renderer::Default requires optional dependencies: $@"; + +eval { require App::Yath::Theme; 1 } + or skip_all "App::Yath::Theme required: $@"; + +our $CLASS = 'App::Yath::Renderer::Default'; + +my $settings = bless({}, 'MockSettings'); +my $theme = App::Yath::Theme->new(); + +sub make_renderer { + my %extra = @_; + return $CLASS->new( + settings => $settings, + theme => $theme, + color => 0, + %extra, + ); +} + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/init render_event write finish step/); + +# --- Construction --- + +ok(my $r = make_renderer(), "can construct with required arguments"); + +# --- Defaults set by init() --- + +ok(defined $r->verbose, "verbose is set after init"); +ok(defined $r->start_time, "start_time is set after init"); + +# --- IO handle is initialised --- + +ok($r->io, "io handle is set after init"); + +# --- Composer is initialised --- + +isa_ok($r->{composer}, ['App::Yath::Renderer::Default::Composer'], + "composer is an instance of the Composer class"); + +# --- color is false when explicitly disabled --- + +my $no_color = make_renderer(color => 0); +ok(!$no_color->color, "color is false when color => 0"); + +# --- weight defaults to 0 (inherited) --- + +is($r->weight, 0, "weight() returns 0"); + +# --- render_event does not die for a minimal event --- + +ok( + lives { + $r->render_event({ + facet_data => {}, + stamp => time(), + }); + }, + "render_event() does not die for a minimal event", +); done_testing; diff --git a/t/unit/App/Yath/Renderer/Default/Composer.t b/t/unit/App/Yath/Renderer/Default/Composer.t index afea39ce9..2e5692f13 100644 --- a/t/unit/App/Yath/Renderer/Default/Composer.t +++ b/t/unit/App/Yath/Renderer/Default/Composer.t @@ -1,5 +1,93 @@ use Test2::V0 -target => 'App::Yath::Renderer::Default::Composer'; -skip_all "write me"; +# --- Construction --- + +ok(my $c = $CLASS->new(), "can construct"); +ref_ok($c, 'HASH', "instance is a hashref"); + +# --- Interface --- + +can_ok($CLASS, qw/ + new + render_one_line + render_verbose + render_super_verbose + render_brief + render_assert + render_plan + render_info + render_errors +/); + +# --- render_one_line: passing assert --- + +{ + my $f = { assert => { pass => 1, details => 'test description' } }; + my $out = $c->render_one_line($f); + ref_ok($out, 'ARRAY', "render_one_line returns arrayref for assert"); + is($out->[1], 'PASS', "tag is PASS for a passing assert"); +} + +# --- render_one_line: failing assert --- + +{ + my $f = { assert => { pass => 0, details => 'oops' } }; + my $out = $c->render_one_line($f); + ref_ok($out, 'ARRAY', "render_one_line returns arrayref for failing assert"); + is($out->[1], 'FAIL', "tag is FAIL for a failing assert"); +} + +# --- render_one_line: plan --- + +{ + my $f = { plan => { count => 3 } }; + my $out = $c->render_one_line($f); + ref_ok($out, 'ARRAY', "render_one_line returns arrayref for plan"); +} + +# --- render_one_line: empty facet_data returns undef --- + +{ + my $out = $c->render_one_line({}); + ok(!defined($out), "render_one_line returns undef for empty facet data"); +} + +# --- render_verbose: returns arrayref of lines --- + +{ + my $f = { plan => { count => 5 } }; + my $out = $c->render_verbose($f); + ref_ok($out, 'ARRAY', "render_verbose returns arrayref"); + ok(scalar(@$out) > 0, "render_verbose returns at least one line for plan"); +} + +# --- render_verbose: passing assert --- + +{ + my $f = { assert => { pass => 1, details => 'ok' } }; + my $out = $c->render_verbose($f); + ref_ok($out, 'ARRAY', "render_verbose returns arrayref for passing assert"); + ok(scalar(@$out) > 0, "render_verbose returns lines for assert"); +} + +# --- render_verbose: failing assert includes debug output --- + +{ + my $f = { + assert => { pass => 0, details => 'nope' }, + errors => [{ tag => 'ERROR', details => 'something went wrong', fail => 1 }], + }; + my $out = $c->render_verbose($f); + ref_ok($out, 'ARRAY', "render_verbose returns arrayref for failing assert"); + ok(scalar(@$out) > 0, "render_verbose returns lines for failing assert"); +} + +# --- render_super_verbose: wraps render_verbose --- + +{ + my $f = { assert => { pass => 1, details => 'test' } }; + my $out = $c->render_super_verbose($f); + ref_ok($out, 'ARRAY', "render_super_verbose returns arrayref"); +} done_testing; diff --git a/t/unit/App/Yath/Renderer/Formatter.t b/t/unit/App/Yath/Renderer/Formatter.t index 3ac7351cf..2f1d40608 100644 --- a/t/unit/App/Yath/Renderer/Formatter.t +++ b/t/unit/App/Yath/Renderer/Formatter.t @@ -1,5 +1,80 @@ -use Test2::V0 -target => 'App::Yath::Renderer::Formatter'; +use Test2::V0; -skip_all "write me"; +eval { require App::Yath::Renderer::Formatter; 1 } + or skip_all "App::Yath::Renderer::Formatter requires optional dependencies: $@"; + +our $CLASS = 'App::Yath::Renderer::Formatter'; + +# We use a minimal mock formatter so we can instantiate the renderer without a +# real Test2 formatter being present. +{ + package MockFormatter; + sub new { bless {}, shift } + sub write { } + sub step { } + sub finalize { } + sub can { + my ($self, $meth) = @_; + return $self->SUPER::can($meth); + } +} +$INC{'MockFormatter.pm'} = 1; + +my $settings = bless({}, 'MockSettings'); + +sub make_renderer { + my %extra = @_; + return $CLASS->new( + settings => $settings, + formatter => 'MockFormatter', + io => \*STDOUT, + io_err => \*STDERR, + %extra, + ); +} + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/init render_event step finish formatter/); + +# --- Construction --- + +ok(my $r = make_renderer(), "can construct with a mock formatter"); + +# --- formatter attribute is the instantiated formatter object --- + +isa_ok($r->formatter, ['MockFormatter'], "formatter attribute holds a MockFormatter instance"); + +# --- do_step is set based on whether formatter has step() --- + +ok($r->do_step, "do_step is true when formatter has step()"); + +# --- show_job_end defaults to 1 --- + +ok($r->show_job_end, "show_job_end defaults to 1"); + +# --- render_event does not die for a minimal event --- + +ok( + lives { + $r->render_event({ + facet_data => {}, + stamp => time(), + }); + }, + "render_event() does not die for a minimal event", +); + +# --- step() delegates to the formatter --- + +ok(lives { $r->step() }, "step() does not die"); + +# --- finish() calls formatter finalize() --- + +ok(lives { $r->finish() }, "finish() does not die"); done_testing; diff --git a/t/unit/App/Yath/Renderer/JUnit.t b/t/unit/App/Yath/Renderer/JUnit.t index 803520a64..78540409c 100644 --- a/t/unit/App/Yath/Renderer/JUnit.t +++ b/t/unit/App/Yath/Renderer/JUnit.t @@ -1,5 +1,60 @@ -use Test2::V0 -target => 'App::Yath::Renderer::JUnit'; +use Test2::V0; -skip_all "write me"; +# JUnit renderer requires XML::Generator. Skip gracefully if not installed. +eval { require App::Yath::Renderer::JUnit; 1 } + or skip_all "App::Yath::Renderer::JUnit requires optional dependencies: $@"; + +our $CLASS = 'App::Yath::Renderer::JUnit'; + +my $settings = bless({}, 'MockSettings'); + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/init render_event finish/); + +# --- Construction --- + +ok(my $r = $CLASS->new(settings => $settings), "can construct"); + +# --- init() sets up internal data structures --- + +ok($r->{xml}, "init() creates xml generator"); +ok($r->{xml_content}, "init() creates xml_content arrayref"); +ref_ok($r->{xml_content}, 'ARRAY', "xml_content is an arrayref"); +ref_ok($r->{tests}, 'HASH', "init() creates tests hashref"); + +# --- Default junit_file --- + +ok($r->{junit_file}, "init() sets junit_file"); + +# --- allow_passing_todos respects env var --- + +{ + local $ENV{ALLOW_PASSING_TODOS} = ''; + my $r2 = $CLASS->new(settings => $settings); + ok(!$r2->{allow_passing_todos}, "allow_passing_todos is false when env var not set"); +} + +{ + local $ENV{ALLOW_PASSING_TODOS} = '1'; + my $r3 = $CLASS->new(settings => $settings); + ok($r3->{allow_passing_todos}, "allow_passing_todos is true when env var is set"); +} + +# --- render_event: event without a job_id is silently ignored --- + +ok( + lives { + $r->render_event({ + facet_data => { harness => {} }, + stamp => time(), + }); + }, + "render_event() does not die for event without job_id", +); done_testing; diff --git a/t/unit/App/Yath/Renderer/Logger.t b/t/unit/App/Yath/Renderer/Logger.t index 726d66707..6a50697ad 100644 --- a/t/unit/App/Yath/Renderer/Logger.t +++ b/t/unit/App/Yath/Renderer/Logger.t @@ -1,5 +1,134 @@ -use Test2::V0 -target => 'App::Yath::Renderer::Logger'; +use Test2::V0; -skip_all "write me"; +eval { require App::Yath::Renderer::Logger; 1 } + or skip_all "App::Yath::Renderer::Logger requires optional dependencies: $@"; + +our $CLASS = 'App::Yath::Renderer::Logger'; + +# Utility functions in Logger.pm are package subs, not methods. +my $expand = App::Yath::Renderer::Logger->can('expand'); +my $expand_format = App::Yath::Renderer::Logger->can('expand_log_file_format'); +my $normalize = App::Yath::Renderer::Logger->can('normalize_log_file'); + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/start render_event finish weight/); + +# --- weight --- + +is(App::Yath::Renderer::Logger->new(settings => bless({}, 'MockSettings'))->weight, + -100, "weight() is -100"); + +# --- expand(): letter u returns current USER --- + +is($expand->('u', undef), $ENV{USER}, "expand 'u' returns \$ENV{USER}"); + +# --- expand(): letter p returns PID --- + +is($expand->('p', undef), $$, "expand 'p' returns current PID"); + +# --- expand(): unknown letter is passed through unchanged --- + +is($expand->('Z', undef), '%!Z', "expand unknown letter returns literal %!Z"); + +# --- expand(): letter P returns empty string when no project --- + +{ + package MockYath; + sub new { bless {}, shift } + sub project { undef } + + package MockSettingsForP; + sub new { bless {}, shift } + sub yath { MockYath->new() } + sub maybe { undef } + + package main; +} + +my $s_no_project = MockSettingsForP->new(); +is($expand->('P', $s_no_project), '', "expand 'P' returns empty string when no project"); + +# --- expand(): letter P appends ~ when project is set --- + +{ + package MockYathWithProject; + sub new { bless {}, shift } + sub project { 'myproject' } + + package MockSettingsWithProject; + sub new { bless {}, shift } + sub yath { MockYathWithProject->new() } + sub maybe { undef } + + package main; +} + +my $s_project = MockSettingsWithProject->new(); +is($expand->('P', $s_project), 'myproject~', "expand 'P' returns 'project~'"); + +# --- normalize_log_file(): no auto_ext leaves filename unchanged (except clean_path) --- + +{ + package MockLogging; + sub new { bless {}, shift } + sub auto_ext { 0 } + sub bzip2 { 0 } + sub gzip { 0 } + + package MockSettingsForNorm; + sub new { bless {}, shift } + sub logging { MockLogging->new() } + + package main; +} + +my $norm_settings = MockSettingsForNorm->new(); +my $result = $normalize->('/tmp/mylog.jsonl', $norm_settings); +like($result, qr{mylog\.jsonl$}, "normalize_log_file preserves .jsonl extension"); + +# --- normalize_log_file(): auto_ext adds .jsonl if missing --- + +{ + package MockLoggingAutoExt; + sub new { bless {}, shift } + sub auto_ext { 1 } + sub bzip2 { 0 } + sub gzip { 0 } + + package MockSettingsAutoExt; + sub new { bless {}, shift } + sub logging { MockLoggingAutoExt->new() } + + package main; +} + +my $ae_settings = MockSettingsAutoExt->new(); +my $ae_result = $normalize->('/tmp/mylog', $ae_settings); +like($ae_result, qr{mylog\.jsonl$}, "normalize_log_file adds .jsonl when auto_ext is on"); + +# --- normalize_log_file(): auto_ext + gzip adds .gz suffix --- + +{ + package MockLoggingGzip; + sub new { bless {}, shift } + sub auto_ext { 1 } + sub bzip2 { 0 } + sub gzip { 1 } + + package MockSettingsGzip; + sub new { bless {}, shift } + sub logging { MockLoggingGzip->new() } + + package main; +} + +my $gz_settings = MockSettingsGzip->new(); +my $gz_result = $normalize->('/tmp/mylog', $gz_settings); +like($gz_result, qr{mylog\.jsonl\.gz$}, "normalize_log_file adds .jsonl.gz when auto_ext+gzip"); done_testing; diff --git a/t/unit/App/Yath/Renderer/Notify.t b/t/unit/App/Yath/Renderer/Notify.t index 2dc10883c..af64e0d8d 100644 --- a/t/unit/App/Yath/Renderer/Notify.t +++ b/t/unit/App/Yath/Renderer/Notify.t @@ -1,5 +1,55 @@ -use Test2::V0 -target => 'App::Yath::Renderer::Notify'; +use Test2::V0; -skip_all "write me"; +eval { require App::Yath::Renderer::Notify; 1 } + or skip_all "App::Yath::Renderer::Notify requires optional dependencies: $@"; + +our $CLASS = 'App::Yath::Renderer::Notify'; + +# Minimal mock settings: provides the notify group with text_module => undef +# so render_event can run its full logic without errors. +{ + package MockNotifyGroup; + sub new { bless {}, shift } + sub text_module { undef } + + package MockNotifySettings; + sub new { bless {}, shift } + sub notify { MockNotifyGroup->new() } +} + +my $settings = MockNotifySettings->new(); + +# Mock event object whose facet_data returns an empty hashref. +{ + package MockNotifyEvent; + sub new { bless {}, shift } + sub facet_data { {} } +} + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/render_event exit_hook/); + +# --- Construction --- + +ok(my $r = $CLASS->new(settings => $settings), "can construct"); + +# --- render_event does not die for an event with no failures --- + +ok( + lives { $r->render_event(MockNotifyEvent->new()) }, + "render_event() does not die for a no-failure event", +); + +# --- Tracking attributes start undef --- + +ok(!defined($r->final), "final attribute starts undef"); +ok(!defined($r->tries), "tries attribute starts undef"); +ok(!defined($r->problems), "problems attribute starts undef"); +ok(!defined($r->problem_cids), "problem_cids attribute starts undef"); done_testing; diff --git a/t/unit/App/Yath/Renderer/QVF.t b/t/unit/App/Yath/Renderer/QVF.t index e06992d24..da09d68e8 100644 --- a/t/unit/App/Yath/Renderer/QVF.t +++ b/t/unit/App/Yath/Renderer/QVF.t @@ -1,5 +1,57 @@ -use Test2::V0 -target => 'App::Yath::Renderer::QVF'; +use Test2::V0; -skip_all "write me"; +eval { require App::Yath::Renderer::QVF; 1 } + or skip_all "App::Yath::Renderer::QVF requires optional dependencies: $@"; + +eval { require App::Yath::Theme; 1 } + or skip_all "App::Yath::Theme required: $@"; + +our $CLASS = 'App::Yath::Renderer::QVF'; + +my $settings = bless({}, 'MockSettings'); +my $theme = App::Yath::Theme->new(); + +sub make_renderer { + my %extra = @_; + return $CLASS->new( + settings => $settings, + theme => $theme, + color => 0, + %extra, + ); +} + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer::Default'], "inherits from App::Yath::Renderer::Default"); +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/init write update_active_disp/); + +# --- init(): sets verbose to 100 when original verbose was 0 --- + +{ + my $r = make_renderer(verbose => 0); + is($r->verbose, 100, "init() sets verbose to 100 when unset"); + is($r->real_verbose, 0, "real_verbose stores original value 0"); +} + +# --- init(): preserves a non-zero verbose but still captures it in real_verbose --- + +{ + my $r = make_renderer(verbose => 2); + is($r->verbose, 2, "init() keeps explicit verbose value when truthy"); + is($r->real_verbose, 2, "real_verbose stores original verbose 2"); +} + +# --- job_buffers starts empty --- + +{ + my $r = make_renderer(); + my $bufs = $r->job_buffers; + ok(!defined($bufs) || ref($bufs) eq 'HASH', "job_buffers is undef or hashref"); +} done_testing; diff --git a/t/unit/App/Yath/Renderer/ResetTerm.t b/t/unit/App/Yath/Renderer/ResetTerm.t index c8350dd55..1093a179e 100644 --- a/t/unit/App/Yath/Renderer/ResetTerm.t +++ b/t/unit/App/Yath/Renderer/ResetTerm.t @@ -1,5 +1,34 @@ use Test2::V0 -target => 'App::Yath::Renderer::ResetTerm'; +use Capture::Tiny qw/capture/; -skip_all "write me"; +my $settings = bless({}, 'MockSettings'); + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/render_event finish weight/); + +# --- Construction --- + +ok(my $r = $CLASS->new(settings => $settings), "can construct"); + +# --- render_event is a no-op --- + +ok(lives { $r->render_event({}) }, "render_event({}) does not die"); +ok(lives { $r->render_event(undef) }, "render_event(undef) does not die"); + +# --- weight --- + +is($r->weight, -99999999, "weight() is -99999999"); +ok($r->weight < 0, "weight() is negative (runs late)"); + +# --- finish() is silent when STDOUT is not a TTY --- +# In a test run STDOUT is not a TTY, so no escape sequences are printed. + +my ($out) = capture { $r->finish() }; +is($out, '', "finish() produces no output when STDOUT is not a TTY"); done_testing; diff --git a/t/unit/App/Yath/Renderer/Server.t b/t/unit/App/Yath/Renderer/Server.t index 5ccb6e37f..a956b0894 100644 --- a/t/unit/App/Yath/Renderer/Server.t +++ b/t/unit/App/Yath/Renderer/Server.t @@ -1,5 +1,40 @@ -use Test2::V0 -target => 'App::Yath::Renderer::Server'; +use Test2::V0; -skip_all "write me"; +# App::Yath::Renderer::Server requires Plack and other web-server dependencies. +# Skip gracefully when they are absent. +eval { require App::Yath::Renderer::Server; 1 } + or skip_all "App::Yath::Renderer::Server requires optional dependencies: $@"; + +our $CLASS = 'App::Yath::Renderer::Server'; + +# Server inherits from DB whose init() calls $settings->yath->project. +package MockYath { + sub new { bless {project => 'test-project'}, shift } + sub project { $_[0]->{project} } +} +package MockSettings { + sub new { bless {}, shift } + sub yath { MockYath->new() } +} + +my $settings = MockSettings->new(); + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer::DB'], "inherits from App::Yath::Renderer::DB"); +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/start exit_hook/); + +# --- Construction --- + +ok(my $r = $CLASS->new(settings => $settings), "can construct"); + +# --- Attributes start undef --- + +ok(!defined($r->config), "config attribute starts undef"); +ok(!defined($r->server), "server attribute starts undef"); done_testing; diff --git a/t/unit/App/Yath/Renderer/Summary.t b/t/unit/App/Yath/Renderer/Summary.t index 8bb9be7cc..e64542c64 100644 --- a/t/unit/App/Yath/Renderer/Summary.t +++ b/t/unit/App/Yath/Renderer/Summary.t @@ -1,5 +1,117 @@ -use Test2::V0 -target => 'App::Yath::Renderer::Summary'; +use Test2::V0; -skip_all "write me"; +eval { require App::Yath::Renderer::Summary; 1 } + or skip_all "App::Yath::Renderer::Summary requires optional dependencies: $@"; + +eval { require App::Yath::Theme; 1 } + or skip_all "App::Yath::Theme required: $@"; + +use Capture::Tiny qw/capture/; + +our $CLASS = 'App::Yath::Renderer::Summary'; + +my $settings = bless({}, 'MockSettings'); +my $theme = App::Yath::Theme->new(); + +sub make_renderer { + my %extra = @_; + return $CLASS->new( + settings => $settings, + theme => $theme, + quiet => 0, + color => 0, + %extra, + ); +} + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/render_event exit_hook weight render_summary render_final_data write_summary_file/); + +# --- Construction --- + +ok(my $r = make_renderer(), "can construct"); + +# --- render_event is a no-op --- + +ok(lives { $r->render_event({}) }, "render_event() does not die"); + +# --- weight --- + +is($r->weight, -99, "weight() is -99"); + +# --- render_summary: passing run --- + +{ + my $renderer = make_renderer(); + my ($out) = capture { + $renderer->render_summary({ + pass => 1, + failures => 0, + tests_seen => 5, + asserts_seen => 42, + }); + }; + + like($out, qr/PASSED/, "render_summary shows PASSED for passing run"); + like($out, qr/File Count.*5/, "render_summary shows file count"); + like($out, qr/Assertion Count.*42/, "render_summary shows assertion count"); +} + +# --- render_summary: failing run --- + +{ + my $renderer = make_renderer(); + my ($out) = capture { + $renderer->render_summary({ + pass => 0, + failures => 2, + tests_seen => 3, + asserts_seen => 10, + }); + }; + + like($out, qr/FAILED/, "render_summary shows FAILED for failing run"); + like($out, qr/Fail Count.*2/, "render_summary shows failure count"); +} + +# --- render_summary: quiet > 1 suppresses output --- + +{ + my $renderer = make_renderer(quiet => 2); + my ($out) = capture { + $renderer->render_summary({ + pass => 1, + failures => 0, + tests_seen => 1, + asserts_seen => 1, + }); + }; + is($out, '', "render_summary produces no output when quiet > 1"); +} + +# --- render_final_data: failed jobs --- + +{ + my $renderer = make_renderer(); + my ($out) = capture { + $renderer->render_final_data({ + failed => [['job1', 't/foo.t', []]], + }); + }; + like($out, qr/failed/i, "render_final_data shows failed jobs section"); +} + +# --- write_summary_file: no file configured means no write --- + +{ + my $renderer = make_renderer(); + ok(lives { $renderer->write_summary_file({pass => 1, tests_seen => 1, asserts_seen => 1}, {}) }, + "write_summary_file does not die when no file is configured"); +} done_testing; diff --git a/t/unit/App/Yath/Renderer/TAPHarness.t b/t/unit/App/Yath/Renderer/TAPHarness.t index 8579d416d..08141c367 100644 --- a/t/unit/App/Yath/Renderer/TAPHarness.t +++ b/t/unit/App/Yath/Renderer/TAPHarness.t @@ -1,5 +1,36 @@ use Test2::V0 -target => 'App::Yath::Renderer::TAPHarness'; -skip_all "write me"; +my $settings = bless({}, 'MockSettings'); + +# --- Inheritance --- + +isa_ok($CLASS, ['App::Yath::Renderer'], "inherits from App::Yath::Renderer"); + +# --- Interface --- + +can_ok($CLASS, qw/render_event finish/); + +# --- Construction --- + +ok(my $r = $CLASS->new(settings => $settings), "can construct"); + +# --- render_event is a no-op --- + +ok(lives { $r->render_event({}) }, "render_event({}) does not die"); +ok(lives { $r->render_event(undef) }, "render_event(undef) does not die"); + +# --- finish() sets $TAP::Harness::Yath::SUMMARY --- +# We provide a minimal mock auditor whose summary() method returns a sentinel. + +{ + package MockAuditor; + sub new { bless {}, shift } + sub summary { 'test-summary-sentinel' } +} + +$r->finish(MockAuditor->new()); +no strict 'refs'; +is(${'TAP::Harness::Yath::SUMMARY'}, 'test-summary-sentinel', + "finish() stores auditor->summary in \$TAP::Harness::Yath::SUMMARY"); done_testing; diff --git a/t/unit/Test2/Formatter/Stream.t b/t/unit/Test2/Formatter/Stream.t index 69a10a5c0..ed73b38d5 100644 --- a/t/unit/Test2/Formatter/Stream.t +++ b/t/unit/Test2/Formatter/Stream.t @@ -1,5 +1,29 @@ -use Test2::V0; # -target => 'Test2::Formatter::Stream' +use Test2::V0; -skip_all "write me"; +# Test2::Formatter::Stream imports Test2::Harness::Collector::Child at compile +# time, which requires the process to be running inside a yath collector. When +# loaded outside that context it dies with "We do not appear to be inside a +# collector". Skip gracefully in that case. +eval { require Test2::Formatter::Stream; 1 } + or skip_all "Test2::Formatter::Stream requires a collector context: $@"; + +our $CLASS = 'Test2::Formatter::Stream'; + +# --- Inheritance --- + +isa_ok($CLASS, ['Test2::Formatter'], "inherits from Test2::Formatter"); + +# --- Interface --- + +can_ok($CLASS, qw/ + init record encoding write + hide_buffered handles + set_no_header set_no_diag set_no_numbers set_handles + terminate finalize +/); + +# --- hide_buffered class method --- + +ok(!$CLASS->hide_buffered, "hide_buffered() returns false"); done_testing;