API-Docker

 view release on metacpan or  search on metacpan

t/stream_error.t  view on Meta::CPAN

      register_cleanup(sub {
        my $after = eval { $docker->containers->list(all => 1) } || [];
        eval { $docker->containers->remove($_->id, force => 1) }
          for grep { !$seen_before{ $_->id } } @$after;
      });
    }

    my $dockerfile = "FROM alpine:3\nRUN exit 7\n";
    my $tar = _tar_context($dockerfile);

    my $events = eval {
      $docker->images->build(context => $tar, t => $tag, forcerm => 1)
    };
    my $err = $@;

    ok !defined $events, 'no return value from the failed build';
    isa_ok $err, 'API::Docker::Error::Stream';
    ok scalar @{ $err->events } > 1, 'the build output came with it';

    my ($error_event) = grep { ref $_ eq 'HASH' && $_->{errorDetail} } @{ $err->events };
    ok $error_event, 'one of the carried events is the one that triggered the croak';

    # The claim is that the RUN step's exit code reaches the caller -- not
    # any particular sentence about it, which karr k50 already ruled out as
    # a stable interface. Measured 2026-08-27: Docker 29.7.2/API 1.55 puts it
    # in errorDetail structurally --
    # {"code":7,"message":"...returned a non-zero code: 7"} -- while Podman
    # 5.4.2/API 1.41 sends only {"message":"...exit status 7\n"}, no `code`
    # key at all. So the exit code is genuinely only prose on Podman; assert
    # the strongest form each engine actually offers rather than a wording
    # that happens to appear on both (there isn't one).
    if (live_engine() eq 'docker') {
      is $error_event->{errorDetail}{code}, 7,
        'Docker carries the RUN exit status structurally, in errorDetail.code';
    }
    else {
      like "$err", qr/exit status 7/,
        'Podman carries the RUN exit status only in the message text';
    }

    my $tagged = eval { $docker->images->inspect($tag) };
    ok !$tagged, 'the failed build left no tagged image behind';

    if (live_engine() eq 'docker') {
      my %seen_before = map { $_ => 1 } @before;
      my @leaked = grep { !$seen_before{$_} }
        map { $_->id } @{ $docker->containers->list(all => 1) };
      is_deeply [ sort @leaked ], [],
        'forcerm=1 left no intermediate container behind (karr k63)';
    }
  };
}

# A minimal ustar archive holding one file, so the live build needs no
# Archive::Tar.
sub _tar_context {
  my ($content) = @_;
  my $name = 'Dockerfile';
  my $header = pack 'a100 a8 a8 a8 a12 a12 A8 a1 a100 a255',
    $name, '0000644', '0000000', '0000000',
    sprintf('%011o', length $content), sprintf('%011o', time),
    '', '0', '', '';
  my $checksum = 0;
  $checksum += $_ for unpack 'C*', $header;
  substr($header, 148, 8) = sprintf('%06o', $checksum) . "\0 ";
  my $body = $content . "\0" x ((512 - length($content) % 512) % 512);
  return $header . $body . "\0" x 1024;
}

done_testing;



( run in 1.264 second using v1.01-cache-2.11-cpan-54e63673c56 )