BusyBird

 view release on metacpan or  search on metacpan

lib/BusyBird/Test/StatusStorage.pm  view on Meta::CPAN

package BusyBird::Test::StatusStorage;
use v5.8.0;
use strict;
use warnings;
use Exporter 5.57 qw(import);
use DateTime;
use DateTime::Duration;
use Test::More;
use Test::Builder;
use Test::Fatal 0.006 qw(dies_ok);
use BusyBird::DateTime::Format;
use BusyBird::StatusStorage;
use BusyBird::Util ();
use Carp;
use utf8;
use Encode qw(encode_utf8);

our %EXPORT_TAGS = (
    storage => [
        qw(test_storage_common test_storage_ordered test_storage_truncation test_storage_missing_arguments),
        qw(test_storage_requires_status_ids test_storage_undef_in_array),
    ],
    status => [qw(test_status_id_set test_status_id_list)],
);
our @EXPORT_OK = ();

BusyBird::Util::export_ok_all_tags();
push @EXPORT_OK, qw(test_cases_for_ack);

my $datetime_formatter = 'BusyBird::DateTime::Format';

sub status {
    my ($id, $level, $acked_at) = @_;
    croak "you must specify id" if not defined $id;
    my $status = {
        id => $id,
        created_at => $datetime_formatter->format_datetime(
            DateTime->from_epoch(epoch => $id)
        ),
    };
    $status->{busybird}{level} = $level if defined $level;
    $status->{busybird}{acked_at} = $acked_at if defined $acked_at;
    return $status;
}

sub nowstring {
    return $datetime_formatter->format_datetime(
        DateTime->now(time_zone => 'UTC')
    );
}

sub add_datetime_days {
    my ($datetime_str, $days) = @_;
    my $dtd = DateTime::Duration->new(days => ($days > 0 ? $days : -$days));
    my $orig_dt = $datetime_formatter->parse_datetime($datetime_str);
    return $datetime_formatter->format_datetime(
        ($days > 0) ? ($orig_dt + $dtd) : ($orig_dt - $dtd)
    );
}

sub id_counts {
    my @statuses_or_ids = @_;
    my %id_counts = ();
    foreach my $s_id (@statuses_or_ids) {
        my $id = ref($s_id) ? $s_id->{id} : $s_id;
        $id_counts{$id} += 1;
    }
    return %id_counts;
}

sub id_list {
    my @statuses_or_ids = @_;
    return map { ref($_) ? $_->{id} : $_ } @statuses_or_ids;
}

sub acked {
    my ($s) = @_;
    no autovivification;
    return $s->{busybird}{acked_at};
}

sub test_status_id_set {
    ## unordered status ID set test
    my ($got_statuses, $exp_statuses_or_ids, $msg) = @_;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    return is_deeply(
        { id_counts @$got_statuses },
        { id_counts @$exp_statuses_or_ids },
        $msg
    );
}

sub test_status_id_list {
    ## ordered status ID list test
    my ($got_statuses, $exp_statuses_or_ids, $msg) = @_;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    return is_deeply(
        [id_list @$got_statuses],
        [id_list @$exp_statuses_or_ids],
        $msg
    );
}

sub sync_get {
    my ($storage, $loop, $unloop, %query) = @_;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    my $callbacked = 0;
    my $statuses;
    $storage->get_statuses(%query, callback => sub {
        my $error = shift;
        $statuses = shift;
        is($error, undef, 'operation succeed');
        $callbacked = 1;
        $unloop->();
    });
    $loop->();
    ok($callbacked, 'callbacked');
    return $statuses;
}

