- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI) - Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend - Dockerfile.gui: Astro static build served via nginx - docker-compose.yml: backend (internal) + frontend (port 5353) - nginx.conf: root redirects to /es/, /api/ proxied to backend - zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
700 lines
20 KiB
Perl
700 lines
20 KiB
Perl
package TestUtil::DSL;
|
||
|
||
use warnings;
|
||
use strict;
|
||
use v5.24;
|
||
|
||
use Carp qw(confess croak);
|
||
|
||
use TestUtil::DSL::Compiler;
|
||
|
||
# This is needed in order to squelch “too late to run INIT block” warnings.
|
||
use Zonemaster::Engine ();
|
||
|
||
=encoding utf8
|
||
|
||
=head1 NAME
|
||
|
||
TestUtil::DSL - a domain-specific language for easy testing of test cases
|
||
|
||
=head1 SYNOPSIS
|
||
|
||
Because this package lies in the testing directory C<t/>, which is not listed in C<@INC> during testing,
|
||
it must be loaded using the following code:
|
||
|
||
use File::Basename qw( dirname );
|
||
use File::Spec::Functions qw( rel2abs );
|
||
use lib dirname( rel2abs( $0 ) );
|
||
use TestUtil::DSL;
|
||
|
||
A test case can then be tested with the following minimal skeleton:
|
||
|
||
testing_test_case 'Example', 'example01';
|
||
|
||
all_tags qw(EX01_ALL_GOOD
|
||
EX01_MINOR_ISSUE
|
||
EX01_SOMETHING_WRONG
|
||
EX01_SOME_OTHER_PROBLEM
|
||
EX01_BREAKS_ON_SOME_FEATURE);
|
||
|
||
root_hints 'ns1' => [ qw(127.1.0.1 fda1:b2:c3::127:1:0:1) ],
|
||
'ns2' => [ qw(127.1.0.2 fda1:b2:c3::127:1:0:2) ];
|
||
zone_name_template '{SCENARIO}.{TESTCASE}.xa';
|
||
|
||
scenario 'GOOD-{1..3}' => sub {
|
||
expect EX01_ALL_GOOD;
|
||
};
|
||
|
||
# more scenarios here
|
||
|
||
no_more_scenarios;
|
||
|
||
=cut
|
||
|
||
use Exporter 'import';
|
||
|
||
our @EXPORT = qw(all_tags
|
||
clear_cache
|
||
expect
|
||
expect_others
|
||
fake_ds
|
||
fake_ns
|
||
forbid
|
||
forbid_others
|
||
no_more_scenarios
|
||
not_testable
|
||
root_hints
|
||
scenario
|
||
testing_test_case
|
||
todo
|
||
zone
|
||
zone_name_template);
|
||
|
||
# Define a stack for internal context objects, some functions to manipulate it,
|
||
# and only expose the top of the stack.
|
||
{
|
||
my @STACK = ();
|
||
|
||
sub STATE () {
|
||
$STACK[-1];
|
||
}
|
||
|
||
sub check_state ($) {
|
||
my ($expected_state) = @_;
|
||
|
||
if (defined $expected_state) {
|
||
if (scalar @STACK == 0 or $expected_state ne STATE->{_STATE}) {
|
||
croak "This keyword is not valid in this context."
|
||
}
|
||
}
|
||
elsif (scalar @STACK != 0) {
|
||
croak "This keyword can only be used once";
|
||
}
|
||
}
|
||
|
||
sub pop_state {
|
||
my $result = pop @STACK // croak "Attempted to pop empty stack";
|
||
delete $result->{_STATE};
|
||
return $result;
|
||
}
|
||
|
||
sub push_state ($) {
|
||
push @STACK, { _STATE => $_[0] };
|
||
}
|
||
}
|
||
|
||
# Internal mechanics
|
||
|
||
sub _is_uint_bits {
|
||
my ($value, $bits) = @_;
|
||
return ($value =~ /^\d+$/a and $value >= 0 and $value < (1 << $bits));
|
||
}
|
||
|
||
sub _expand_scenario_names {
|
||
my @scenario_names = @_;
|
||
my @result;
|
||
|
||
for my $name (@scenario_names) {
|
||
if ($name=~ /^ (?<prefix> [^{]*) \{
|
||
(?<start> \d+) \Q..\E (?<end> \d+)
|
||
\} $ /x ) {
|
||
for my $i ($+{start}..$+{end}) {
|
||
push @result, "$+{prefix}$i";
|
||
}
|
||
}
|
||
else {
|
||
push @result, $name;
|
||
}
|
||
}
|
||
|
||
return @result;
|
||
}
|
||
|
||
=head1 TOP-LEVEL KEYWORDS
|
||
|
||
=head2 all_tags
|
||
|
||
all_tags qw(SOME_TAG SOME_OTHER_TAG AND_ANOTHER_ONE);
|
||
|
||
List all the message tags that the test case being tested is expected to emit.
|
||
|
||
=cut
|
||
|
||
sub all_tags (@) {
|
||
my @tags = @_;
|
||
check_state 'testing_test_case';
|
||
|
||
%{STATE->{all_tags}} = map { $_ => 1 } @tags;
|
||
}
|
||
|
||
=head2 no_more_scenarios
|
||
|
||
no_more_scenarios;
|
||
|
||
Ends a declaration of a test. Similar to C<done_testing> in L<Test::More>.
|
||
|
||
=cut
|
||
|
||
sub no_more_scenarios () {
|
||
check_state 'testing_test_case';
|
||
my $spec = pop_state;
|
||
|
||
# Do not clutter AST with data the compiler does not need
|
||
delete $spec->{zone_name_template};
|
||
delete $spec->{all_tags};
|
||
|
||
TestUtil::DSL::Compiler::compile($spec)->();
|
||
}
|
||
|
||
=head2 scenario
|
||
|
||
Declares one or more scenario blocks.
|
||
|
||
A scenario is defined by a name and a set of expectations. The name is used to
|
||
derive the name of a zone on which to run the test case being tested. The
|
||
result of this run is a collection of messages, which are then compared to the
|
||
expected result declared in the scenario block.
|
||
|
||
The scenario block body is a special coderef, evaluated in a special context which
|
||
gives meaning to a set of scenario-specific keywords. See L<SCENARIO-SPECIFIC
|
||
KEYWORDS> for more information on what keywords are valid in a scenario block.
|
||
|
||
The C<scenario> keywords has several legal syntaxes.
|
||
|
||
The first form declares a single scenario:
|
||
|
||
scenario 'SCENARIO-NAME' => sub {
|
||
# scenario declaration here
|
||
};
|
||
|
||
The second form declares multiple scenarios, all sharing the same expectations:
|
||
|
||
scenario qw(SCENARIO-ONE SCENARIO-TWO) => sub {
|
||
# scenario declaration here
|
||
};
|
||
|
||
Scenario names may end with a number range expressed as C<{M..N}> where C<M>
|
||
and C<N> are integers. This is a shorthand for listing scenarios that all
|
||
share the same prefix but differ only by a numbered suffix. This notation
|
||
allows for concise declarations of scenarios that may be configured
|
||
differently but are expected to yield the same set of messages from the test
|
||
case. For example, the following two declarations are equivalent:
|
||
|
||
scenario qw(GOOD-{1..2} EXAMPLE-{1..4}) => sub { ... };
|
||
|
||
scenario qw(GOOD-1 GOOD-2
|
||
EXAMPLE-1 EXAMPLE-2 EXAMPLE-3 EXAMPLE-4) => sub { ... };
|
||
|
||
|
||
=cut
|
||
|
||
sub scenario (@) {
|
||
check_state 'testing_test_case';
|
||
|
||
croak "At least two arguments required" unless scalar @_ >= 2;
|
||
croak "A 'root_hints' must appear before a 'scenario' block"
|
||
unless exists STATE->{root_hints};
|
||
|
||
my $definition = pop @_;
|
||
my @names = _expand_scenario_names(@_);
|
||
|
||
my @context = caller(0);
|
||
|
||
my $scenario = {
|
||
names => \@names,
|
||
caller => [ @context[1..2] ],
|
||
body => do {
|
||
my $PARENT_STATE = STATE;
|
||
push_state 'scenario';
|
||
|
||
STATE->{status} = [ 'testable' ];
|
||
|
||
# Keep track of contents of “all_tags” so that expect and forbid
|
||
# can raise errors if given a tag not in that list.
|
||
STATE->{all_tags} = $PARENT_STATE->{all_tags};
|
||
|
||
# Keep track of keys never mentioned by “expect” or “forbid” respectively
|
||
# so that “expect_others” and “forbid_others” can be defined in terms
|
||
# of “expect” and “forbid” respectively.
|
||
%{STATE->{not_expected}} = map { $_ => 1 } keys %{STATE->{all_tags}};
|
||
%{STATE->{not_forbidden}} = map { $_ => 1 } keys %{STATE->{all_tags}};
|
||
|
||
# Evaluate definition
|
||
my $obj = do { $definition->(); pop_state; };
|
||
|
||
# Clean up
|
||
delete $obj->{all_tags};
|
||
delete $obj->{not_expected};
|
||
delete $obj->{not_forbidden};
|
||
|
||
# Set default zone name (template)
|
||
$obj->{zone} //= STATE->{zone_name_template};
|
||
|
||
# Sanity checks
|
||
if (not defined $obj->{zone}) {
|
||
croak "No 'zone' keyword in scenario block and no previous "
|
||
. "'zone_name_template' seen";
|
||
}
|
||
|
||
# TODO do we need more sanity checks?
|
||
$obj;
|
||
}
|
||
};
|
||
|
||
foreach my $name (@{$scenario->{names}}) {
|
||
STATE->{scenario_status}{$name} = $scenario->{body}{status};
|
||
}
|
||
push @{STATE->{ops}}, [ scenario => $scenario ];
|
||
}
|
||
|
||
|
||
=head2 testing_test_case
|
||
|
||
testing_test_case 'MyModule' 'mymodule01';
|
||
|
||
Declare a unit test for a test case. The arguments for that keyword are the
|
||
name of the test plan, and the name of the test case being tested
|
||
respectively.
|
||
|
||
The DSL expects this keyword to be used first, before any other keyword.
|
||
|
||
=cut
|
||
|
||
sub testing_test_case ($$) {
|
||
check_state undef;
|
||
push_state 'testing_test_case';
|
||
|
||
my ($test_module, $test_case) = @_;
|
||
|
||
STATE->{test_module} = $test_module;
|
||
STATE->{test_case} = $test_case;
|
||
}
|
||
|
||
|
||
=head2 root_hints
|
||
|
||
root_hints 'ns1.example' => [ qw(198.51.113.10 2001:db8:0:8::53) ],
|
||
'ns2.example' => [ qw(198.51.113.20 2001:db8:0:8::1:53) ];
|
||
|
||
Declare the set of name servers which are authoritative for the root zone.
|
||
These root hints apply to all scenarios that follow this keyword.
|
||
|
||
This keyword must be used at least one before defining the first scenario
|
||
block.
|
||
|
||
=cut
|
||
|
||
sub root_hints (%) {
|
||
my (%root_hints) = @_;
|
||
check_state 'testing_test_case';
|
||
STATE->{root_hints} = 1;
|
||
|
||
push @{STATE->{ops}}, [ root_hints => \%root_hints ];
|
||
}
|
||
|
||
=head2 zone_name_template
|
||
|
||
zone_name_template '{SCENARIO}.{TESTCASE}.xa';
|
||
zone_name_template 'child.parent.{SCENARIO}.{TESTCASE}.xa';
|
||
|
||
Declare a default zone name template for the C<scenario> specifications that follow.
|
||
|
||
The string passed as arguments may contain placeholders, such as C<{SCENARIO}>
|
||
or C<{TESTCASE}>, that are substituted accordingly.
|
||
|
||
=cut
|
||
|
||
sub zone_name_template ($) {
|
||
my ($template) = @_;
|
||
check_state 'testing_test_case';
|
||
STATE->{zone_name_template} = $template;
|
||
}
|
||
|
||
=head2 clear_cache
|
||
|
||
clear_cache;
|
||
|
||
Clears the resolver cache for subsequent tests. This might be necessary if
|
||
this caching somehow causes negative side effects.
|
||
|
||
Note that it is unnecessary to use this keyword right after L<root_hints>,
|
||
because L<root_hints> also clears the resolver cache.
|
||
|
||
=cut
|
||
|
||
sub clear_cache () {
|
||
check_state 'testing_test_case';
|
||
push @{STATE->{ops}}, [ "clear_cache" ];
|
||
}
|
||
|
||
|
||
=head1 SCENARIO-SPECIFIC KEYWORDS
|
||
|
||
=head2 zone
|
||
|
||
zone 'myzone.example.xa';
|
||
zone 'myzone.{SCENARIO}.{TESTCASE}.xb';
|
||
|
||
Declare a zone name to be used in the scenario.
|
||
|
||
This declaration is optional, because the default value is inherited from the
|
||
top-level keyword L<zone_name_template>. It can be useful for one or two
|
||
exceptions to a general rule.
|
||
|
||
The same placeholders that are valid for L<zone_name_template> can also be
|
||
used here.
|
||
|
||
=cut
|
||
|
||
sub zone ($) {
|
||
my ($zone) = @_;
|
||
check_state 'scenario';
|
||
|
||
# Template expansion is done at a later stage
|
||
STATE->{zone} = $zone;
|
||
}
|
||
|
||
=head2 fake_ds
|
||
|
||
fake_ds <key tag>, <algo>, <type>, <digest>
|
||
fake_ds 51966, 8, 3, 'ABCDABCDABCDABCD';
|
||
|
||
Provide a DS record as part of the fake delegation to be used in the scenario.
|
||
|
||
=cut
|
||
|
||
sub fake_ds($$$$) {
|
||
my ($tag, $algo, $type, $digest) = @_;
|
||
|
||
unless ( _is_uint_bits($tag, 16) ) {
|
||
croak "$tag: not a valid key tag";
|
||
}
|
||
unless ( _is_uint_bits($algo, 8) ) {
|
||
croak "$algo: not a valid algorithm";
|
||
}
|
||
unless ( _is_uint_bits($type, 8) ) {
|
||
croak "$type: not a valid type";
|
||
}
|
||
unless ( $digest =~ /^ (?: [0-9a-f]{2} )+ $/ix ) {
|
||
croak "$digest: not a valid digest";
|
||
}
|
||
|
||
push @{STATE->{fake_ds}}, {
|
||
keytag => $tag,
|
||
algorithm => $algo,
|
||
type => $type,
|
||
digest => $digest
|
||
};
|
||
}
|
||
|
||
=head2 fake_ns
|
||
|
||
fake_ns <nameserver>[, <IP address…>]
|
||
|
||
Provide an NS record as part of the fake delegation to be used in the scenario.
|
||
The keyword accepts a name server name and optionally a list of IP addresses that
|
||
will be used as glue records if given.
|
||
|
||
Examples:
|
||
|
||
fake_ns 'ns1.example';
|
||
fake_ns 'ns1.example' => '192.0.2.50';
|
||
fake_ns 'ns1.example' => '192.0.2.50', '2001:db8:0:8::53';
|
||
|
||
Passing the same name server name to more than one C<fake_ns> keyword is not
|
||
allowed and is an error.
|
||
|
||
=cut
|
||
|
||
sub fake_ns($@) {
|
||
my ($name, @ips) = @_;
|
||
|
||
if (exists STATE->{fake_ns}{$name}) {
|
||
croak "'fake_ns' cannot be used for '$name' more than once";
|
||
}
|
||
|
||
STATE->{fake_ns}{$name} = \@ips;
|
||
}
|
||
|
||
=head2 expect
|
||
|
||
List one or more tags that the test case is expected to generate when it is run
|
||
on the scenario being defined.
|
||
|
||
This keyword allows for multiple syntaxes.
|
||
|
||
The first form specifies that at least one message of a certain tag is to be
|
||
expected among the test case’s output for the scenario:
|
||
|
||
expect <tag>;
|
||
|
||
The second form is a shorthand that avoids repeating the C<expect> keyword. It
|
||
is equivalent to using the first form as many times as there are tags given as
|
||
arguments.
|
||
|
||
expect <tag1>, <tag2>, ...;
|
||
|
||
The third form specifies a single tag name and a hashref of keys and criteria.
|
||
It specifies that a message whose tag matches the name and whose arguments
|
||
exist and match the supplied criteria must exist among the test case’s output.
|
||
See L<Criteria> below for a more in-depth explanation of this form.
|
||
|
||
expect <tag> => { <argument1> => <criterion1>, ... };
|
||
|
||
The fourth form takes a tag name and a coderef: it searches for all messages
|
||
whose tag matches, evaluates the coderef with C<@_> set to the list of
|
||
messages matching the tag and expects the coderef to return a true value. This
|
||
form is useful for situations where the third form falls short. See L<Criteria>
|
||
below for examples.
|
||
|
||
expect <tag> => sub { my @messages = @_; ... };
|
||
|
||
The C<expect> keyword can be used more than once in a scenario. All of the
|
||
checks specified by C<expect> and C<forbid> keywords must pass in order for
|
||
the scenario to pass.
|
||
|
||
=head3 Criteria
|
||
|
||
For the third form, arguments can be matched against:
|
||
|
||
=over
|
||
|
||
=item strings, which are compared with C<eq>;
|
||
|
||
=item regular expressions;
|
||
|
||
=item and coderefs, where C<$_> is bound to the message parameter’s value.
|
||
|
||
=back
|
||
|
||
The following expects C<SOME_TAG> to be generated with arguments C<ns_list>,
|
||
C<other_argument>, C<third_argument> and C<fourth_argument>. C<ns_list> must
|
||
be equal to C<ns1.example/127.0.60.1>, C<other_argument> must match
|
||
C</^ns2\..*/>, C<third_argument> must be case-insensitively equal to
|
||
C<example> and C<fourth_argument> is only tested for presence.
|
||
|
||
expect SOME_TAG => {
|
||
ns_list => 'ns1.example/127.0.60.1',
|
||
other_argument => qr/^ns2\..*/,
|
||
third_argument => sub { fc $_ eq fc 'EXAMPLE' },
|
||
fourth_argument => sub { 1 },
|
||
};
|
||
|
||
Note that if the test case emits C<SOME_TAG> with an argument that is not
|
||
listed in the C<expect> keyword, it is deemed an error.
|
||
|
||
Checking that a message tag has no parameters at all can also be done as
|
||
follows:
|
||
|
||
expect TAG_WITHOUT_PARAMETERS => {};
|
||
|
||
More free-form criteria can be provided by means of the fourth form. The following
|
||
example checks if the scenario generated exactly three times a given message tag:
|
||
|
||
expect SOME_TAG => sub { scalar @_ == 3 };
|
||
|
||
The following example ensures that all instances of SOME_TAG have a
|
||
C<some_argument> parameter equal to the string C<ns1.example/127.0.60.1>:
|
||
|
||
expect SOME_TAG => sub {
|
||
my @messages = @_;
|
||
for my $m (@messages) {
|
||
return 0 if $m->{some_argument} ne 'ns1.example/127.0.60.1';
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
=cut
|
||
|
||
sub expect (@) {
|
||
if (scalar @_ == 2 and ref $_[1] eq 'HASH') {
|
||
# Third form
|
||
my ($tag, $args) = @_;
|
||
_declare_message_test( expect => $tag, args => $args );
|
||
}
|
||
elsif (scalar @_ == 2 and ref $_[1] eq 'CODE') {
|
||
# Fourth form
|
||
my ($tag, $code) = @_;
|
||
_declare_message_test( expect => $tag, code => $code );
|
||
}
|
||
elsif (scalar @_ >= 1) {
|
||
# First or second form
|
||
foreach my $tag (@_) {
|
||
_declare_message_test( expect => $tag );
|
||
}
|
||
}
|
||
else {
|
||
croak "Need at least one argument";
|
||
}
|
||
}
|
||
|
||
=head2 forbid
|
||
|
||
forbid <tag1>, <tag2>, …
|
||
|
||
Specifies that the listed tags are not to be emitted by the test case in the
|
||
scenario being defined. In other words, in the messages generated by the test
|
||
case, no message should appear whose tags are in the listed tags.
|
||
|
||
The C<forbid> keyword can be used more than once in a scenario. All of the
|
||
checks specified by C<expect> and C<forbid> keywords must pass in order for
|
||
the scenario to pass.
|
||
|
||
=cut
|
||
|
||
sub forbid (@) {
|
||
my @tags = @_;
|
||
|
||
croak "Need at least one argument" unless scalar @tags >= 1;
|
||
|
||
foreach my $tag (@_) {
|
||
_declare_message_test( forbid => $tag );
|
||
}
|
||
}
|
||
|
||
sub _declare_message_test {
|
||
my ( $type, $tag, %args ) = @_;
|
||
my @context = caller(1);
|
||
|
||
exists STATE->{all_tags}{$tag}
|
||
or croak "Tag '$tag' used in '$type' clause but not declared in 'all_tags'";
|
||
|
||
delete STATE->{not_expected}{$tag} if $type eq 'expect';
|
||
delete STATE->{not_forbidden}{$tag} if $type eq 'forbid';
|
||
|
||
push @{STATE->{message_tests}}, [ $type, [ @context[1..2] ], $tag, %args ];
|
||
}
|
||
|
||
=head2 expect_others
|
||
|
||
expect_others;
|
||
|
||
Specifies that all tags not listed by any C<forbid> tag in the scenario
|
||
definition must appear in the test case’s output for the scenario.
|
||
|
||
=cut
|
||
|
||
sub expect_others () {
|
||
croak "Cannot use expect_others if forbid_others is already given" if STATE->{forbid_others};
|
||
croak "Cannot use expect_others more than once" if STATE->{expect_others};
|
||
STATE->{expect_others} = 1;
|
||
|
||
_declare_message_test( expect => $_ ) foreach ( keys %{STATE->{not_forbidden}} );
|
||
}
|
||
|
||
=head2 forbid_others
|
||
|
||
forbid_others;
|
||
|
||
Specifies that all tags not listed by any C<expect> tag in the scenario
|
||
definition must not appear in the test case’s output for the scenario.
|
||
|
||
=cut
|
||
|
||
sub forbid_others () {
|
||
croak "Cannot use forbid_others if expect_others is already given" if STATE->{expect_others};
|
||
croak "Cannot use forbid_others more than once" if STATE->{forbid_others};
|
||
STATE->{forbid_others} = 1;
|
||
|
||
_declare_message_test( forbid => $_ ) foreach ( keys %{STATE->{not_expected}} );
|
||
}
|
||
|
||
=head2 not_testable
|
||
|
||
not_testable 'optional reason';
|
||
|
||
Mark the scenario as not testable.
|
||
|
||
Scenarios marked as not testable are skipped during testing, unless they are
|
||
listed explicitly in the C<ZONEMASTER_SELECTED_SUBTESTS> environment variable.
|
||
|
||
It is semantically equivalent to L<Test::More#todo_skip>.
|
||
|
||
This facility is useful for defining scenarios which cannot be tested because
|
||
the infrastructure (e.g. DNS servers, test zones…) needed to elicit the set of
|
||
messages expected by the scenario is not yet available.
|
||
|
||
No packets related to the scenario are saved to the test’s C<.data> file when
|
||
C<ZONEMASTER_RECORD> is set in the environment. This means that no data is
|
||
recorded for a test zone that does not reflect the scenario accurately.
|
||
Rerecording is only necessary when the C<not_testable> keyword is removed.
|
||
|
||
The C<not_testable> keyword should not be used for other reasons than lack of
|
||
infrastructure, such as if the test fails because of a known bug in the test
|
||
case’s implementation. In those situations, C<todo> is more appropriate.
|
||
|
||
The optional reason, if supplied, is used in the test harness’s diagnostic
|
||
output. It is useful for documenting the exact reason why the test is not
|
||
testable, for example by referring to an item on an issue tracker.
|
||
|
||
=cut
|
||
|
||
sub not_testable (;$) {
|
||
if (exists STATE->{todo}) {
|
||
croak "'not_testable' cannot be combined with 'todo'";
|
||
}
|
||
if (exists STATE->{not_testable}) {
|
||
croak "'not_testable' can only be given once";
|
||
}
|
||
STATE->{status} = [ 'not_testable', $_[0] ];
|
||
}
|
||
|
||
=head2 todo
|
||
|
||
todo 'optional reason';
|
||
|
||
Mark the scenario as “to do”.
|
||
|
||
Scenarios marked as to do are executed normally during testing, but are
|
||
expected to fail. Failing “to do” scenarios do not cause the parent test suite
|
||
to fail.
|
||
|
||
It is semantically equivalent to L<Test::More#TODO:-BLOCK>.
|
||
|
||
This facility is useful for defining scenarios that are expected to fail
|
||
because of a known bug in the test case’s implementation that has not been
|
||
fixed yet.
|
||
|
||
Packets related to the scenario are saved to the test’s C<.data> file when
|
||
C<ZONEMASTER_RECORD> is set in the environment. This means that no rerecord is
|
||
necessary when the C<todo> keyword is removed or when trying to fix the
|
||
implementation.
|
||
|
||
The optional reason, if supplied, is used in the test harness’s diagnostic
|
||
output. It is useful for documenting the exact reason why the test is expected
|
||
to fail, for example by referring to an item on an issue tracker.
|
||
|
||
=cut
|
||
|
||
sub todo (;$) {
|
||
if (exists STATE->{not_testable}) {
|
||
croak "'todo' cannot be combined with 'not_testable'";
|
||
}
|
||
if (exists STATE->{todo}) {
|
||
croak "'todo' can only be given once";
|
||
}
|
||
STATE->{status} = [ 'todo', $_[0] ];
|
||
}
|
||
|
||
1;
|