Developer-Dashboard
view release on metacpan or search on metacpan
t/11-coverage-closure.t view on Meta::CPAN
use Developer::Dashboard::PageResolver;
use Developer::Dashboard::PageRuntime;
use Developer::Dashboard::PageStore;
use Developer::Dashboard::PathRegistry;
use Developer::Dashboard::Prompt;
use Developer::Dashboard::SessionStore;
use Developer::Dashboard::Web::App;
# dies_like($code, $pattern, $label)
# Runs a code reference and asserts that it dies with the expected pattern.
# Input: code reference, regex pattern, and test label.
# Output: Test::More assertion result.
sub dies_like {
my ( $code, $pattern, $label ) = @_;
my $error = eval { $code->(); 1 } ? '' : $@;
like( $error, $pattern, $label );
}
my $original_cwd = getcwd();
my $home = tempdir(CLEANUP => 1);
local $ENV{HOME} = $home;
local $ENV{DEVELOPER_DASHBOARD_BOOKMARKS};
local $ENV{DEVELOPER_DASHBOARD_CONFIGS};
local $ENV{DEVELOPER_DASHBOARD_CHECKERS};
chdir $home or die "Unable to chdir to $home: $!";
my $repo = File::Spec->catdir( $home, 'projects', 'coverage-app' );
my $bin = File::Spec->catdir( $home, 'bin' );
make_path( File::Spec->catdir( $repo, '.git' ), $bin );
open my $compose_fh, '>', File::Spec->catfile( $repo, 'compose.yaml' ) or die $!;
print {$compose_fh} "services:\n app:\n image: perl:latest\n";
close $compose_fh;
open my $compose_dev_fh, '>', File::Spec->catfile( $repo, 'compose.dev.yaml' ) or die $!;
print {$compose_dev_fh} "services:\n app:\n environment:\n MODE: dev\n";
close $compose_dev_fh;
open my $compose_worker_fh, '>', File::Spec->catfile( $repo, 'compose.worker.yaml' ) or die $!;
print {$compose_worker_fh} "services:\n worker:\n image: perl:latest\n";
close $compose_worker_fh;
open my $compose_debug_fh, '>', File::Spec->catfile( $repo, 'compose.debug.yaml' ) or die $!;
print {$compose_debug_fh} "services:\n debug:\n image: alpine\n";
close $compose_debug_fh;
open my $compose_test_fh, '>', File::Spec->catfile( $repo, 'compose.test.yaml' ) or die $!;
print {$compose_test_fh} "services:\n test:\n image: alpine\n";
close $compose_test_fh;
open my $repo_cfg_fh, '>', File::Spec->catfile( $repo, '.developer-dashboard.json' ) or die $!;
print {$repo_cfg_fh} <<'JSON';
{
"nested": { "repo": 1 },
"collectors": [
{ "name": "repo.collector", "command": "printf repo", "cwd": "home", "interval": 5 },
{ "name": "cfg.collector", "command": "printf cfg", "cwd": "home", "interval": 7 }
],
"providers": [
{ "id": "cfg-provider", "title": "Config Provider", "body": "cfg page body" },
{ "id": "shared-provider", "page": { "id": "shared-provider", "title": "Shared Provider", "layout": { "body": "shared body" } } }
],
"docker": {
"files": ["compose.dev.yaml", "compose.test.yaml"],
"project_overlays": ["compose.test.yaml"],
"services": {
"worker": { "files": ["compose.worker.yaml"] }
},
"addons": {
"debug": {
"files": ["compose.debug.yaml"],
"modes": ["dev"],
"env": { "DEBUG_ENABLED": "1" }
},
"extra": {
"files": ["compose.debug.yaml"]
}
},
"modes": {
"dev": {
"files": ["compose.dev.yaml"],
"env": { "APP_MODE": "dev" }
}
}
}
}
JSON
close $repo_cfg_fh;
open my $docker_bin_fh, '>', File::Spec->catfile( $bin, 'docker' ) or die $!;
print {$docker_bin_fh} <<"SH";
#!/bin/sh
printf 'ARGS:%s\n' "\$*"
printf 'DEBUG:%s\n' "\${DEBUG_ENABLED:-}"
printf 'MODE:%s\n' "\${APP_MODE:-}"
SH
close $docker_bin_fh;
chmod 0755, File::Spec->catfile( $bin, 'docker' );
local $ENV{PATH} = $bin . ':' . ( $ENV{PATH} || '' );
my $paths = Developer::Dashboard::PathRegistry->new(
home => $home,
workspace_roots => [ File::Spec->catdir( $home, 'projects' ) ],
project_roots => [ File::Spec->catdir( $home, 'projects' ) ],
);
my $files = Developer::Dashboard::FileRegistry->new( paths => $paths );
my $config = Developer::Dashboard::Config->new( files => $files, paths => $paths, repo_root => $repo );
$config->save_global(
{
nested => { global => 1 },
collectors => [
{ name => 'global.collector', command => 'printf global', cwd => 'home', interval => 3 },
],
}
);
my $merged = $config->merged;
ok( $merged->{nested}{global}, 'config merged keeps global nested hash values' );
ok( $merged->{nested}{repo}, 'config merged keeps repo nested hash values' );
my $jobs = $config->collectors;
is( scalar( grep { $_->{name} eq 'repo.collector' } @$jobs ), 1, 'config collectors include repo collectors' );
is( scalar( grep { $_->{name} eq 'cfg.collector' } @$jobs ), 1, 'config collectors include additional config collectors' );
my $pages = Developer::Dashboard::PageStore->new( paths => $paths );
my $actions = Developer::Dashboard::ActionRunner->new( files => $files, paths => $paths );
t/11-coverage-closure.t view on Meta::CPAN
my $prepare_merge_page = Developer::Dashboard::PageDocument->new(
title => 'Developer Dashboard',
layout => { body => '<h1>[% title %]</h1>[% stash.a %]' },
meta => {
codes => [
{ id => 'CODE1', body => '{ a => 1 }' },
{ id => 'CODE2', body => 'hide print $a' },
],
},
);
my $prepare_merge_result = $runtime->prepare_page(
page => $prepare_merge_page,
source => 'saved',
);
like( $prepare_merge_result->{layout}{body}, qr{<h1>Developer Dashboard</h1>1}, 'prepare_page renders returned CODE hash values into HTML stash data' );
like( join( '', @{ $prepare_merge_result->{meta}{runtime_outputs} || [] } ), qr/a => 1/, 'returned CODE hash values are dumped into rendered runtime output' );
like( join( '', @{ $prepare_merge_result->{meta}{runtime_outputs} || [] } ), qr/1/, 'hide print keeps printed output but suppresses the print return value' );
my $stop_result = $runtime->run_code_blocks(
page => Developer::Dashboard::PageDocument->new(
meta => { codes => [ { id => 'CODE1', body => 'stop("halt");' }, { id => 'CODE2', body => 'print "later";' } ] },
),
source => 'saved',
);
like( $stop_result->{errors}[0], qr/^halt\b/, 'page runtime stop helper captures stop message and halts further blocks' );
my $error_result = $runtime->run_code_blocks(
page => Developer::Dashboard::PageDocument->new(
meta => { codes => [ { id => 'CODE1', body => 'die "boom";' } ] },
),
source => 'saved',
);
like( $error_result->{errors}[0], qr/boom/, 'page runtime captures generic code errors' );
my $transient_code_page = Developer::Dashboard::PageDocument->new(
meta => { codes => [ { id => 'CODE1', body => 'print "transient-code";' } ] },
);
my $transient_code_result = $runtime->run_code_blocks( page => $transient_code_page, source => 'transient' );
like( join( '', @{ $transient_code_result->{outputs} } ), qr/transient-code/, 'page runtime allows transient code through the legacy runtime' );
dies_like(
sub {
$runtime->_run_single_block( code => 'not perl !!!', state => {} );
},
qr/syntax error|Bareword|Compilation failed/s,
'page runtime surfaces code compilation failures',
);
my $docker = Developer::Dashboard::DockerCompose->new(
config => $config,
paths => $paths,
);
my $resolved = $docker->resolve(
project_root => $repo,
addons => ['debug'],
modes => [],
services => ['worker'],
args => ['config'],
);
ok( scalar( grep { $_ =~ /compose\.debug\.yaml$/ } @{ $resolved->{files} } ), 'docker resolve includes addon overlays' );
ok( scalar( grep { $_ eq 'dev' } @{ $resolved->{modes} } ), 'docker resolve pulls addon-provided modes into the resolution' );
my $docker_run = $docker->run(
project_root => $repo,
addons => ['debug'],
services => ['worker'],
args => ['config'],
);
is( $docker_run->{exit_code}, 0, 'docker compose wrapper executes stub docker successfully' );
like( $docker_run->{stdout}, qr/DEBUG:1/, 'docker compose wrapper injects addon environment into command execution' );
like( $docker_run->{stdout}, qr/MODE:dev/, 'docker compose wrapper injects mode environment into command execution' );
{
package Local::ActionMock;
# new()
# Constructs a mock action runner returning generic hash results.
# Input: none.
# Output: Local::ActionMock object.
sub new { bless {}, shift }
# run_page_action()
# Returns a generic action result without a body field.
# Input: ignored.
# Output: hash reference.
sub run_page_action { return { ok => 1 } }
# run_encoded_action()
# Returns a generic encoded action result without a body field.
# Input: ignored.
# Output: hash reference.
sub run_encoded_action { return { ok => 1 } }
}
{
package Local::ActionDie;
# new()
# Constructs a mock action runner that always throws.
# Input: none.
# Output: Local::ActionDie object.
sub new { bless {}, shift }
# run_page_action()
# Throws a page action denial error for coverage of web error handling.
# Input: ignored.
# Output: never returns.
sub run_page_action { die "denied\n" }
# run_encoded_action()
# Throws an encoded action denial error for coverage of web error handling.
# Input: ignored.
# Output: never returns.
sub run_encoded_action { die "encoded denied\n" }
}
my $auth = Developer::Dashboard::Auth->new( files => $files, paths => $paths );
my $sessions = Developer::Dashboard::SessionStore->new( paths => $paths );
my $web_json = Developer::Dashboard::Web::App->new(
actions => Local::ActionMock->new,
auth => $auth,
pages => $pages,
resolver => $resolver,
sessions => $sessions,
);
( run in 1.468 second using v1.01-cache-2.11-cpan-df04353d9ac )