sub sync_get_unacked_counts {
    my ($storage, $loop, $unloop, $timeline) = @_;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    my $callbacked = 0;
    my $result;
    $storage->get_unacked_counts(
        timeline => $timeline, callback => sub {
            my ($error, $unacked_counts) = @_;
            is($error, undef, 'operation succeed');
            $result = $unacked_counts;
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, 'callbacked');
    return %$result;
}

lib/BusyBird/Test/StatusStorage.pm  view on Meta::CPAN

            { sync_get_unacked_counts($storage, $loop, $unloop, $tl) },
            { total => 0 },
            "$tl is empty"
        );
    }
    
    note("--- put_statuses (insert), single");
    $callbacked = 0;
    $storage->put_statuses(
        timeline => '_test_tl1',
        mode => 'insert',
        statuses => status(1),
        callback => sub {
            my ($error, $num) = @_;
            is($error, undef, 'put_statuses succeed.');
            is($num, 1, 'put 1 status');
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");
    is_deeply(
        { sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1') },
        { total => 1, 0 => 1 },
        '1 unacked status'
    );
    note('--- put_statuses (insert), multiple');
    $callbacked = 0;
    $storage->put_statuses(
        timeline => '_test_tl1',
        mode => 'insert',
        statuses => [map { status($_) } 2..5],
        callback => sub {
            my ($error, $num) = @_;
            is($error, undef, 'put_statuses succeed');
            is($num, 4, 'put 4 statuses');
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");
    is_deeply(
        { sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1') },
        { total => 5, 0 => 5 },
        '5 unacked status'
    );

    note('--- get_statuses: any, all');
    $callbacked = 0;
    $storage->get_statuses(
        timeline => '_test_tl1',
        count => 'all',
        callback => sub {
            my ($error, $statuses) = @_;
            is($error, undef, "get_statuses succeed");
            test_status_id_set($statuses, [1..5], "1..5 statuses");
            foreach my $s (@$statuses) {
                no autovivification;
                ok(!$s->{busybird}{acked_at}, "status is not acked");
            }
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");

    note('--- ack_statuses: all');
    $callbacked = 0;
    $storage->ack_statuses(
        timeline => '_test_tl1',
        callback => sub {
            my ($error, $num) = @_;
            is($error, undef, "ack_statuses succeed");
            is($num, 5, "5 statuses acked.");
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");
    is_deeply(
        { sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1') },
        { total => 0 },
        "all acked"
    );
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl1', count => 'all'
    }, sub {
        my $statuses = shift;
        is(int(@$statuses), 5, "5 statueses");
        foreach my $s (@$statuses) {
            no autovivification;
            ok($s->{busybird}{acked_at}, 'acked');
        }
    };

    note('--- delete_statuses (single deletion)');
    $callbacked = 0;
    $storage->delete_statuses(
        timeline => '_test_tl1',
        ids => 3,
        callback => sub {
            my ($error, $num) = @_;
            is($error, undef, "operation succeed.");
            is($num, 1, "1 deletion");
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl1', count => 'all'
    }, sub {
        my $statuses = shift;
        test_status_id_set($statuses, [1,2,4,5], "ID=3 is deleted");
    };

    note('--- delete_statuses (multiple deletion)');
    $callbacked = 0;
    $storage->delete_statuses(
        timeline => '_test_tl1',
        ids => [1, 4],
        callback => sub {
            my ($error, $num) = @_;
            is($error, undef, 'operation succeed');
            is($num, 2, "2 statuses deleted");
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl1', count => 'all'
    }, sub {
        my $statuses = shift;
        test_status_id_set($statuses, [2,5], "ID=1,4 are deleted");
    };

    note('--- delete_statuses (all deletion)');
    $callbacked = 0;
    $storage->delete_statuses(
        timeline => '_test_tl1',
        ids => undef,
        callback => sub {
            my ($error, $num) = @_;
            is($error, undef, 'operation succeed');
            is($num, 2, "2 statuses deleted");
            $callbacked = 1;
            $unloop->();
        }
    );

lib/BusyBird/Test/StatusStorage.pm  view on Meta::CPAN

            is($error, undef, 'ack_statuses succeed');
            $callbacked = 1;
            $unloop->();
        }
    );
    $loop->();
    ok($callbacked, "callbacked");
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl1', count => 'all',
    }, sub {
        my $statuses = shift;
        test_status_id_set($statuses, [1..5], "5 statuses");
        foreach my $s (@$statuses) {
            ok(acked($s), "Status ID = $s->{id} is acked");
        }
    };
    note('--- put (insert): try to insert existent status');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl1',
        mode => 'insert', target => status(3), exp_change => 0,
        exp_unacked => [], exp_acked => [1..5]
    );
    note('--- put (update): change to unacked');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl1',
        mode => 'update', target => [map {status($_)} (2,4)], exp_change => 2,
        exp_unacked => [2,4], exp_acked => [1,3,5]
    );
    note('--- put (update): change to unacked');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl1',
        mode => 'update', target => [map { status($_) } (3,5)],
        exp_change => 2, exp_unacked => [2,3,4,5], exp_acked => [1]
    );
    is_deeply(
        {sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1')},
        {total => 4, 0 => 4}, '4 unacked statuses'
    );
    note('--- put (update): change level');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl1',
        mode => 'update',
        target => [map { status($_, ($_ % 2 + 1), $_ == 1 ? nowstring() : undef) } (1..5)],
        exp_change => 5, exp_unacked => [2,3,4,5], exp_acked => [1]
    );
    is_deeply(
        {sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1')},
        {total => 4, 1 => 2, 2 => 2}, "4 unacked statuses in 2 levels"
    );
    note('--- put (upsert): acked statuses');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl1',
        mode => 'upsert', target => [map { status($_, 7, nowstring()) } (4..7)],
        exp_change => 4, exp_unacked => [2,3], exp_acked => [1,4..7]
    );
    note('--- get and put(update): back to unacked');
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl1', count => 'all', ack_state => 'acked'
    }, sub {
        my $statuses = shift;
        delete $_->{busybird}{acked_at} foreach @$statuses;
        change_and_check(
            $storage, $loop, $unloop, timeline => '_test_tl1',
            mode => 'update', target => $statuses,
            exp_change => 5, exp_unacked => [1..7], exp_acked => []
        );
    };
    is_deeply(
        {sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl1')},
        {total => 7, 1 => 1, 2 => 2, 7 => 4}, "3 levels"
    );

    note('--- put(insert): to another timeline');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl2',
        mode => 'insert', target => [map { status($_) } (1..10)],
        exp_change => 10, exp_unacked => [1..10], exp_acked => []
    );
    is_deeply(
        {sync_get_unacked_counts($storage, $loop, $unloop, '_test_tl2')},
        {total => 10, 0 => 10}, '10 unacked statuses'
    );
    ## change_and_check(
    ##     $storage, $loop, $unloop, timeline => '_test_tl2',
    ##     mode => 'ack', target => [1..5],
    ##     exp_change => 5, exp_unacked => [6..10], exp_acked => [1..5]
    ## );
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl2',
        mode => 'update', target => [map {status($_, undef, nowstring())} (1..5)],
        exp_change => 5, exp_unacked => [6..10], exp_acked => [1..5]
    );
    note('--- get: single, any state');
    foreach my $id (1..10) {
        on_statuses $storage, $loop, $unloop, {
            timeline => '_test_tl2', count => 1, max_id => $id
        }, sub {
            my $statuses = shift;
            is(int(@$statuses), 1, "get 1 status");
            is($statuses->[0]{id}, $id, "... and its ID is $id");
        };
    }
    note('--- get: single, specific state');
    foreach my $id (1..10) {
        my $correct_state = ($id <= 5) ? 'acked' : 'unacked';
        my $wrong_state = $correct_state eq 'acked' ? 'unacked' : 'acked';
        on_statuses $storage, $loop, $unloop, {
            timeline => '_test_tl2', count => 1, max_id => $id,
            ack_state => $correct_state,
        }, sub {
            my $statuses = shift;
            is(int(@$statuses), 1, "get 1 status");
            is($statuses->[0]{id}, $id, "... and its ID is $id");
        };
        foreach my $count ('all', 1, 10) {
            on_statuses $storage, $loop, $unloop, {
                timeline => '_test_tl2', count => $count, max_id => $id,
                ack_state => $wrong_state
            }, sub {
                my $statuses = shift;
                is(int(@$statuses), 0,

lib/BusyBird/Test/StatusStorage.pm  view on Meta::CPAN

            });
            $loop->();
            ok($callbacked, 'callbacked');
            my $already_acked_at = nowstring();
            change_and_check(
                $storage, $loop, $unloop, timeline => '_test_acks', mode => 'insert',
                target => [(map {status($_, 0, $already_acked_at)} 1..10), (map {status($_)} 11..20)],
                exp_change => 20, exp_acked => [1..10], exp_unacked => [11..20]
            );
            my %target_args = ();
            $target_args{target_ids} = $case->{req}{ids} if exists $case->{req}{ids};
            $target_args{target_max_id} = $case->{req}{max_id} if exists $case->{req}{max_id};
            change_and_check(
                $storage, $loop, $unloop, timeline => '_test_acks', mode => 'ack',
                %target_args, exp_change => $case->{exp_count}, exp_acked => $case->{exp_acked}, exp_unacked => $case->{exp_unacked}
            );
        }
    }

    {
        note('--- -- -- Unicode timeline name and Unicode status ID');
        foreach my $timeline_name ("_test_ascii", '_test_ゆにこーど') {
            note(encode_utf8("--- -- timeline: $timeline_name"));
            $storage->delete_statuses(timeline => $timeline_name, ids => undef, callback => sub {
                my $e = shift;
                is($e, undef, "initial delete");
                $unloop->();
            });
            $loop->();
            my @statuses = map { status($_) } 0..1;
            $statuses[1]{id} = 'いち';
            $statuses[0]{text} = 'テキスト ゼロ';
            $statuses[1]{text} = 'テキスト いち';
            change_and_check(
                $storage, $loop, $unloop, timeline => $timeline_name, mode => 'insert', target => \@statuses,
                exp_change => 2, exp_acked => [], exp_unacked => [qw(0 いち)]
            );
            check_contains($storage, $loop, $unloop,
                           {timeline => $timeline_name, query => [$statuses[1], 'に', 'いち', 0]},
                           [undef, [$statuses[1], 'いち', 0], ['に']],
                           encode_utf8("contains() works fine with Unicode timeline $timeline_name and Unicode status IDs"));
            $storage->get_unacked_counts(timeline => $timeline_name, callback => sub {
                my ($e, $unacked_counts) = @_;
                is($e, undef, "get unacked counts succeed");
                is_deeply($unacked_counts, {total => 2, 0 => 2}, "unacked counts OK");
                $unloop->();
            });
            $loop->();
            change_and_check(
                $storage, $loop, $unloop, timeline => $timeline_name, mode => 'ack', target_ids => $statuses[1]{id},
                exp_change => 1, exp_acked => ["いち"], exp_unacked => [0]
            );
            foreach my $status (@statuses) {
                my $got_statuses = sync_get(
                    $storage, $loop, $unloop,
                    timeline => $timeline_name, count => 1, max_id => $status->{id}
                );
                test_status_id_list($got_statuses, [$status->{id}], encode_utf8("status ID $status->{id} OK"));
                is($got_statuses->[0]{text}, $status->{text}, encode_utf8("status text '$status->{text}' OK"));
                if($got_statuses->[0]{id} eq "0") {
                    ok(!$got_statuses->[0]{busybird}{acked_at}, "status 0 is not acked");
                }else {
                    ok($got_statuses->[0]{busybird}{acked_at}, "status 1 is acked");
                }
            }
            $statuses[1]{busybird}{level} = 5;
            change_and_check(
                $storage, $loop, $unloop, timeline => $timeline_name, mode => "update", target => $statuses[1],
                exp_change => 1, exp_acked => [], exp_unacked => [0, "いち"]
            );
            $storage->get_unacked_counts(timeline => $timeline_name, callback => sub {
                my ($e, $unacked_counts) = @_;
                is($e, undef, "get unacked counts succeed");
                is_deeply($unacked_counts, {total => 2, 0 => 1, 5 => 1}, "unacked counts OK");
                $unloop->();
            });
            $loop->();
            change_and_check(
                $storage, $loop, $unloop, timeline => $timeline_name, mode => "delete", target => [map {$_->{id}} @statuses],
                exp_change => 2, exp_acked => [], exp_unacked => []
            );
        }
    }

    note('--- clean up');
    foreach my $tl ('_test_tl1', '_test_tl2') {
        $callbacked = 0;
        $storage->delete_statuses(timeline => $tl, ids => undef, callback => sub {
            my $error= shift;
            is($error, undef, "operation succeed");
            $callbacked = 1;
            $unloop->();
        });
        $loop->();
        ok($callbacked, "callbacked");
    }
}

sub test_storage_ordered {
    my ($storage, $loop, $unloop) = @_;
    $loop ||= sub {};
    $unloop ||= sub {};
    note('-------- test_storage_ordered');
    note('--- clear timeline');
    my $callbacked = 0;
    foreach my $tl (qw(_test_tl3 _test_tl4 _test_tl5)) {
        $callbacked = 0;
        $storage->delete_statuses(timeline => $tl, ids => undef, callback => sub {
            my $error = shift;
            is($error, undef, "operation succeed");
            $callbacked = 1;
            $unloop->();
        });
        $loop->();
        ok($callbacked, "callbacked");    
    }
    note('--- acked_at and created_at are preserved');
    foreach my $case (
        {label => "both unset", created_at => undef, acked_at => undef},
        {label => "only created_at set", created_at => 'Mon Jul 01 22:11:41 +0900 2013',
         acked_at => undef},
        {label => "only acked_at set", created_at => undef,
         acked_at => "Wed Apr 17 04:23:29 -0500 2013"},
        {label => 'both set', created_at => 'Fri Oct 12 00:36:44 +0000 2012',
         acked_at => 'Thu Oct 25 13:10:00 +0200 2012'},
    ) {
        note("--- -- case: $case->{label}");
        $callbacked = 0;
        my $status = status(1);
        $status->{created_at} = $case->{created_at};
        $status->{busybird}{acked_at} = $case->{acked_at};
        $storage->put_statuses(
            timeline => "_test_tl3", mode => 'insert', statuses => $status,
            callback => sub {
                my ($error, $count) = @_;
                is($error, undef, "put succeed");
                is($count, 1, "1 inserted");
                $callbacked = 1;
                $unloop->();
            }
        );
        $loop->();
        ok($callbacked, "callbacked");
        on_statuses $storage, $loop, $unloop, {timeline => '_test_tl3', count => 'all'}, sub {
            my $statuses = shift;
            is(scalar(@$statuses), 1, "1 status obtained");
            is($statuses->[0]{created_at}, $case->{created_at}, "created_at is preserved");
            is($statuses->[0]{busybird}{acked_at}, $case->{acked_at}, "acked_at is preserved");
        };
        change_and_check(
            $storage, $loop, $unloop, timeline => '_test_tl3',
            mode => 'delete', target => undef, exp_change => 1,
            exp_unacked => [], exp_acked => []
        );
    }
    note('--- populate timeline');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl3',
        mode => 'insert', target => [map {status $_} (1..30)],
        label => 'first insert',
        exp_change => 30, exp_unacked => [1..30], exp_acked => []
    );
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl3',
        mode => 'ack', target => undef, label => 'ack all',
        exp_change => 30, exp_unacked => [], exp_acked => [1..30]
    );
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl3',
        mode => 'insert', target => [map {status $_} (31..60)],
        label => "another insert", exp_change => 30,
        exp_unacked => [31..60], exp_acked => [1..30]
    );
    my %base = (timeline => '_test_tl3');

    get_and_check_list(
        $storage, $loop, $unloop, {%base, count => 'all'}, [reverse 1..60],
        'get: no max_id, any state, all'
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base, count => 20}, [reverse 41..60],
        'get: no max_id, any state, partial'
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base, count => 40}, [reverse 21..60],
        'get: no max_id, any state, both states'
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base, count => 120}, [reverse 1..60],
        'get: no max_id, any state, count larger than the size'
    );

    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'unacked', count => 'all'},
        [reverse 31..60],
        'get: no max_id unacked, all'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'unacked', count => 15},
        [reverse 46..60 ],
        'get: no max_id, unacked, partial'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'unacked', count => 50},
        [reverse 31..60],

