isync/src/run-tests.pl
Oswald Buddenhagen 767a318eea add new sync operation 'Old'
this is essentially the same as 'New', but for previously seen messages,
such as those that would have been instantly expunged (because they were
marked as deleted), those that we failed to store for some reason, and
already expired ones that are now flagged.

REFMAIL: CAOgBZNonT0s0b_yPs2vx81Ru3cQp5M93xpZ3syWBW-2CNoX_ow@mail.gmail.com
2022-06-19 16:10:01 +02:00

1817 lines
40 KiB
Perl
Executable File

#! /usr/bin/perl -w
#
# SPDX-FileCopyrightText: 2006-2022 Oswald Buddenhagen <ossi@users.sf.net>
# SPDX-License-Identifier: GPL-2.0-or-later
#
use warnings;
use strict;
use Carp;
$SIG{__WARN__} = \&Carp::cluck;
$SIG{__DIE__} = \&Carp::confess;
use Cwd;
use Clone 'clone';
use File::Path;
use File::Temp 'tempdir';
my $use_vg = $ENV{USE_VALGRIND};
my $use_st = $ENV{USE_STRACE};
my $mbsync = getcwd()."/mbsync";
my (@match, $start);
for my $arg (@ARGV) {
if ($arg eq "+") {
$start = 1;
} else {
push @match, $arg;
}
}
die("Need exactly one test name when using start syntax.\n")
if ($start && (@match != 1));
if (!-d "tmp") {
unlink "tmp";
my $tdir = tempdir();
symlink $tdir, "tmp" or die "Cannot symlink temp directory: $!\n";
}
chdir "tmp" or die "Cannot enter temp direcory.\n";
use enum qw(:=1 A..Z);
sub mn($) { my ($n) = @_; $n == 0 ? "0" : chr(64 + $n) }
sub mf($) { my ($f) = @_; length($f) ? $f : '-' }
my $sync_flags = "<>^~DFPRST";
my $msg_flags = "DFPRST*?";
sub process_flag_add($$$$$$)
{
my ($flgr, $add, $ok_flags, $num, $e, $what) = @_;
return if ($add eq "");
for my $flg (split('', $add)) {
die("Adding invalid flag '$flg' for $what ".mn($num)." (at $e).\n")
if (index($ok_flags, $flg) < 0);
my $i = index($$flgr, $flg);
die("Adding duplicate flag '$flg' to $what ".mn($num)." (at $e).\n")
if ($i >= 0);
$$flgr .= $flg;
}
$$flgr = $ok_flags =~ s/[^$$flgr]//gr; # sort
}
sub process_flag_del($$$$$)
{
my ($flgr, $del, $num, $e, $what) = @_;
for my $flg (split('', $del)) {
my $i = index($$flgr, $flg);
die("Removing absent flag '$flg' from $what ".mn($num)." (at $e).\n")
if ($i < 0);
substr($$flgr, $i, 1) = '';
}
}
sub process_flag_update($$$$$$$)
{
my ($flgr, $add, $del, $ok_flags, $num, $e, $what) = @_;
process_flag_del($flgr, $del, $num, $e, $what);
process_flag_add($flgr, $add, $ok_flags, $num, $e, $what);
}
sub parse_flags($$$$$)
{
my ($rflg, $ok_flags, $num, $e, $what) = @_;
my $add = $$rflg;
$$rflg = "";
process_flag_add($rflg, $add, $ok_flags, $num, $e, $what);
}
sub parse_flag_update($)
{
my ($stsr) = @_;
my ($add, $del) = ("", "");
while ($$stsr =~ s,^([-+])([^-+]+),,) {
if ($1 eq "+") {
$add .= $2;
} else {
$del .= $2;
}
}
return ($add, $del);
}
# Returns UID.
sub create_msg($$$$$)
{
my ($num, $flg, $bs, $t, $e) = @_;
my ($mur, $msr, $n2ur) = (\$$bs{max_uid}, $$bs{messages}, $$bs{num2uid});
$$mur++;
if ($flg ne "_") {
parse_flags(\$flg, $msg_flags, $num, $e, "$t side");
$$msr{$$mur} = [ $num, $flg ];
}
push @{$$n2ur{$num}}, $$mur;
return $$mur;
}
# Returns old UID, new UID.
sub parse_msg($$$$$)
{
my ($num, $sts, $cs, $t, $e) = @_;
my $bs = $$cs{$t};
my ($msr, $n2ur) = ($$bs{messages}, $$bs{num2uid});
$$cs{"${t}_trash"}{$num} = 1
if ($sts =~ s,^#,,);
my $ouid;
my $uids = \@{$$n2ur{$num}};
if ($sts =~ s,^&$,,) {
$ouid = 0;
} elsif ($sts =~ s,^&(\d+),,) {
my $n = int($1);
die("Referencing unrecognized instance $n of message ".mn($num)." on $t side (at $e).\n")
if (!$n || $n > @$uids);
$ouid = $$uids[$n - 1];
} else {
$ouid = @$uids ? $$uids[-1] : 0;
}
my $nuid = ($sts =~ s,^\|,,) ? 0 : $ouid;
if ($sts eq "_" or $sts =~ s,^\*,,) {
die("Adding already present message ".mn($num)." on $t side (at $e).\n")
if (defined($$msr{$ouid}));
$nuid = create_msg($num, $sts, $bs, $t, $e);
} elsif ($sts =~ s,^\^,,) {
die("Duplicating absent message ".mn($num)." on $t side (at $e).\n")
if (!defined($$msr{$ouid}));
$nuid = create_msg($num, $sts, $bs, $t, $e);
} elsif ($sts eq "/") {
# Note that we don't delete $$n2ur{$num}, as state entries may
# refer to expunged messages. Subject re-use is not supported here.
die("Deleting absent message ".mn($num)." from $t side (at $e).\n")
if (!delete $$msr{$ouid});
$nuid = 0;
} elsif ($sts ne "") {
my ($add, $del) = parse_flag_update(\$sts);
die("Unrecognized message command '$sts' for $t side (at $e).\n")
if ($sts ne "");
die("No message ".mn($num)." present on $t side (at $e).\n")
if (!defined($$msr{$ouid}));
process_flag_update(\$$msr{$ouid}[1], $add, $del, $msg_flags, $num, $e, "$t side");
}
return $ouid, $nuid;
}
# Returns UID.
sub resolv_msg($$$)
{
my ($num, $cs, $t) = @_;
return 0 if (!$num);
my $uids = \@{$$cs{$t}{num2uid}{$num}};
die("No message ".mn($num)." present on $t side (in header).\n")
if (!@$uids);
return $$uids[-1];
}
# Returns index, or undef if not found.
sub find_ent($$$)
{
my ($fu, $nu, $ents) = @_;
for (my $i = 0; $i < @$ents; $i++) {
my $ent = $$ents[$i];
return $i if ($$ent[0] == $fu and $$ent[1] == $nu);
}
return undef;
}
sub find_ent_chk($$$$$)
{
my ($fu, $nu, $cs, $num, $e) = @_;
my $enti = find_ent($fu, $nu, $$cs{state}{entries});
die("No state entry $fu:$nu present for ".mn($num)." (at $e).\n")
if (!defined($enti));
return $enti;
}
sub parse_chan($;$)
{
my ($ics, $ref) = @_;
my $cs;
if ($ref) {
$cs = clone($ref);
} else {
$cs = {
# messages: { uid => [ subject, flags ], ... }
far => { max_uid => 0, messages => {}, num2uid => {} },
near => { max_uid => 0, messages => {}, num2uid => {} },
# trashed messages: { subject => is_placeholder, ... }
far_trash => { },
near_trash => { },
# entries: [ [ far_uid, near_uid, flags ], ... ]
state => { entries => [] }
};
}
my $ss = $$cs{state};
my $ents = $$ss{entries};
my $enti;
for (my ($i, $e) = (3, 1); $i < @$ics; $i += 4, $e++) {
my ($num, $far, $sts, $near) = @{$ics}[$i .. $i+3];
my ($ofu, $nfu) = parse_msg($num, $far, $cs, "far", $e);
my ($onu, $nnu) = parse_msg($num, $near, $cs, "near", $e);
if ($sts =~ s,^\*,,) {
$enti = find_ent($nfu, $nnu, $ents);
die("State entry $nfu:$nnu already present for ".mn($num)." (at $e).\n")
if (defined($enti));
parse_flags(\$sts, $sync_flags, $num, $e, "sync entry");
push @$ents, [ $nfu, $nnu, $sts ];
} elsif ($sts =~ s,^\^,,) {
die("No current state entry for ".mn($num)." (at $e).\n")
if (!defined($enti));
parse_flags(\$sts, $sync_flags, $num, $e, "sync entry");
splice @$ents, $enti++, 0, ([ $nfu, $nnu, $sts ]);
} elsif ($sts eq "/") {
$enti = find_ent_chk($ofu, $onu, $cs, $num, $e);
splice @$ents, $enti, 1;
} elsif ($sts ne "") {
my $t = -1;
if ($sts =~ s,^<,,) {
$t = 0;
} elsif ($sts =~ s,^>,,) {
$t = 1;
}
my ($add, $del) = parse_flag_update(\$sts);
die("Unrecognized state command '".$sts."' for ".mn($num)." (at $e).\n")
if ($sts ne "");
$enti = find_ent_chk($ofu, $onu, $cs, $num, $e);
my $ent = $$ents[$enti++];
process_flag_update(\$$ent[2], $add, $del, $sync_flags, $num, $e, "sync entry");
if ($t >= 0) {
my $uid = $t ? $nnu : $nfu;
$$ent[$t] = ($uid && $$cs{$t ? "near" : "far"}{messages}{$uid}) ? $uid : 0;
}
} else {
$enti = undef;
}
}
$$ss{max_pulled} = resolv_msg($$ics[0], $cs, "far");
$$ss{max_expired} = resolv_msg($$ics[1], $cs, "far");
$$ss{max_pushed} = resolv_msg($$ics[2], $cs, "near");
return $cs;
}
sub qm($)
{
shift;
s/\\/\\\\/g;
s/\"/\\"/g;
s/\"/\\"/g;
s/\n/\\n/g;
return $_;
}
# [ $far, $near, $channel ]
sub writecfg($)
{
my ($sfx) = @_;
open(FILE, ">", ".mbsyncrc") or
die "Cannot open .mbsyncrc.\n";
print FILE
"FSync no
MaildirStore far
Path \"\"
Inbox far
".$$sfx[0]."
MaildirStore near
Path \"\"
Inbox near
".$$sfx[1]."
Channel test
Far :far:
Near :near:
SyncState *
".$$sfx[2];
close FILE;
}
sub killcfg()
{
unlink $_ for (glob("*.log"));
unlink ".mbsyncrc";
}
# $run_async, $mbsync_options, $log_file
# Return: $exit_code, \@mbsync_output
sub runsync($$$)
{
my ($async, $flags, $file) = @_;
my $cmd;
if ($use_vg) {
$cmd = "valgrind -q --error-exitcode=1 ";
} elsif ($use_st) {
$cmd = "strace ";
} else {
$flags .= " -D";
}
if ($async == 2) {
$flags .= " -TA";
} elsif ($async == 1) {
$flags .= " -Ta";
}
$cmd .= "$mbsync -Tz $flags -c .mbsyncrc test";
open FILE, "$cmd 2>&1 |";
my @out = <FILE>;
close FILE or push(@out, $! ? "*** error closing mbsync: $!\n" : "*** mbsync exited with signal ".($?&127).", code ".($?>>8)."\n");
if ($file) {
open FILE, ">$file" or die("Cannot create $file: $!\n");
print FILE @out;
close FILE;
}
return $?, \@out;
}
use constant CHOMP => 1;
sub readfile($;$)
{
my ($file, $chomp) = @_;
open(FILE, $file) or return;
my @nj = <FILE>;
close FILE;
chomp(@nj) if ($chomp);
return \@nj;
}
# $path
sub readbox_impl($$)
{
my ($bn, $cb) = @_;
(-d $bn."/tmp" and -d $bn."/new" and -d $bn."/cur") or
die "Invalid mailbox '$bn'.\n";
for my $d ("cur", "new") {
opendir(DIR, $bn."/".$d) or next;
for my $f (grep(!/^\.\.?$/, readdir(DIR))) {
open(FILE, "<", $bn."/".$d."/".$f) or die "Cannot read message '$f' in '$bn'.\n";
my ($sz, $num, $ph) = (0);
while (<FILE>) {
/^Subject: (\[placeholder\] )?(\d+)$/ && ($ph = defined($1), $num = int($2));
$sz += length($_);
}
close FILE;
if (!defined($num)) {
print STDERR "message '$f' in '$bn' has no identifier.\n";
exit 1;
}
$cb->($num, $ph, $sz, $f);
}
}
}
# $path
sub readbox($)
{
my $bn = shift;
(-d $bn) or
die "No mailbox '$bn'.\n";
my %ms;
readbox_impl($bn, sub {
my ($num, $ph, $sz, $f) = @_;
if ($f !~ /^\d+\.\d+_\d+\.[-[:alnum:]]+,U=(\d+):2,(.*)$/) {
print STDERR "unrecognided file name '$f' in '$bn'.\n";
exit 1;
}
my ($uid, $flg) = (int($1), $2);
@{$ms{$uid}} = ($num, $flg.($sz > 1000 ? "*" : "").($ph ? "?" : ""));
});
my $uidval = readfile($bn."/.uidvalidity", CHOMP);
die "Cannot read UID validity of mailbox '$bn': $!\n" if (!$uidval);
my $mu = $$uidval[1];
return { max_uid => $mu, messages => \%ms };
}
# $path
sub readtrash($)
{
my $bn = shift;
(-d $bn) or
return {};
my %ms;
readbox_impl($bn, sub {
my ($num, $ph, undef, undef) = @_;
$ms{$num} = $ph;
});
return \%ms;
}
# \%fallback_sync_state
sub readstate(;$)
{
my ($fbss) = @_;
my $fn = "near/.mbsyncstate";
if ($fbss) {
$fn .= ".new";
return $fbss if (!-s $fn);
}
my $ls = readfile($fn, CHOMP);
if (!$ls) {
print STDERR "Cannot read sync state $fn: $!\n";
return;
}
my @ents;
my %ss = (
max_pulled => 0,
max_expired => 0,
max_pushed => 0,
entries => \@ents
);
my ($far_val, $near_val) = (0, 0);
my %hdr = (
'FarUidValidity' => \$far_val,
'NearUidValidity' => \$near_val,
'MaxPulledUid' => \$ss{max_pulled},
'MaxPushedUid' => \$ss{max_pushed},
'MaxExpiredFarUid' => \$ss{max_expired}
);
OUTER: while (1) {
while (@$ls) {
$_ = shift(@$ls);
last OUTER if (!length($_));
if (!/^([^ ]+) (\d+)$/) {
print STDERR "Malformed sync state header entry: $_\n";
return;
}
my $want = delete $hdr{$1};
if (!defined($want)) {
print STDERR "Unexpected sync state header entry: $1\n";
return;
}
$$want = int($2);
}
print STDERR "Unterminated sync state header.\n";
return;
}
delete $hdr{'MaxExpiredFarUid'}; # optional field
my @ky = keys %hdr;
if (@ky) {
print STDERR "Keys missing from sync state header: @ky\n";
return;
}
if ($far_val ne '1' or $near_val ne '1') {
print STDERR "Unexpected UID validity $far_val $near_val (instead of 1 1)\n";
return;
}
for (@$ls) {
if (!/^(\d+) (\d+) (.*)$/) {
print STDERR "Malformed sync state entry: $_\n";
return;
}
push @ents, [ int($1), int($2), $3 ];
}
return \%ss;
}
# \%fallback_sync_state
sub readchan(;$)
{
my ($fbss) = @_;
return {
far => readbox("far"),
near => readbox("near"),
far_trash => readtrash("far_trash"),
near_trash => readtrash("near_trash"),
state => readstate($fbss)
};
}
# $box_name, \%box_state
sub mkbox($$)
{
my ($bn, $bs) = @_;
rmtree($bn);
(mkdir($bn) and mkdir($bn."/tmp") and mkdir($bn."/new") and mkdir($bn."/cur")) or
die "Cannot create mailbox $bn.\n";
open(FILE, ">", $bn."/.uidvalidity") or die "Cannot create UID validity for mailbox $bn.\n";
print FILE "1\n$$bs{max_uid}\n";
close FILE;
my $ms = $$bs{messages};
for my $uid (keys %$ms) {
my ($num, $flg) = @{$$ms{$uid}};
my $big = $flg =~ s/\*//;
my $ph = $flg =~ s/\?//;
open(FILE, ">", $bn."/".($flg =~ /S/ ? "cur" : "new")."/0.1_".$num.".local,U=".$uid.":2,".$flg) or
die "Cannot create message ".mn($num)." in mailbox $bn.\n";
print FILE "From: foo\nTo: bar\nDate: Thu, 1 Jan 1970 00:00:00 +0000\nSubject: ".($ph?"[placeholder] ":"").$num."\n\n".(("A"x50)."\n")x($big*30);
close FILE;
}
}
# \%sync_state
sub mkstate($)
{
my ($ss) = @_;
open(FILE, ">", "near/.mbsyncstate") or
die "Cannot create sync state.\n";
print FILE "FarUidValidity 1\nMaxPulledUid ".$$ss{max_pulled}."\n".
"NearUidValidity 1\nMaxExpiredFarUid ".$$ss{max_expired}.
"\nMaxPushedUid ".$$ss{max_pushed}."\n\n";
for my $ent (@{$$ss{entries}}) {
print FILE $$ent[0]." ".$$ent[1]." ".$$ent[2]."\n";
}
close FILE;
}
# \%chan_state
sub mkchan($)
{
my ($cs) = @_;
mkbox("far", $$cs{far});
mkbox("near", $$cs{near});
rmtree("far_trash");
rmtree("near_trash");
mkstate($$cs{state});
}
# $box_name, \%actual_box_state, \%reference_box_state
sub cmpbox($$$)
{
my ($bn, $bs, $ref_bs) = @_;
my $ret = 0;
my ($ref_mu, $ref_ms) = ($$ref_bs{max_uid}, $$ref_bs{messages});
my ($mu, $ms) = ($$bs{max_uid}, $$bs{messages});
if ($mu != $ref_mu) {
print STDERR "MAXUID mismatch for '$bn': got $mu, wanted $ref_mu\n";
$ret = 1;
}
for my $uid (sort { $a <=> $b } keys %$ref_ms) {
my ($num, $flg) = @{$$ref_ms{$uid}};
my $m = $$ms{$uid};
if (!defined $m) {
print STDERR "Missing message $bn:$uid:".mn($num)."\n";
$ret = 1;
next;
}
if ($$m[0] != $num) {
print STDERR "Subject mismatch for $bn:$uid:".
" got ".mn($$m[0]).", wanted ".mn($num)."\n";
return 1;
}
if ($$m[1] ne $flg) {
print STDERR "Flag mismatch for $bn:$uid:".mn($num).":".
" got ".mf($$m[1]).", wanted ".mf($flg)."\n";
$ret = 1;
}
}
for my $uid (sort { $a <=> $b } keys %$ms) {
if (!defined($$ref_ms{$uid})) {
print STDERR "Excess message $bn:$uid:".mn($$ms{$uid}[0])."\n";
$ret = 1;
}
}
return $ret;
}
# $box_name, \%actual_box_state, \%reference_box_state
sub cmptrash($$$)
{
my ($bn, $ms, $ref_ms) = @_;
my $ret = 0;
for my $num (sort { $a <=> $b } keys %$ref_ms) {
my $ph = $$ms{$num};
if (!defined($ph)) {
print STDERR "Missing message $bn:".mn($num)."\n";
$ret = 1;
}
if ($ph) {
print STDERR "Message $bn:".mn($num)." is placeholder\n";
$ret = 1;
}
}
for my $num (sort { $a <=> $b } keys %$ms) {
if (!defined($$ref_ms{$num})) {
print STDERR "Excess message $bn:".mn($num).($$ms{$num} ? " (is placeholder)" : "")."\n";
$ret = 1;
}
}
return $ret;
}
sub mapmsg($$)
{
my ($uid, $bs) = @_;
if ($uid) {
if (my $msg = $$bs{messages}{$uid}) {
return $$msg[0];
}
}
return 0;
}
# \%actual_chan_state, \%reference_chan_state
sub cmpstate($$)
{
my ($cs, $ref_cs) = @_;
my ($ss, $fbs, $nbs) = ($$cs{state}, $$cs{far}, $$cs{near});
return 1 if (!$ss);
my ($ref_ss, $ref_fbs, $ref_nbs) = ($$ref_cs{state}, $$ref_cs{far}, $$ref_cs{near});
return 0 if ($ss == $ref_ss);
my $ret = 0;
for my $h (['MaxPulledUid', 'max_pulled'],
['MaxExpiredFarUid', 'max_expired'],
['MaxPushedUid', 'max_pushed']) {
my ($hn, $sn) = @$h;
my ($got, $want) = ($$ss{$sn}, $$ref_ss{$sn});
if ($got != $want) {
print STDERR "Sync state header entry $hn mismatch: got $got, wanted $want\n";
$ret = 1;
}
}
my $ref_ents = $$ref_ss{entries};
my $ents = $$ss{entries};
for (my $i = 0; $i < @$ents || $i < @$ref_ents; $i++) {
my ($ent, $fuid, $nuid, $num);
if ($i < @$ents) {
$ent = $$ents[$i];
($fuid, $nuid) = ($$ent[0], $$ent[1]);
my ($fnum, $nnum) = (mapmsg($fuid, $fbs), mapmsg($nuid, $nbs));
if ($fnum && $nnum && $fnum != $nnum) {
print STDERR "Invalid sync state entry $fuid:$nuid:".
" mismatched subjects (".mn($fnum).":".mn($nnum).")\n";
return 1;
}
$num = $fnum || $nnum;
}
if ($i == @$ref_ents) {
print STDERR "Excess sync state entry $fuid:$nuid (".mn($num).")\n";
return 1;
}
my $rent = $$ref_ents[$i];
my ($rfuid, $rnuid) = ($$rent[0], $$rent[1]);
my $rnum = mapmsg($rfuid, $ref_fbs) || mapmsg($rnuid, $ref_nbs);
if ($i == @$ents) {
print STDERR "Missing sync state entry $rfuid:$rnuid (".mn($rnum).")\n";
return 1;
}
if ($fuid != $rfuid || $nuid != $rnuid || $num != $rnum) {
print STDERR "Unexpected sync state entry:".
" got $fuid:$nuid (".mn($num)."), wanted $rfuid:$rnuid (".mn($rnum).")\n";
return 1;
}
if ($$ent[2] ne $$rent[2]) {
print STDERR "Flag mismatch in sync state entry $fuid:$nuid (".mn($rnum)."):".
" got ".mf($$ent[2]).", wanted ".mf($$rent[2])."\n";
$ret = 1;
}
}
return $ret;
}
# \%actual_chan_state, \%reference_chan_state
sub cmpchan($$)
{
my ($cs, $ref_cs) = @_;
my $rslt = 0;
$rslt |= cmpbox("far", $$cs{far}, $$ref_cs{far});
$rslt |= cmpbox("near", $$cs{near}, $$ref_cs{near});
$rslt |= cmptrash("far_trash", $$cs{far_trash}, $$ref_cs{far_trash});
$rslt |= cmptrash("near_trash", $$cs{near_trash}, $$ref_cs{near_trash});
$rslt |= cmpstate($cs, $ref_cs);
return $rslt;
}
# \%box_state
sub printbox($)
{
my ($bs) = @_;
my ($mu, $ms) = ($$bs{max_uid}, $$bs{messages});
print " [ $mu,\n ";
my $frst = 1;
for my $uid (sort { $a <=> $b } keys %$ms) {
my ($num, $flg) = @{$$ms{$uid}};
if ($frst) {
$frst = 0;
} else {
print ", ";
}
print mn($num).", ".$uid.", \"".$flg."\"";
}
print " ],\n";
}
# \%sync_state
sub printstate($)
{
my ($ss) = @_;
return if (!$ss);
print " [ ".$$ss{max_pulled}.", ".$$ss{max_expired}.", ".$$ss{max_pushed}.",\n ";
my $frst = 1;
for my $ent (@{$$ss{entries}}) {
if ($frst) {
$frst = 0;
} else {
print ", ";
}
print(($$ent[0] // "??").", ".($$ent[1] // "??").", \"".($$ent[2] // "??")."\"");
}
print " ],\n";
}
# \%chan_state
sub printchan($)
{
my ($cs) = @_;
printbox($$cs{far});
printbox($$cs{near});
printstate($$cs{state});
}
# $run_async, \%source_state, \%target_state, \@channel_configs
sub test_impl($$$$)
{
my ($async, $sx, $tx, $sfx) = @_;
mkchan($sx);
my ($xopt, $xsopt) = @$sfx > 3 ? ($$sfx[3], " ".$$sfx[3]) : ("", "");
my ($xc, $ret) = runsync($async, "-Tj -TJ".$xsopt, "1-initial.log");
my $rtx = readchan($$sx{state}) if (!$xc);
if ($xc || cmpchan($rtx, $tx)) {
print "Input:\n";
printchan($sx);
print "Options:\n";
print " [ ".join(", ", map('"'.qm($_).'"', @$sfx))." ]\n";
if (!$xc) {
print "Expected result:\n";
printchan($tx);
print "Actual result:\n";
printchan($rtx);
}
print "Debug output:\n";
print @$ret;
exit 1;
}
my ($nj, $njl, $nje) = (undef, 0, 0);
if ($$rtx{state} != $$sx{state}) {
$nj = readfile("near/.mbsyncstate.journal");
STEPS: {
for (reverse @$ret) {
if (/^### (\d+) steps, (\d+) entries ###$/) {
$njl = int($1) - 1;
$nje = int($2);
last STEPS;
}
}
die("Cannot extract step count.\n");
}
my ($jxc, $jret) = runsync($async, "-0 --no-expunge".$xsopt, "2-replay.log");
my $jrcs = readstate() if (!$jxc);
if ($jxc || cmpstate({ far => $$tx{far}, near => $$tx{near}, state => $jrcs }, $tx)) {
print "Journal replay failed.\n";
print "Options:\n";
print " [ ".join(", ", map('"'.qm($_).'"', @$sfx))." ], [ \"-0\", \"--no-expunge\" ]\n";
print "Old State:\n";
printstate($$sx{state});
print "Journal:\n".join("", @$nj)."\n";
if (!$jxc) {
print "Expected New State:\n";
printstate($$tx{state});
print "New State:\n";
printstate($jrcs);
}
print "Debug output:\n";
print @$jret;
exit 1;
}
}
my ($ixc, $iret) = runsync($async, $xopt, "3-verify.log");
my $irtx = readchan() if (!$ixc);
if ($ixc || cmpchan($irtx, $tx)) {
print "Idempotence verification run failed.\n";
print "Input == Expected result:\n";
printchan($tx);
print "Options:\n";
print " [ ".join(", ", map('"'.qm($_).'"', @$sfx))." ]\n";
if (!$ixc) {
print "Actual result:\n";
printchan($irtx);
}
print "Debug output:\n";
print @$iret;
exit 1;
}
rmtree "near";
rmtree "far";
rmtree "near_trash";
rmtree "far_trash";
for (my $l = 1; $l <= $njl; $l++) {
mkchan($sx);
my ($nxc, $nret) = runsync($async, "-Ts$l".$xsopt, "4-interrupt.log");
if ($nxc != 100 << 8) {
print "Interrupting at step $l/$njl failed.\n";
print "Debug output:\n";
print @$nret;
exit 1;
}
my $pnnj = readfile("near/.mbsyncstate.journal");
($nxc, $nret) = runsync($async, "-Tj".$xsopt, "5-resume.log");
my $nrtx = readchan($$sx{state}) if (!$nxc);
if ($nxc || cmpchan($nrtx, $tx)) {
print "Resuming from step $l/$njl failed.\n";
print "Input:\n";
printchan($sx);
print "Options:\n";
print " [ ".join(", ", map('"'.qm($_).'"', @$sfx))." ]\n";
my $nnj = readfile("near/.mbsyncstate.journal");
my $ln = $#$pnnj;
print "Journal:\n".join("", @$nnj[0..$ln])."-------\n".join("", @$nnj[($ln + 1)..$#$nnj])."\n";
print "Full journal:\n".join("", @$nj[0..$nje])."=======\n".join("", @$nj[($nje + 1)..$#$nj])."\n";
if (!$nxc) {
print "Expected result:\n";
printchan($tx);
print "Actual result:\n";
printchan($nrtx);
}
print "Debug output:\n";
print @$nret;
exit 1;
}
rmtree "near";
rmtree "far";
rmtree "near_trash";
rmtree "far_trash";
}
}
# $title, \@source_state, \@target_state, \@channel_configs
sub test($$$$)
{
my ($ttl, $isx, $itx, $sfx) = @_;
if (@match) {
if ($start) {
return if (index($ttl, $match[0]) < 0);
@match = ();
} else {
return if (!grep { index($ttl, $_) >= 0 } @match);
}
}
print "Testing: ".$ttl." ...\n";
writecfg($sfx);
my $sx = parse_chan($isx);
my $tx = parse_chan($itx, $sx);
test_impl(0, $sx, $tx, $sfx);
test_impl(1, $sx, $tx, $sfx);
test_impl(2, $sx, $tx, $sfx);
killcfg();
}
################################################################################
# Format of the test defs:
# ( max_pulled, max_expired, max_pushed, { subject, far, state, near }... )
# Everything is a delta; for the input, the reference is the empty state.
# Common commands:
# *f => create with flags, appending at end of list
# / => destroy
# -f, +f => remove/add flags
# Far/near:
# Special commands:
# _ => create phantom message (reserve UID for expunged message)
# ^f => create with flags, duplicating the subject
# # => create in trash; deletion may follow
# | => use zero UID for state modification, even if msg exists; cmd may follow
# & => use zero UID for state identification, even if message exists
# &n => use UID of n'th occurence of subject for state id; command may follow
# Special flag suffixes:
# * => big
# ? => placeholder
# State:
# Special commands:
# <, > => update far/near message link; flag updates may follow
# ^f => create with flags, appending right after last command's entry
# Special flag prefixes as in actual state file.
# Generic syncing tests
my @x01 = (
I, 0, I,
P, "_", "*", "_",
R, "*", "", "", # Skipped/failed messages to prevent maxuid topping
S, "", "", "*",
A, "*F", "*", "*",
B, "*", "*", "*F",
C, "*FS", "*", "*F",
D, "*", "*", "*",
E, "*T", "*", "*",
F, "*", "*", "*T",
O, "*T", "*T", "*T",
Q, "_", "*T", "*T",
G, "*F", "*", "_",
H, "*FT", "*", "*",
I, "_", "*", "*",
J, "*T", "", "",
K, "*P", "", "",
L, "", "", "*T",
M, "", "", "*",
);
my @O01 = ("", "", "");
my @X01 = (
M, 0, K,
P, "", "/", "",
A, "", "+F", "+F",
B, "+F", "+F", "",
C, "", "+FS", "+S",
E, "", "+T", "+T",
F, "+T", "+T", "",
Q, "", "<", "",
G, "+T", ">", "",
H, "", "+FT", "+FT",
I, "", "<", "+T",
L, "*T", "*T", "",
M, "*", "*", "",
J, "", "*T", "*T",
K, "", "*P", "*P",
);
test("full", \@x01, \@X01, \@O01);
my @O02 = ("", "", "Expunge Both\n");
my @X02 = (
M, 0, K,
P, "", "/", "",
A, "", "+F", "+F",
B, "+F", "+F", "",
C, "", "+FS", "+S",
E, "/", "/", "/",
F, "/", "/", "/",
O, "/", "/", "/",
Q, "", "/", "/",
G, "/", "/", "",
H, "/", "/", "/",
I, "", "/", "/",
J, "/", "", "",
L, "", "", "/",
M, "*", "*", "",
K, "", "*P", "*P",
);
test("full + expunge both", \@x01, \@X02, \@O02);
my @O03 = ("", "", "Expunge Near\n");
my @X03 = (
M, 0, K,
P, "", "/", "",
A, "", "+F", "+F",
B, "+F", "+F", "",
C, "", "+FS", "+S",
E, "", ">+T", "/",
F, "+T", ">+T", "/",
O, "", ">", "/",
Q, "", "/", "/",
G, "+T", ">", "",
H, "", ">+T", "/",
I, "", "/", "/",
L, "", "", "/",
M, "*", "*", "",
K, "", "*P", "*P",
);
test("full + expunge near side", \@x01, \@X03, \@O03);
my @O04 = ("", "", "Sync Pull\n");
my @X04 = (
K, 0, I,
P, "", "/", "", # Only because test uses Maildir driver
A, "", "+F", "+F",
C, "", "+FS", "+S",
E, "", "+T", "+T",
Q, "", "<", "",
H, "", "+FT", "+FT",
I, "", "<", "+T",
J, "", "*T", "*T",
K, "", "*P", "*P",
);
test("pull", \@x01, \@X04, \@O04);
my @O05 = ("", "", "Sync Flags\n");
my @X05 = (
I, 0, I,
P, "", "/", "",
A, "", "+F", "+F",
B, "+F", "+F", "",
C, "", "+FS", "+S",
E, "", "+T", "+T",
F, "+T", "+T", "",
Q, "", "<", "",
H, "", "+FT", "+FT",
);
test("flags", \@x01, \@X05, \@O05);
my @O06 = ("", "", "Sync Gone\n");
my @X06 = (
I, 0, I,
P, "", "/", "",
G, "+T", ">", "",
Q, "", "<", "",
I, "", "<", "+T",
);
test("gone", \@x01, \@X06, \@O06);
my @O07 = ("", "", "Sync New\n");
my @X07 = (
M, 0, K,
L, "*T", "*T", "",
M, "*", "*", "",
J, "", "*T", "*T",
K, "", "*P", "*P",
);
test("new", \@x01, \@X07, \@O07);
my @O08 = ("", "", "Sync PushFlags PullGone\n");
my @X08 = (
I, 0, I,
P, "", "/", "",
B, "+F", "+F", "",
C, "", "+F", "",
F, "+T", "+T", "",
Q, "", "<", "",
I, "", "<", "+T",
);
test("push flags + pull gone", \@x01, \@X08, \@O08);
my @O09 = ("", "", "Sync None\nExpunge Both\n");
my @X09 = (
I, 0, I,
P, "", "/", "",
E, "/", "", "",
F, "", "", "/",
O, "/", "/", "/",
Q, "", "/", "/",
H, "/", "", "",
J, "/", "", "",
L, "", "", "/",
);
test("noop + expunge both", \@x01, \@X09, \@O09);
my @O0A = ("", "", "Sync None\nExpunge Near\n");
# Sync entries are not updated/pruned, because the far side is not loaded.
my @X0A = (
I, 0, I,
F, "", "", "/",
O, "", "", "/",
Q, "", "", "/",
L, "", "", "/",
);
test("noop + expunge near side", \@x01, \@X0A, \@O0A);
# Size restriction tests
my @x20 = (
0, 0, 0,
A, "*", "", "",
B, "*FPS*", "", "",
C, "", "", "*FS*",
);
my @O21 = ("MaxSize 1k\n", "MaxSize 1k\n", "Sync Full Old\nExpunge Near\n");
my @X21 = (
C, 0, B,
C, "*S?", "*<S", "",
A, "", "*", "*",
B, "", "*>PS", "*PS?",
);
test("max size", \@x20, \@X21, \@O21);
my @x22 = (
L, 0, V,
A, "*", "", "",
B, "*PR*", "", "",
V, "*FPR*", "", "",
C, "*FPR?", "*<DP", "*DP*",
W, "*FPR?", "*<DP", "*DFP*",
M, "*FS?", "*<", "**",
N, "*FS?", "*<S", "**",
O, "*FS?", "*<S", "*S*",
P, "*FS?", "*<", "*S*",
Q, "*F?", "*<S", "*S*",
R, "*F?", "*<", "*S*",
S, "*F?", "*<S", "**",
D, "*PR?", "*<DP", "*DFP*",
E, "*PR*", "*>DP", "*DP?",
F, "*S*", "*>", "*?",
G, "*S*", "*>S", "*?",
H, "*S*", "*>S", "*S?",
I, "*S*", "*>", "*S?",
J, "**", "*>S", "*S?",
K, "**", "*>", "*S?",
L, "**", "*>S", "*?",
A, "", "*", "*",
B, "", "*>DP", "*DFP?",
V, "", "*>DP", "*DFP?",
);
my @X22 = (
S, 0, V,
B, "", ">->D+R", "^PR*",
B, "", "", "&1/",
V, "", ">->D+FR", "^FPR*",
V, "", "", "&1/",
C, "^PR*", "<-<D+R", "-D+R",
C, "&1+T", "^", "&",
W, "^FPR*", "<-<D+FR", "-D+R",
W, "&1+T", "^", "&",
M, "^*", "<-<", "",
M, "&1+T", "^", "&",
N, "^*", "<-<S", "",
N, "&1+T", "^", "&",
O, "^S*", "<-<", "",
O, "&1+T", "^", "&",
P, "^S*", "<-<+S", "",
P, "&1+T", "^", "&",
Q, "^*", "<-<S", "-S",
Q, "&1+T", "^", "&",
R, "^S*", "<-<+S", "",
R, "&1+T", "^", "&",
S, "^*", "<-<S", "",
S, "&1+T", "^", "&",
D, "", "-D+R", "-D+R",
E, "", "-D+R", "-D+R",
F, "", "+S", "+S",
G, "-S", "-S", "",
I, "", "+S", "",
J, "", "-S", "-S",
L, "", "-S", "",
);
test("max size + flagging", \@x22, \@X22, \@O21);
my @x24 = (
C, 0, A,
A, "*", "*", "*",
B, "**", "*^", "",
C, "*FP*", "*^", "",
);
my @X24 = (
C, 0, C,
B, "", ">-^+>", "*?",
C, "", ">-^+>P", "*P?",
);
test("max size (pre-1.4 legacy)", \@x24, \@X24, \@O21);
# Expiration tests
my @x30 = (
0, 0, 0,
A, "*F", "", "",
B, "*", "", "",
C, "*S", "", "",
D, "*", "", "",
E, "*S", "", "",
F, "*", "", "",
);
my @O31 = ("", "", "MaxMessages 3\n");
my @X31 = (
F, C, F,
A, "", "*F", "*F",
B, "", "*", "*",
D, "", "*", "*",
E, "", "*S", "*S",
F, "", "*", "*",
);
test("max messages", \@x30, \@X31, \@O31);
my @O32 = ("", "", "MaxMessages 3\nExpireUnread yes\n");
my @X32 = (
F, C, F,
A, "", "*F", "*F",
D, "", "*", "*",
E, "", "*S", "*S",
F, "", "*", "*",
);
test("max messages vs. unread", \@x30, \@X32, \@O32);
my @x33 = (
G, D, G,
A, "*F", "*F", "*F",
B, "*", "*", "*S",
C, "*S", "*", "*",
D, "*S", "", "",
E, "*", "*", "*S",
F, "*S", "*S", "*S",
G, "*", "*", "*S",
H, "*", "", "",
I, "*", "", "",
);
my @X33 = (
I, F, I,
B, "+S", "+~S", "+T",
C, "", "+~S", "+ST",
E, "+S", "+~S", "+T",
F, "", "+~", "+T",
G, "+S", "+S", "",
H, "", "*", "*",
I, "", "*", "*",
);
test("max messages + expire - full", \@x33, \@X33, \@O31);
my @O34 = ("", "", "Sync New\nMaxMessages 3\n");
my @X34 = (
I, F, I,
B, "", "+~", "+T",
E, "", "+~", "+T",
F, "", "+~", "+T",
H, "", "*", "*",
I, "", "*", "*",
);
test("max messages + expire - new", \@x33, \@X34, \@O34);
my @x35 = (
B, 0, B,
A, "*F", "*F", "*F",
B, "*", "*", "*",
C, "*", "", "",
D, "*F", "", "",
E, "*", "", "",
F, "*", "", "",
G, "*", "", "",
H, "*F", "", "",
);
my @O35 = ("", "", "Sync New\nMaxMessages 3\nExpireUnread yes\n");
my @X35 = (
H, E, H,
B, "", "+~", "+T",
D, "", "*F", "*F",
F, "", "*", "*",
G, "", "*", "*",
H, "", "*F", "*F",
);
test("max messages + expire - too many new", \@x35, \@X35, \@O35);
my @x38 = (
F, C, 0,
A, "*FS", "*FS", "*S",
B, "*FS", "*~S", "*ST",
C, "*S", "*~S", "_",
D, "*", "*", "*",
E, "*", "*", "*",
F, "*", "*", "*",
);
my @O38 = ("", "", "MaxMessages 3\nExpunge Both\n");
my @X38 = (
F, C, F,
A, "-F", "/", "/",
B, "", "-~+F", "-T+F",
C, "", "/", "",
);
test("max messages + expunge", \@x38, \@X38, \@O38);
# Expiration with size restriction tests
my @x40 = (
P, 0, P,
A, "**", "*>", "*?",
B, "**", "*>", "*S?",
C, "**", "*>S", "*S?",
D, "*S*", "*>S", "*S?",
E, "*S*", "*>", "*S?",
F, "*S*", "*>S", "*?",
G, "*S*", "*>", "*?",
H, "**", "*>S", "*?",
I, "*F*", "*>", "*?",
J, "*F*", "*>", "*S?",
K, "*F*", "*>S", "*S?",
L, "*FS*", "*>S", "*S?",
M, "*FS*", "*>", "*S?",
N, "*FS*", "*>S", "*?",
O, "*FS*", "*>", "*?",
P, "*F*", "*>S", "*?",
Q, "**", "", "",
R, "*FS*", "", "",
S, "*S*", "", "",
T, "**", "", "",
U, "*FS*", "", "",
V, "*S*", "", "",
);
my @O41 = ("", "", "MaxSize 1k\nMaxMessages 3\nExpireUnread no\n");
my @X41 = (
V, S, V,
C, "", "-S", "-S",
D, "", "+~", "+T",
E, "", "+~S", "+T",
F, "-S", "-S", "",
G, "", "+~S", "+ST",
H, "", "-S", "",
K, "", "-S", "-S",
M, "", "+S", "",
N, "-S", "-S", "",
O, "", "+S", "+S",
P, "", "-S", "",
Q, "", "*>", "*?",
R, "", "*>S", "*S?",
T, "", "*>", "*?",
U, "", "*>S", "*S?",
V, "", "*>S", "*S?",
);
test("max size without upgrade + max messages", \@x40, \@X41, \@O41);
my @x42 = (
T, Q, T,
A, "**", "*>", "*F?",
B, "**", "*>", "*FS?",
C, "**", "*>S", "*FS?",
D, "*S*", "*>S", "*FS?",
E, "*S*", "*>", "*FS?",
F, "*S*", "*>S", "*F?",
G, "*S*", "*>", "*F?",
H, "**", "*>S", "*F?",
I, "*F*", "*>", "*F?",
J, "*F*", "*>", "*FS?",
K, "*F*", "*>S", "*FS?",
L, "*FS*", "*>S", "*FS?",
M, "*FS*", "*>", "*FS?",
N, "*FS*", "*>S", "*F?",
O, "*FS*", "*>", "*F?",
P, "*F*", "*>S", "*F?",
Q, "*S", "", "",
R, "*", "*", "*",
S, "*", "*", "*",
T, "*", "*", "*",
);
my @X43 = (
T, Q, P,
A, "", ">->", "^*",
A, "&", "^", "&1+T",
B, "", ">->", "^*",
B, "&", "^", "&1+T",
C, "", ">->S", "^*",
C, "&", "^", "&1+T",
D, "", "/", "",
D, "&", "^", "&1+T",
E, "", "/", "",
E, "&", "^", "&1+T",
F, "-S", ">->S", "^*",
F, "&", "^", "&1+T",
G, "", "/", "",
G, "&", "^", "&1+T",
H, "", ">->S", "^*",
H, "&", "^", "&1+T",
I, "", ">->+F", "^F*",
I, "&", "^", "&1+T",
J, "", ">->+F", "^F*",
J, "&", "^", "&1+T",
K, "", ">->S+F", "^F*",
K, "&", "^", "&1+T",
L, "", ">->+F", "^FS*",
L, "&", "^", "&1+T",
M, "", ">->+FS", "^FS*",
M, "&", "^", "&1+T",
N, "-S", ">->S+F", "^F*",
N, "&", "^", "&1+T",
O, "", ">->+FS", "^FS*",
O, "&", "^", "&1+T",
P, "", ">->S+F", "^F*",
P, "&", "^", "&1+T",
);
test("max size with upgrade + max messages", \@x42, \@X43, \@O41);
# Test for legacy/tampered states with inaccurate maxuid tracking
# Joined post-push & post-pull state to have just one test -
# there is no way how this could have occurred naturally.
my @x60 = (
0, C, 0,
A, "*S", "", "_",
B, "*FS", "*FS", "*FS",
C, "*S", "", "_",
D, "*", "*", "*",
E, "*", "*", "*",
F, "*", "", "",
G, "*", "", "",
H, "", "", "*",
I, "", "", "_",
J, "*", "*", "*",
);
my @O61 = ("", "", "Sync Flags\n"); # Need to fetch old messages
my @X61 = (
E, C, E,
);
test("maxuid topping", \@x60, \@X61, \@O61);
# Tests for refreshing previously skipped/failed/expired messages.
# We don't know the flags at the time of the (hypothetical) previous
# sync, so we can't know why a particular message is missing.
# We test with MaxMessages to cover different behaviors wrt maxxfuid.
my @x70 = (
L, E, P,
A, "*", "", "",
B, "*F", "", "",
C, "", "", "*",
D, "*F", "*F", "*F",
E, "*", "", "_", # MaxExpiredFarUid
F, "*", "*", "*",
G, "_", "*T", "*T",
H, "*T", "*T", "_",
I, "", "", "*T",
J, "", "", "*",
K, "*T", "", "",
L, "*", "", "", # MaxPulledUid
M, "_", "*T", "*T",
N, "*T", "*T", "_",
O, "", "", "*T",
P, "", "", "*", # MaxPushedUid
Q, "*T", "", "",
R, "*", "", "",
S, "", "", "*",
);
my @O71 = ("", "", "Sync New\nMaxMessages 20\nExpireUnread yes\n");
my @X71 = (
S, E, R,
N, "", ">", "",
S, "*", "*", "",
Q, "", "*T", "*T",
R, "", "*", "*",
);
test("not old", \@x70, \@X71, \@O71);
my @O71a = ("", "", "Sync New\nMaxMessages 3\nExpireUnread yes\n");
test("not old + expire", \@x70, \@X71, \@O71a);
my @O72 = ("", "", "Sync New Old\nMaxMessages 20\nExpireUnread yes\nExpunge Near\n");
my @X72 = (
S, E, R,
G, "", "/", "/",
H, "", ">", "",
I, "", "", "/",
M, "", "/", "/",
N, "", ">", "",
O, "", "", "/",
C, "*", "*", "",
J, "*", "*", "",
P, "*", "*", "",
S, "*", "*", "",
B, "", "*F", "*F",
L, "", "*", "*",
R, "", "*", "*",
);
test("old + expunge near", \@x70, \@X72, \@O72);
# This omits the entries that cause idempotence problems
# due to not counting/expiring newly pushed messages.
my @x70a = (
L, E, O,
A, "*", "", "",
B, "*F", "", "",
D, "*F", "*F", "*F",
E, "*", "", "_", # MaxExpiredFarUid
G, "_", "*T", "*T",
H, "*T", "*T", "_",
I, "", "", "*T",
K, "*T", "", "",
L, "*", "", "", # MaxPulledUid
M, "_", "*T", "*T",
N, "*T", "*T", "_",
O, "", "", "*T", # MaxPushedUid
Q, "*T", "", "",
R, "*", "", "",
S, "", "", "*",
);
my @O72a = ("", "", "Sync New Old\nMaxMessages 3\nExpireUnread yes\nExpunge Near\n");
my @X72a = (
S, E, R,
G, "", "/", "/",
H, "", ">", "",
I, "", "", "/",
M, "", "/", "/",
N, "", ">", "",
O, "", "", "/",
S, "*", "*", "",
B, "", "*F", "*F",
L, "", "*", "*",
R, "", "*", "*",
);
test("old + expire + expunge near", \@x70a, \@X72a, \@O72a);
my @O73 = ("", "", "Sync Pull New Old\nMaxMessages 20\nExpireUnread yes\n");
my @X73 = (
R, E, P,
B, "", "*F", "*F",
G, "", "<", "",
H, "", ">", "",
K, "", "*T", "*T",
L, "", "*", "*",
M, "", "<", "",
N, "", ">", "",
Q, "", "*T", "*T",
R, "", "*", "*",
);
test("pull old", \@x70, \@X73, \@O73);
# This is "weird", because expiration overtook pulling.
my @x80 = (
D, L, Q,
A, "", "", "*",
B, "*", "", "",
C, "*T", "", "",
D, "*F", "", "", # MaxPulledUid
E, "_", "*T", "*T",
F, "*T", "*T", "_",
G, "", "", "*T",
H, "", "", "*",
I, "*T", "", "",
J, "*", "", "",
K, "*F", "*F", "*F",
L, "*", "*~", "_", # MaxExpiredFarUid
M, "*", "*", "*",
N, "_", "*T", "*T",
O, "*T", "*T", "_",
P, "", "", "*T",
Q, "", "", "*", # MaxPushedUid
R, "*T", "", "",
S, "*", "", "",
T, "", "", "*T",
U, "", "", "*",
);
my @X81 = (
U, L, S,
F, "", ">", "",
L, "", "/", "",
O, "", ">", "",
T, "*T", "*T", "",
U, "*", "*", "",
I, "", "*T", "*T",
J, "", "*", "*",
R, "", "*T", "*T",
S, "", "*", "*",
);
test("weird not old", \@x80, \@X81, \@O71);
my @X81a = (
U, L, S,
F, "", ">", "",
L, "", "/", "",
O, "", ">", "",
T, "*T", "*T", "",
U, "*", "*", "",
I, "", "*T", "*T",
R, "", "*T", "*T",
S, "", "*", "*",
);
test("weird not old + expire", \@x80, \@X81a, \@O71a);
my @X82 = (
U, L, S,
E, "", "/", "/",
F, "", ">", "",
G, "", "", "/",
L, "", "/", "",
N, "", "/", "/",
O, "", ">", "",
P, "", "", "/",
T, "", "", "/",
A, "*", "*", "",
H, "*", "*", "",
Q, "*", "*", "",
U, "*", "*", "",
D, "", "*F", "*F",
J, "", "*", "*",
S, "", "*", "*",
);
test("weird old + expunge near", \@x80, \@X82, \@O72);
my @x80a = (
D, L, Q,
B, "*", "", "",
C, "*T", "", "",
D, "*F", "", "", # MaxPulledUid
E, "_", "*T", "*T",
F, "*T", "*T", "_",
G, "", "", "*T",
H, "", "", "*",
I, "*T", "", "",
K, "*F", "*F", "*F",
L, "*", "*~", "_", # MaxExpiredFarUid
N, "_", "*T", "*T",
O, "*T", "*T", "_",
P, "", "", "*T",
Q, "", "", "*", # MaxPushedUid
R, "*T", "", "",
T, "", "", "*T",
U, "", "", "*",
);
my @X82a = (
U, L, D,
E, "", "/", "/",
F, "", ">", "",
G, "", "", "/",
L, "", "/", "",
N, "", "/", "/",
O, "", ">", "",
P, "", "", "/",
T, "", "", "/",
H, "*", "*", "",
Q, "*", "*", "",
U, "*", "*", "",
D, "", "*F", "*F",
);
test("weird old + expire + expunge near", \@x80a, \@X82a, \@O72a);
my @X83 = (
S, L, Q,
E, "", "<", "",
F, "", ">", "",
L, "", "/", "",
N, "", "<", "",
O, "", ">", "",
D, "", "*F", "*F",
I, "", "*T", "*T",
J, "", "*", "*",
R, "", "*T", "*T",
S, "", "*", "*",
);
test("weird pull old", \@x80, \@X83, \@O73);
# Messages that would be instantly expunged on the target side.
my @x90 = (
C, 0, C,
A, "*DRT*", "*>DS", "*DFPS?",
B, "*DR*", "*>DS", "*DFPST?",
C, "*", "*", "*",
D, "*T", "", "",
);
my @O91 = ("", "", "Expunge Near\n", "-Tx");
my @X91 = (
D, 0, C,
A, "+P", ">+P", "|",
A, "&", "^", "+T",
B, "+PT", ">+PT", "|",
B, "&", "^", "",
);
test("doomed", \@x90, \@X91, \@O91);
# Trashing
my @x10 = (
K, A, K,
A, "*", "*~", "*T",
B, "*T", "*^", "",
C, "*T", "*", "*T",
D, "_", "*", "*",
E, "*", "*", "_",
F, "**", "*>", "*T?",
G, "*T?", "*<", "**",
H, "**", "*>", "*FT?",
I, "*FT?", "*<", "**",
J, "**", "*>", "*F?",
K, "*F?", "*<", "**",
L, "*T", "", "",
M, "", "", "*T",
R, "", "", "*", # Force maxuid in the interrupt-resume test.
S, "*", "", "",
);
my @O11 = ("Trash far_trash\n", "Trash near_trash\n",
"MaxMessages 20\nExpireUnread yes\nMaxSize 1k\nExpunge Both\n");
my @X11 = (
R, A, S,
A, "", "/", "/",
B, "#/", "/", "",
C, "#/", "/", "#/",
D, "", "/", "#/",
E, "#/", "/", "",
F, "#/", "/", "/",
G, "/", "/", "#/",
H, "#/", "/", "/",
I, "/", "/", "#/",
J, "", ">->", "^*",
J, "", "", "&1/",
K, "^*", "<-<", "",
K, "&1/", "", "",
L, "#/", "", "",
M, "", "", "#/",
R, "*", "*", "",
S, "", "*", "*",
);
test("trash", \@x10, \@X11, \@O11);
my @O12 = ("Trash far_trash\n", "Trash near_trash\nTrashNewOnly true\n",
"MaxMessages 20\nExpireUnread yes\nMaxSize 1k\nExpunge Both\n");
my @X12 = (
R, A, S,
A, "", "/", "/",
B, "#/", "/", "",
C, "#/", "/", "/",
D, "", "/", "/",
E, "#/", "/", "",
F, "#/", "/", "/",
G, "/", "/", "#/",
H, "#/", "/", "/",
I, "/", "/", "#/",
J, "", ">->", "^*",
J, "", "", "&1/",
K, "^*", "<-<", "",
K, "&1/", "", "",
L, "#/", "", "",
M, "", "", "#/",
R, "*", "*", "",
S, "", "*", "*",
);
test("trash only new", \@x10, \@X12, \@O12);
my @O13 = ("Trash far_trash\nTrashRemoteNew true\n", "",
"MaxMessages 20\nExpireUnread yes\nMaxSize 1k\nExpunge Both\n");
my @X13 = (
R, A, S,
A, "", "/", "/",
B, "#/", "/", "",
C, "#/", "/", "/",
D, "", "/", "/",
E, "#/", "/", "",
F, "#/", "/", "/",
G, "#/", "/", "/",
H, "#/", "/", "/",
I, "#/", "/", "/",
J, "", ">->", "^*",
J, "", "", "&1/",
K, "^*", "<-<", "",
K, "&1/", "", "",
L, "#/", "", "",
M, "#", "", "/",
R, "*", "*", "",
S, "", "*", "*",
);
test("trash new remotely", \@x10, \@X13, \@O13);
print "OK.\n";