Imported Upstream version 3.3.3
[debian/amanda] / installcheck / chunker.pl
1 # Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use Test::More tests => 60;
21
22 use warnings;
23 use strict;
24
25 use lib '@amperldir@';
26 use Installcheck::Run;
27 use Installcheck::Mock;
28 use IO::Handle;
29 use IPC::Open3;
30 use Data::Dumper;
31 use IO::Socket::INET;
32 use POSIX ":sys_wait_h";
33 use Cwd qw(abs_path);
34 use File::Path;
35
36 use Amanda::Paths;
37 use Amanda::Header qw( :constants );
38 use Amanda::Debug;
39 use Amanda::Holding;
40 use Amanda::Util;
41
42 # put the debug messages somewhere
43 Amanda::Debug::dbopen("installcheck");
44 Installcheck::log_test_output();
45
46 my $test_hdir = "$Installcheck::TMP/chunker-holding";
47 my $test_hfile = "$test_hdir/holder";
48 my $chunker_stderr_file = "$Installcheck::TMP/chunker-stderr";
49 my $debug = !exists $ENV{'HARNESS_ACTIVE'};
50
51 # information on the current run
52 my ($datestamp, $handle);
53 my ($chunker_pid, $chunker_in, $chunker_out, $last_chunker_reply, $chunker_reply_timeout);
54 my $writer_pid;
55
56 sub run_chunker {
57     my ($description, %params) = @_;
58
59     cleanup_chunker();
60
61     diag("******** $description") if $debug;
62
63     my $testconf = Installcheck::Run::setup();
64     $testconf->add_param('debug_chunker', 9);
65     $testconf->write();
66
67     if (exists $params{'ENOSPC_at'}) {
68         diag("setting CHUNKER_FAKE_ENOSPC_AT=$params{ENOSPC_at}") if $debug;
69         $ENV{'CHUNKER_FAKE_ENOSPC_AT'} = $params{'ENOSPC_at'};
70     } else {
71         delete $ENV{'CHUNKER_FAKE_ENOSPC'};
72     }
73
74     open(CHUNKER_ERR, ">", $chunker_stderr_file);
75     $chunker_in = $chunker_out = '';
76     $chunker_pid = open3($chunker_in, $chunker_out, ">&CHUNKER_ERR",
77         "$amlibexecdir/chunker", "TESTCONF");
78     close CHUNKER_ERR;
79     $chunker_in->blocking(1);
80     $chunker_out->autoflush();
81
82     pass("spawned new chunker for 'test $description'");
83
84     # define this to get the installcheck to wait and allow you to attach
85     # a gdb instance to the chunker
86     if ($params{'use_gdb'}) {
87         $chunker_reply_timeout = 0; # no timeouts while debugging
88         diag("attach debugger to pid $chunker_pid and press ENTER");
89         <>;
90     } else {
91         $chunker_reply_timeout = 120;
92     }
93
94     chunker_cmd("START $datestamp");
95 }
96
97 sub wait_for_exit {
98     if ($chunker_pid) {
99         waitpid($chunker_pid, 0);
100         $chunker_pid = undef;
101     }
102 }
103
104 sub cleanup_chunker {
105     -d $test_hdir and rmtree($test_hdir);
106     mkpath($test_hdir);
107
108     # make a small effort to collect zombies
109     if ($chunker_pid) {
110         if (waitpid($chunker_pid, WNOHANG) == $chunker_pid) {
111             $chunker_pid = undef;
112         }
113     }
114     if ($writer_pid) {
115         if (waitpid($writer_pid, WNOHANG) == $writer_pid) {
116             $writer_pid = undef;
117         }
118     }
119 }
120
121 sub wait_for_writer {
122     if ($writer_pid) {
123         if (waitpid($writer_pid, 0) == $writer_pid) {
124             $writer_pid = undef;
125         }
126     }
127 }
128
129 sub chunker_cmd {
130     my ($cmd) = @_;
131
132     diag(">>> $cmd") if $debug;
133     print $chunker_in "$cmd\n";
134 }
135
136 sub chunker_reply {
137     local $SIG{ALRM} = sub { die "Timeout while waiting for reply\n" };
138     alarm($chunker_reply_timeout);
139     $last_chunker_reply = $chunker_out->getline();
140     alarm(0);
141
142     if (!$last_chunker_reply) {
143         die("wrong pid") unless ($chunker_pid == waitpid($chunker_pid, 0));
144         my $exit_status = $?;
145
146         open(my $fh, "<", $chunker_stderr_file) or die("open $chunker_stderr_file: $!");
147         my $stderr = do { local $/; <$fh> };
148         close($fh);
149
150         diag("chunker stderr:\n$stderr") if $stderr;
151         die("chunker (pid $chunker_pid) died unexpectedly with status $exit_status");
152     }
153
154     # trim trailing whitespace -- C chunker outputs an extra ' ' after
155     # single-word replies
156     $last_chunker_reply =~ s/\s*$//;
157     diag("<<< $last_chunker_reply") if $debug;
158
159     return $last_chunker_reply;
160 }
161
162 sub check_logs {
163     my ($expected, $msg) = @_;
164     my $re;
165     my $line;
166
167     # must contain a pid line at the beginning and end
168     unshift @$expected, qr/^INFO chunker chunker pid \d+$/;
169     push @$expected, qr/^INFO chunker pid-done \d+$/;
170
171     open(my $logfile, "<", "$CONFIG_DIR/TESTCONF/log/log")
172         or die("opening log: $!");
173     my @logfile = grep(/^\S+ chunker /, <$logfile>);
174     close($logfile);
175
176     while (@logfile and @$expected) {
177         my $logline = shift @logfile;
178         my $expline = shift @$expected;
179         chomp $logline;
180         if ($logline !~ $expline) {
181             like($logline, $expline, $msg);
182             return;
183         }
184     }
185     if (@logfile) {
186         fail("$msg (extra trailing log lines)");
187         return;
188     }
189     if (@$expected) {
190         fail("$msg (logfile ends early)");
191         diag("first missing line should match ");
192         diag("".$expected->[0]);
193         return;
194     }
195
196     pass($msg);
197 }
198
199 sub check_holding_chunks {
200     my ($filename, $chunks, $host, $disk, $datestamp, $level) = @_;
201
202     my $msg = ".tmp holding chunk files";
203     my $exp_nchunks = @$chunks;
204     my $nchunks = 0;
205     while ($filename) {
206         $nchunks++;
207
208         my $filename_tmp = "$filename.tmp";
209         if (!-f $filename_tmp) {
210             fail($msg);
211             diag("file $filename_tmp doesn't exist");
212             diag(`ls -1l $test_hdir`);
213             return 0;
214         }
215
216         my $fh;
217         open($fh, "<", $filename_tmp) or die("opening $filename_tmp: $!");
218         my $hdr_str = Amanda::Util::full_read(fileno($fh), Amanda::Holding::DISK_BLOCK_BYTES);
219         close($fh);
220
221         my $hdr = Amanda::Header->from_string($hdr_str);
222         my $exp_type = ($nchunks == 1)? $F_DUMPFILE : $F_CONT_DUMPFILE;
223         if ($hdr->{'type'} != $exp_type) {
224             my ($exp, $got) = (Amanda::Header::filetype_t_to_string($exp_type),
225                                Amanda::Header::filetype_t_to_string($hdr->{'type'}));
226             fail($msg);
227             diag("file $filename_tmp has header type $got; expected $exp");
228             return 0;
229         }
230
231         my $ok = 1;
232         $ok &&= $hdr->{'name'} eq $host;
233         $ok &&= $hdr->{'disk'} eq $disk;
234         $ok &&= $hdr->{'datestamp'} eq $datestamp;
235         $ok &&= $hdr->{'dumplevel'} eq $level;
236         if (!$ok) {
237             fail($msg);
238             diag("file $filename_tmp header has unexpected values:\n" . $hdr->summary());
239             return 0;
240         }
241
242         my $data_size = (stat($filename_tmp))[7] - Amanda::Holding::DISK_BLOCK_BYTES;
243         my $exp_size = (shift @$chunks) * 1024;
244         if (defined $exp_size and $exp_size != $data_size) {
245             fail($msg);
246             diag("file $filename_tmp: expected $exp_size bytes, got $data_size");
247             return 0;
248         } # note: if @$exp_chunks is empty, the final is() will catch it
249
250         my $last_filename = $filename;
251         $filename = $hdr->{'cont_filename'};
252         die("header loop!") if $last_filename eq $filename;
253     }
254
255     return is($nchunks, $exp_nchunks, $msg);
256 }
257
258 sub cleanup_log {
259     my $logfile = "$CONFIG_DIR/TESTCONF/log/log";
260     -f $logfile and unlink($logfile);
261 }
262
263 # functions to create dumpfiles
264
265 sub write_dumpfile_header_to {
266     my ($fh, $size, $hostname, $disk, $expect_failure) = @_;
267
268     my $hdr = Amanda::Header->new();
269     $hdr->{'type'} = $Amanda::Header::F_DUMPFILE;
270     $hdr->{'datestamp'} = $datestamp;
271     $hdr->{'dumplevel'} = 0;
272     $hdr->{'compressed'} = 0;
273     $hdr->{'comp_suffix'} = ".foo";
274     $hdr->{'name'} = $hostname;
275     $hdr->{'disk'} = $disk;
276     $hdr->{'program'} = "INSTALLCHECK";
277     $hdr = $hdr->to_string(Amanda::Holding::DISK_BLOCK_BYTES,
278                            Amanda::Holding::DISK_BLOCK_BYTES);
279
280     $fh->write($hdr);
281 }
282
283 sub write_dumpfile_data_to {
284     my ($fh, $size, $hostname, $disk, $expect_failure) = @_;
285
286     my $bytes_to_write = $size;
287     my $bufbase = substr((('='x127)."\n".('-'x127)."\n") x 4, 8, -3) . "1K\n";
288     die length($bufbase) unless length($bufbase) == 1024-8;
289     my $k = 0;
290     while ($bytes_to_write > 0) {
291         my $buf = sprintf("%08x", $k++).$bufbase;
292         my $written = $fh->syswrite($buf, $bytes_to_write);
293         if (!defined($written)) {
294             die "writing: $!" unless $expect_failure;
295             exit;
296         }
297         $bytes_to_write -= $written;
298     }
299 }
300
301 # connect to the given port and write a dumpfile; this *will* create
302 # zombies, but it's OK -- installchecks aren't daemons.
303 sub write_to_port {
304     my ($port_cmd, $size, $hostname, $disk, $expect_error) = @_;
305
306     my ($header_port, $data_addr) =
307         ($last_chunker_reply =~ /^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+)/);
308
309     # just run this in the child
310     $writer_pid = fork();
311     return unless $writer_pid == 0;
312
313     my $sock = IO::Socket::INET->new(
314         PeerAddr => "127.0.0.1:$header_port",
315         Proto => "tcp",
316         ReuseAddr => 1,
317     );
318
319     write_dumpfile_header_to($sock, $size, $hostname, $disk, $expect_error);
320     close $sock;
321
322     $sock = IO::Socket::INET->new(
323         PeerAddr => $data_addr,
324         Proto => "tcp",
325         ReuseAddr => 1,
326     );
327
328     write_dumpfile_data_to($sock, $size, $hostname, $disk, $expect_error);
329     exit;
330 }
331
332 ########
333
334 ##
335 # A simple, two-chunk PORT-WRITE
336
337 $handle = "11-11111";
338 $datestamp = "20070102030405";
339 run_chunker("simple");
340 # note that features (ffff here) and options (ops) are ignored by the chunker
341 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /boot 0 $datestamp 512 INSTALLCHECK 10240 ops");
342 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
343         "got PORT with data address");
344 write_to_port($last_chunker_reply, 700*1024, "ghost", "/boot", 0);
345 wait_for_writer();
346 chunker_cmd("DONE $handle");
347 like(chunker_reply, qr/^DONE $handle 700 "\[sec [\d.]+ kb 700 kps [\d.]+\]"$/,
348         "got DONE") or die;
349 wait_for_exit();
350
351 check_logs([
352     qr(^SUCCESS chunker ghost /boot $datestamp 0 \[sec [\d.]+ kb 700 kps [\d.]+\]$),
353 ], "logs correct");
354
355 check_holding_chunks($test_hfile, [ 480, 220 ], "ghost", "/boot", $datestamp, 0);
356
357 ##
358 # A two-chunk PORT-WRITE that the dumper flags as a failure, but chunker as PARTIAL
359
360 $handle = "22-11111";
361 $datestamp = "20080808080808";
362 run_chunker("partial");
363 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /root 0 $datestamp 512 INSTALLCHECK 10240 ops");
364 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
365         "got PORT with data address");
366 write_to_port($last_chunker_reply, 768*1024, "ghost", "/root", 0);
367 wait_for_writer();
368 chunker_cmd("FAILED $handle");
369 like(chunker_reply, qr/^PARTIAL $handle 768 "\[sec [\d.]+ kb 768 kps [\d.]+\]"$/,
370         "got PARTIAL") or die;
371 wait_for_exit();
372
373 check_logs([
374     qr(^PARTIAL chunker ghost /root $datestamp 0 \[sec [\d.]+ kb 768 kps [\d.]+\]$),
375 ], "logs correct");
376
377 check_holding_chunks($test_hfile, [ 480, 288 ], "ghost", "/root", $datestamp, 0);
378
379 ##
380 # A two-chunk PORT-WRITE that the dumper flags as a failure and chunker
381 # does too, since no appreciatble bytes were transferred
382
383 $handle = "33-11111";
384 $datestamp = "20070202020202";
385 run_chunker("failed");
386 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /usr 0 $datestamp 512 INSTALLCHECK 10240 ops");
387 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
388         "got PORT with data address");
389 write_to_port($last_chunker_reply, 0, "ghost", "/usr", 0);
390 wait_for_writer();
391 chunker_cmd("FAILED $handle");
392 like(chunker_reply, qr/^FAILED $handle "\[dumper returned FAILED\]"$/,
393         "got FAILED") or die;
394 wait_for_exit();
395
396 check_logs([
397     qr(^FAIL chunker ghost /usr $datestamp 0 \[dumper returned FAILED\]$),
398 ], "logs correct");
399
400 check_holding_chunks($test_hfile, [ 0 ], "ghost", "/usr", $datestamp, 0);
401
402 cleanup_chunker();
403
404 ##
405 # A PORT-WRITE with a USE value smaller than the dump size, but an overly large
406 # chunksize
407
408 $handle = "44-11111";
409 $datestamp = "20040404040404";
410 run_chunker("more-than-use");
411 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /var 0 $datestamp 10240 INSTALLCHECK 512 ops");
412 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
413         "got PORT with data address");
414 write_to_port($last_chunker_reply, 700*1024, "ghost", "/var", 1);
415 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
416         "got RQ-MORE-DISK") or die;
417 chunker_cmd("CONTINUE $handle $test_hfile-u2 10240 512");
418 wait_for_writer();
419 chunker_cmd("DONE $handle");
420 like(chunker_reply, qr/^DONE $handle 700 "\[sec [\d.]+ kb 700 kps [\d.]+\]"$/,
421         "got DONE") or die;
422 wait_for_exit();
423
424 check_logs([
425     qr(^SUCCESS chunker ghost /var $datestamp 0 \[sec [\d.]+ kb 700 kps [\d.]+\]$),
426 ], "logs correct");
427
428 check_holding_chunks($test_hfile, [ 480, 220 ], "ghost", "/var", $datestamp, 0);
429
430 ##
431 # A PORT-WRITE with a USE value smaller than the dump size, and an even smaller
432 # chunksize, with a different chunksize on the second holding disk
433
434 $handle = "55-11111";
435 $datestamp = "20050505050505";
436 run_chunker("more-than-use-and-chunks");
437 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /var 0 $datestamp 96 INSTALLCHECK 160 ops");
438 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
439         "got PORT with data address");
440 write_to_port($last_chunker_reply, 400*1024, "ghost", "/var", 1);
441 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
442         "got RQ-MORE-DISK") or die;
443 chunker_cmd("CONTINUE $handle $test_hfile-u2 128 10240");
444 wait_for_writer();
445 chunker_cmd("DONE $handle");
446 like(chunker_reply, qr/^DONE $handle 400 "\[sec [\d.]+ kb 400 kps [\d.]+\]"$/,
447         "got DONE") or die;
448 wait_for_exit();
449
450 check_logs([
451     qr(^SUCCESS chunker ghost /var $datestamp 0 \[sec [\d.]+ kb 400 kps [\d.]+\]$),
452 ], "logs correct");
453
454 check_holding_chunks($test_hfile, [ 64, 32, 96, 96, 96, 16 ],
455     "ghost", "/var", $datestamp, 0);
456
457 cleanup_chunker();
458
459 ##
460 # A PORT-WRITE with a USE value smaller than the dump size, but with the CONTINUE
461 # giving the same filename, so that the dump continues in the same file
462
463 $handle = "55-22222";
464 $datestamp = "20050505050505";
465 run_chunker("use, continue on same file");
466 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /var/lib 0 $datestamp 10240 INSTALLCHECK 64 ops");
467 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
468         "got PORT with data address");
469 write_to_port($last_chunker_reply, 70*1024, "ghost", "/var/lib", 1);
470 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
471         "got RQ-MORE-DISK") or die;
472 chunker_cmd("CONTINUE $handle $test_hfile 10240 10240");
473 wait_for_writer();
474 chunker_cmd("DONE $handle");
475 like(chunker_reply, qr/^DONE $handle 70 "\[sec [\d.]+ kb 70 kps [\d.]+\]"$/,
476         "got DONE") or die;
477 wait_for_exit();
478
479 check_logs([
480     qr(^SUCCESS chunker ghost /var/lib $datestamp 0 \[sec [\d.]+ kb 70 kps [\d.]+\]$),
481 ], "logs correct");
482
483 check_holding_chunks($test_hfile, [ 70 ],
484     "ghost", "/var/lib", $datestamp, 0);
485
486 cleanup_chunker();
487
488 ##
489 # A PORT-WRITE with a USE value that will trigger in the midst of a header
490 # on the second chunk
491
492 $handle = "66-11111";
493 $datestamp = "20060606060606";
494 run_chunker("out-of-use-during-header");
495 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" ghost ffff /u01 0 $datestamp 96 INSTALLCHECK 120 ops");
496 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
497         "got PORT with data address");
498 write_to_port($last_chunker_reply, 400*1024, "ghost", "/u01", 1);
499 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
500         "got RQ-MORE-DISK") or die;
501 chunker_cmd("CONTINUE $handle $test_hfile-u2 128 10240");
502 wait_for_writer();
503 chunker_cmd("DONE $handle");
504 like(chunker_reply, qr/^DONE $handle 400 "\[sec [\d.]+ kb 400 kps [\d.]+\]"$/,
505         "got DONE") or die;
506 wait_for_exit();
507
508 check_logs([
509     qr(^SUCCESS chunker ghost /u01 $datestamp 0 \[sec [\d.]+ kb 400 kps [\d.]+\]$),
510 ], "logs for more-than-use-and-chunks PORT-WRITE");
511
512 check_holding_chunks($test_hfile, [ 64, 96, 96, 96, 48 ],
513     "ghost", "/u01", $datestamp, 0);
514
515 ##
516 # A two-disk PORT-WRITE, but with the DONE sent before the first byte of data
517 # arrives, to test the ability of the chunker to defer the DONE until it gets
518 # an EOF
519
520 $handle = "77-11111";
521 $datestamp = "20070707070707";
522 run_chunker("early-DONE");
523 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" roast ffff /boot 0 $datestamp 10240 INSTALLCHECK 128 ops");
524 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
525         "got PORT with data address");
526 chunker_cmd("DONE $handle");
527 write_to_port($last_chunker_reply, 180*1024, "roast", "/boot", 0);
528 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
529         "got RQ-MORE-DISK") or die;
530 chunker_cmd("CONTINUE $handle $test_hfile-u2 10240 10240");
531 wait_for_writer();
532 like(chunker_reply, qr/^DONE $handle 180 "\[sec [\d.]+ kb 180 kps [\d.]+\]"$/,
533         "got DONE") or die;
534 wait_for_exit();
535
536 check_logs([
537     qr(^SUCCESS chunker roast /boot $datestamp 0 \[sec [\d.]+ kb 180 kps [\d.]+\]$),
538 ], "logs for simple PORT-WRITE");
539
540 check_holding_chunks($test_hfile, [ 96, 84 ], "roast", "/boot", $datestamp, 0);
541
542 ##
543 # A two-disk PORT-WRITE, where the first disk runs out of space before it hits
544 # the USE limit.
545
546 $handle = "88-11111";
547 $datestamp = "20080808080808";
548 run_chunker("ENOSPC-1", ENOSPC_at => 90*1024);
549 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" roast ffff /boot 0 $datestamp 10240 INSTALLCHECK 10240 ops");
550 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
551         "got PORT with data address");
552 write_to_port($last_chunker_reply, 100*1024, "roast", "/boot", 0);
553 like(chunker_reply, qr/^NO-ROOM $handle 10150$/, # == 10240-90
554         "got NO-ROOM") or die;
555 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
556         "got RQ-MORE-DISK") or die;
557 chunker_cmd("CONTINUE $handle $test_hfile-u2 10240 10240");
558 wait_for_writer();
559 chunker_cmd("DONE $handle");
560 like(chunker_reply, qr/^DONE $handle 100 "\[sec [\d.]+ kb 100 kps [\d.]+\]"$/,
561         "got DONE") or die;
562 wait_for_exit();
563
564 check_logs([
565     qr(^SUCCESS chunker roast /boot $datestamp 0 \[sec [\d.]+ kb 100 kps [\d.]+\]$),
566 ], "logs for simple PORT-WRITE");
567
568 check_holding_chunks($test_hfile, [ 58, 42 ], "roast", "/boot", $datestamp, 0);
569
570 ##
571 # A two-chunk PORT-WRITE, where the second chunk gets ENOSPC in the header.  This
572 # also checks the behavior of rounding down the use value to the nearest multiple
573 # of 32k (with am_floor)
574
575 $handle = "88-22222";
576 $datestamp = "20080808080808";
577 run_chunker("ENOSPC-2", ENOSPC_at => 130*1024);
578 chunker_cmd("PORT-WRITE $handle \"$test_hfile\" roast ffff /boot 0 $datestamp 128 INSTALLCHECK 1000 ops");
579 like(chunker_reply, qr/^PORT (\d+) "?(\d+\.\d+\.\d+\.\d+:\d+;?)+"?$/,
580         "got PORT with data address");
581 write_to_port($last_chunker_reply, 128*1024, "roast", "/boot", 0);
582 like(chunker_reply, qr/^NO-ROOM $handle 864$/, # == am_floor(1000)-128
583         "got NO-ROOM") or die;
584 like(chunker_reply, qr/^RQ-MORE-DISK $handle$/,
585         "got RQ-MORE-DISK") or die;
586 chunker_cmd("CONTINUE $handle $test_hfile-u2 300 128");
587 wait_for_writer();
588 chunker_cmd("DONE $handle");
589 like(chunker_reply, qr/^DONE $handle 128 "\[sec [\d.]+ kb 128 kps [\d.]+\]"$/,
590         "got DONE") or die;
591 wait_for_exit();
592
593 check_logs([
594     qr(^SUCCESS chunker roast /boot $datestamp 0 \[sec [\d.]+ kb 128 kps [\d.]+\]$),
595 ], "logs for simple PORT-WRITE");
596
597 check_holding_chunks($test_hfile, [ 96, 32 ], "roast", "/boot", $datestamp, 0);
598 ok(!-f "$test_hfile.1.tmp",
599     "half-written header is deleted");
600
601 cleanup_chunker();