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 )