lib/BusyBird/Test/StatusStorage.pm  view on Meta::CPAN

    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'unacked', max_id => 20, count => 5},
        [],
        'get: max_id in acked, unacked state'
    );

    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'acked', max_id => 50, count => 10},
        [],
        'get: max_id in unacked, acked state, count in unacked'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'acked', max_id => 45, count => 30},
        [],
        'get: max_id in unacked, acked state, count larger than the unacked size'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'acked', max_id => 20, count => 10},
        [reverse 11..20],
        'get: max_id in acked, acked state, count in acked'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'acked', max_id => 10, count => 30},
        [reverse 1..10],
        'get: max_id in acked, acked state, count larger than acked size'
    );

    {
        note('--- more acked statuses');
        my $now = DateTime->now(time_zone => 'UTC');
        my $yesterday = $now - DateTime::Duration->new(days => 1);
        my $tomorrow = $now + DateTime::Duration->new(days => 1);
        my @more_statuses = (
            (map { status $_, 0, $datetime_formatter->format_datetime($tomorrow)  } 61..70),
            (map { status $_, 0, $datetime_formatter->format_datetime($yesterday) }  71..80)
        );
        change_and_check(
            $storage, $loop, $unloop, timeline => '_test_tl3',
            mode => 'insert', target => \@more_statuses,
            exp_change => 20, exp_unacked => [31..60], exp_acked => [1..30, 61..80]
        );
    }
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'any', count => 'all'},
        [reverse(71..80, 1..30, 61..70, 31..60)],
        'get: mixed acked_at, no max_id, any state, all'
    );
    note('--- move from acked to unacked');
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl3', acked_state => 'acked',
        max_id => 30, count => 10
    }, sub {
        my $statuses = shift;
        delete $_->{busybird}{acked_at} foreach @$statuses;
        change_and_check(
            $storage, $loop, $unloop, timeline => '_test_tl3',
            mode => 'update', target => $statuses,
            exp_change => 10,
            exp_unacked => [21..60], exp_acked => [1..20, 61..80]
        );
    };
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'any', count => 'all'},
        [reverse(71..80, 1..20, 61..70, 21..60)],
        'get:mixed acked_at, no max_id, any state, all'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'any', max_id => 30, count => 30},
        [reverse(11..20, 61..70, 21..30)],
        'get:mixed acked_at, max_id in unacked, any state, count larger than unacked size'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'any', max_id => 15, count => 20},
        [reverse(76..80, 1..15)],
        'get:mixed acked_at, max_id in acked, any state, count in acked'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'unacked', max_id => 50, count => 50},
        [reverse(21..50)],
        'get:mixed acked_at, max_id in unacked, unacked state, count larger than unacked size'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'acked', max_id => 65, count => 30},
        [reverse(76..80, 1..20, 61..65)],
        'get:mixed acked_at, max_id in acked, acked state, count in acked area'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'unacked', max_id => 20, count => 30},
        [],
        'get:mixed acked_at, max_id in acked, unacked state'
    );
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'acked', max_id => 40, count => 30},
        [],
        'get:mixed acked_at, max_id in unacked, acked state'
    );

    note('--- messing with created_at');
    on_statuses $storage, $loop, $unloop, {
        timeline => '_test_tl3', count => 'all'
    }, sub {
        my $statuses = shift;
        is(int(@$statuses), 80, "80 statuses");
        foreach my $s (@$statuses) {
            $s->{created_at} = $datetime_formatter->format_datetime(
                $datetime_formatter->parse_datetime($s->{created_at})
                    + DateTime::Duration->new(days => 100 - $s->{id})
            );
        }
        change_and_check(
            $storage, $loop, $unloop, timeline => '_test_tl3',
            mode => 'update', target => $statuses, exp_change => 80,
            exp_unacked => [21..60], exp_acked => [1..20, 61..80]
        );
    };
    get_and_check_list(
        $storage, $loop, $unloop,
        {%base, ack_state => 'any', count => 'all'},
        [21..60, 61..70, 1..20, 71..80],
        'sorted by descending order of created_at within acked_at group'
    );

    note('--- -- ack test');
    note('--- change acked_at for testing');
    on_statuses $storage, $loop, $unloop, {
        %base, count => 'all', ack_state => 'acked'
    }, sub {
        my $statuses = shift;
        foreach my $s (@$statuses) {
            $s->{busybird}{acked_at} =
                add_datetime_days($s->{busybird}{acked_at}, +2);
        }
        change_and_check(
            $storage, $loop, $unloop, %base, mode => 'update',
            target => $statuses, exp_change => 40,
            exp_unacked => [21..60], exp_acked => [61..70, 1..20, 71..80]
        );
    };
    change_and_check(
        $storage, $loop, $unloop, %base, mode => 'ack', target => 51,
        exp_change => 10, exp_unacked => [21..50], exp_acked => [61..70, 1..20, 71..80, 51..60]
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base, ack_state => 'any', count => 'all'},
        [21..50, 61..70, 1..20, 71..80, 51..60],
        '10 acked statuses are at the bottom, because other acked statuses have acked_at of future.'
    );
    
    note('--- populate another timeline');
    my %base4 = (timeline => '_test_tl4');
    $callbacked = 0;
    $storage->delete_statuses(%base4, ids => undef, callback => sub {
        my $error = shift;
        is($error, undef, "delete succeed");
        $callbacked = 1;
        $unloop->();
    });
    $loop->();
    ok($callbacked, "callbacked");
    change_and_check(
        $storage, $loop, $unloop, %base4,
        mode => 'insert', target => [map {status($_)} (31..40)],
        exp_change => 10, exp_unacked => [31..40], exp_acked => []
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base4, count => 'all'}, [reverse 31..40],
        '10 unacked'
    );
    change_and_check(
        $storage, $loop, $unloop, %base4,
        mode => 'ack', target => 35, exp_change => 5,
        exp_unacked => [36..40], exp_acked => [31..35]
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base4, count => 'all', ack_state => 'acked'},
        [reverse 31..35], '5 acked'
    );
    change_and_check(
        $storage, $loop, $unloop, %base4,
        mode => 'insert', target => [map {status($_)} (26..30, 41..45)],
        exp_change => 10, exp_unacked => [26..30, 36..45], exp_acked => [31..35]
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base4, count => 'all', ack_state => 'unacked'},
        [reverse 26..30, 36..45], '15 unacked statuses'
    );
    note('--- For testing, set acked_at sufficiently old.');
    on_statuses $storage, $loop, $unloop, {
        %base4, count => 'all', ack_state => 'acked'
    }, sub {
        my $statuses = shift;
        foreach my $s (@$statuses) {
            $s->{busybird}{acked_at} = add_datetime_days($s->{busybird}{acked_at}, -1);
        }
        change_and_check(
            $storage, $loop, $unloop, %base4, mode => 'update', target => $statuses,
            exp_change => 5, exp_unacked => [26..30, 36..45], exp_acked => [31..35]
        );
    };
    change_and_check(
        $storage, $loop, $unloop, %base4, mode => 'ack', target => 40, exp_change => 10,
        exp_unacked => [41..45], exp_acked => [36..40, 26..30, 31..35]
    );
    get_and_check_list(
        $storage, $loop, $unloop, {%base4, count => 'all', ack_state => 'acked'},
        [reverse(36..40), reverse(26..30), reverse(31..35)]
    );
    change_and_check(
        $storage, $loop, $unloop, %base4, mode => 'ack', exp_change => 5,
        exp_unacked => [], exp_acked => [26..45]
    );
    {
        note('--- same timestamp: order is free, but must be consistent.');
        my %base5 = (timeline => '_test_tl5');
        my @in_statuses = map {status($_)} (1..10);
        my $created_at = nowstring;
        $_->{created_at} = $created_at foreach @in_statuses;
        change_and_check(
            $storage, $loop, $unloop, %base5, mode => 'insert', target => [@in_statuses[0..4]],
            label => 'insert first five', exp_change => 5, exp_unacked => [1..5], exp_acked => []
        );
        change_and_check(
            $storage, $loop, $unloop, %base5, mode => 'ack', target => undef,
            label => 'ack first five', exp_change => 5, exp_unacked => [], exp_acked => [1..5]
        );
        change_and_check(
            $storage, $loop, $unloop, %base5, mode => 'insert', target => [@in_statuses[5..9]],
            label => 'insert next five', exp_change => 5, exp_unacked => [6..10], exp_acked => [1..5]
        );
        my $whole_timeline = sync_get($storage, $loop, $unloop, %base5, count => 'all');
        foreach my $start_index (0..9) {
            my $max_id = $whole_timeline->[$start_index]{id};
            get_and_check_list(
                $storage, $loop, $unloop, {%base5, count => 'all', max_id => $max_id},
                [ map {$_->{id}} @{$whole_timeline}[$start_index .. 9] ],
                "start_index = $start_index, max_id = $max_id: order is the same as the whole_timeline"
            );
        }
    }

    {
        note('--- -- acks with various argument cases (ordered)');
        foreach my $case (test_cases_for_ack(is_ordered => 1)) {
            my $callbacked = 0;
            next if not defined $case->{req};
            note("--- case: $case->{label}");
            $storage->delete_statuses(timeline => '_test_acks', ids => undef, callback => sub {
                my ($error, $count) = @_;
                is($error, undef, "delete succeed");
                $callbacked = 1;
                $unloop->();
            });
            $loop->();

lib/BusyBird/Test/StatusStorage.pm  view on Meta::CPAN

    note("--- soft_max = $soft_max, hard_max = $hard_max");
    $loop ||= sub {};
    $unloop ||= sub {};
    
    note('--- clear the timeline');
    my $callbacked = 0;
    my %base = (timeline => '_test_tl4');
    $storage->delete_statuses(%base, ids => undef, callback => sub {
        my $error = shift;
        is($error, undef, "delete succeed");
        $callbacked = 1;
        $unloop->();
    });
    $loop->();
    ok($callbacked, 'callbacked');
    on_statuses $storage, $loop, $unloop, {
        %base, count => 'all'
    }, sub {
        my ($statuses) = @_;
        is(int(@$statuses), 0, 'no statuses');
    };
    note('--- populate to the max');
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'insert', target => [map {status($_)} (1..$hard_max)],
        exp_change => $hard_max, exp_unacked => [1..$hard_max],
        exp_acked => []
    );
    note('--- insert another one: truncation occurs');
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'insert', target => status($hard_max+1),
        exp_change => 1, exp_unacked => [($hard_max+1 - ($soft_max-1))..($hard_max+1)],
        exp_acked => []
    );
    note('--- insert multiple statuses: truncation occurs');
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'insert', target => [map { status($_) } ($hard_max+2) .. ($hard_max*2 - $soft_max + 11)],
        exp_change => ($hard_max - $soft_max + 10),
        exp_unacked => [($hard_max*2 - $soft_max*2 + 12) .. ($hard_max*2 - $soft_max + 11)],
        exp_acked => []
    );

    note('--- clear and populate to the max');
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'delete', target => undef,
        exp_change => $soft_max, exp_unacked => [], exp_acked => []
    );
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'insert', target => [map {status($_)} 1..$hard_max],
        exp_change => $hard_max, exp_unacked => [1..$hard_max], exp_acked => []
    );
    note('--- ack the top status');
    on_statuses $storage, $loop, $unloop, {
        %base, count => 1, max_id => $hard_max
    }, sub {
        my ($statuses) = @_;
        $statuses->[0]{busybird}{acked_at} = nowstring();
        change_and_check(
            $storage, $loop, $unloop, %base,
            mode => 'update', target => $statuses,
            exp_change => 1, exp_unacked => [1..($hard_max-1)],
            exp_acked => [$hard_max]
        );
    };
    note('--- inserting another one removes the acked status');
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'insert', target => status($hard_max+1),
        exp_change => 1, exp_unacked => [($hard_max - $soft_max + 1)..($hard_max - 1), ($hard_max + 1)],
        exp_acked => []
    );
    note('--- populate timeline to the max');
    change_and_check(
        $storage, $loop, $unloop, %base,
        mode => 'insert', target => [map {status($_)} ($hard_max+2) .. ($hard_max+2 + $hard_max - $soft_max - 1)],
        exp_change => $hard_max - $soft_max,
        exp_unacked => [($hard_max - $soft_max + 1)..($hard_max - 1), ($hard_max + 1)..($hard_max*2+2 - $soft_max-1)],
        exp_acked => []
    );
    note('--- clear another timeline');
    $storage->delete_statuses(timeline => '_test_tl4_2', ids => undef, callback => sub {
        my $error = shift;
        is($error, undef, "delete succeed");
        $callbacked = 1;
        $unloop->();
    });
    $loop->();
    ok($callbacked, "callbacked");
    note('--- populate another timeline to the max');
    change_and_check(
        $storage, $loop, $unloop, timeline => '_test_tl4_2',
        mode => 'insert', target => [map {status($_)} 1..$hard_max],
        exp_change => $hard_max, exp_unacked => [1..$hard_max], exp_acked => []
    );
    note('--- statuses in the first timeline is maintained.');
    on_statuses $storage, $loop, $unloop, {
        %base, count => 'all'
    }, sub {
        my $statuses = shift;
        test_status_id_list(
            $statuses, [reverse( ($hard_max - $soft_max + 1)..($hard_max - 1), ($hard_max + 1)..($hard_max*2+2 - $soft_max-1) )],
            "statuses in the first timeline are intact"
        );
    };
}

sub test_storage_missing_arguments {
    my ($storage, $loop, $unloop) = @_;
    note("-------- test_storage_missing_arguments");
    dies_ok { $storage->ack_statuses() } 'ack: timeline is missing';
    dies_ok { $storage->get_statuses(callback => sub {}) } 'get: timeline is missing';
    dies_ok { $storage->get_statuses(timeline => 'tl') } 'get: callback is missing';
    dies_ok {
        $storage->put_statuses(mode => 'insert', statuses => []);
    } 'put: timeline is missing';
    dies_ok {
        $storage->put_statuses(timeline => 'tl', mode => 'insert');



( run in 1.573 second using v1.01-cache-2.11-cpan-39bf76dae61 )