App-karr
view release on metacpan or search on metacpan
t/198-pick-rules-shared.t view on Meta::CPAN
my $buf = '';
my $err = do {
local $@;
eval {
local *STDOUT;
open STDOUT, '>', \$buf or die $!;
$cmd->execute( [], [] );
};
$@;
};
die "pick died: $err" if $err;
return $buf =~ /^Picked task (\d+):/m ? $1 : undef;
}
# karr-foundation's ticket mode, asked about the same board. Claims nothing.
sub ticket_id {
my ( $repo ) = @_;
return App::karr::Foundation::Picker->new( store => _store($repo) )->next_ticket;
}
# Ask both selectors about the same board, in the same state, and hold both
# against the pinned expectation. Foundation goes first, because pick writes.
sub both_choose {
my ( $repo, $expected, $why ) = @_;
is( ticket_id($repo), $expected, "foundation ticket mode: $why" );
is( pick_id( $repo, claim => "agent-" . ( $expected // 'none' ) ),
$expected, "karr pick: $why" );
return;
}
subtest 'both callers resolve the rules to one definition' => sub {
ok( App::karr::Cmd::Pick->does('App::karr::Role::PickRules'),
'karr pick composes the shared pick rules' );
ok( App::karr::Foundation::Picker->does('App::karr::Role::PickRules'),
'foundation ticket mode composes the shared pick rules' );
# Same name is not enough -- two copies would answer ->can too. The installed
# coderef has to be the role's own, in both packages, or one of them is
# running something else under the shared name.
for my $method ( qw( pickable pick_rank pick_candidates ) ) {
my $rule = App::karr::Role::PickRules->can($method);
ok( $rule, "the role defines $method" ) or next;
is( App::karr::Cmd::Pick->can($method), $rule,
"karr pick's $method is the role's own coderef" );
is( App::karr::Foundation::Picker->can($method), $rule,
"foundation's $method is the role's own coderef" );
}
};
subtest 'neither caller keeps a rule of its own to drift with' => sub {
# The re-fork guard. Composing the role does not stop anyone from adding a
# second sort or a second eligibility test next to it -- that is exactly how
# this ticket happened -- so read both files and insist the rule is not
# spelled out in either. POD and comments are stripped: both files talk about
# the rules at length, and #198's own comments name them.
my %file = (
'karr pick' => 'lib/App/karr/Cmd/Pick.pm',
'foundation ticket' => 'lib/App/karr/Foundation/Picker.pm',
);
# Each fragment is a piece of the rule that used to be written out locally:
# the three-way comparison chain of the ranking, the two config lists it
# ranks from, and the three predicates eligibility is made of.
my @forbidden = (
[ qr/<=>/, 'a comparison of its own' ],
[ qr/\bpriorities\b/, 'the board priorities list' ],
[ qr/\bclasses\b/, 'the board classes list' ],
[ qr/is_terminal_status/, 'the terminal-status test' ],
[ qr/has_blocked/, 'the blocked test' ],
[ qr/has_claimed_by/, 'the claim test' ],
[ qr/_claim_expired/, 'the claim-expiry test' ],
);
for my $who ( sort keys %file ) {
my $src = path( $file{$who} )->slurp_utf8;
$src =~ s/^=\w+.*?^=cut\b.*?$//msg;
$src =~ s/^\s*#.*$//mg;
for my $rule ( @forbidden ) {
my ( $re, $what ) = @$rule;
unlike( $src, $re,
"$file{$who} ($who) does not carry $what -- it asks the role" );
}
}
};
subtest 'the ranking itself, pinned card by card' => sub {
# A board whose priorities and classes are its own, so nothing here can be
# satisfied by a hardcoded table (#149), and every rung of the rule is
# load-bearing: class before priority, priority most-urgent-last in the
# config list, id as the tie-break, an unknown class ranking where
# `standard` would and an unknown priority ranking below every known one.
my $repo = _board(
priorities => [qw( p0 p1 p2 p3 )],
classes => [qw( alpha beta gamma )],
);
mk( $repo, id => 1, class => 'beta', priority => 'p3' );
mk( $repo, id => 2, class => 'alpha', priority => 'p0' );
mk( $repo, id => 3, class => 'beta', priority => 'p3' );
mk( $repo, id => 4, class => 'gamma', priority => 'p3' );
mk( $repo, id => 5, class => 'beta', priority => 'p1' );
mk( $repo, id => 6, class => 'alpha', priority => 'p1' );
mk( $repo, id => 7, class => 'quux', priority => 'p2' );
mk( $repo, id => 8, class => 'beta', priority => 'nonesuch' );
# 7 and 6 and 2 first: class index 0 (`quux` is not in the list, so it ranks
# where `standard` would, and `standard` is not in this list either -- index
# 0), most urgent priority first. Then beta: p3 with the id tie-break, then
# p1, then the priority the board never heard of. gamma last.
#
# What this order does NOT pin is that last step, the fallback for a class
# the board does not list on a board that lists no `standard` either: 7 has
# the most urgent priority of the three class-0 cards, so it comes out first
# whether that fallback is karr's 0 or kanban-md's -1 (ticket #240). The
# subtest below is the one that can tell those apart -- do not read this
# `@expected` as cover for it.
my @expected = ( 7, 6, 2, 1, 3, 5, 8, 4 );
for my $i ( 0 .. $#expected ) {
both_choose( $repo, $expected[$i], "round " . ( $i + 1 ) . " is task $expected[$i]" );
}
( run in 1.564 second using v1.01-cache-2.11-cpan-364913b4093 )