1#! /somewhere/perl -w 2 3use strict; 4use Machine; 5use Term::ReadLine; 6use IO::File; 7use IO::Pty; 8use Logger; 9use Cwd; 10use POSIX qw(_exit dup2); 11 12$SIG{PIPE} = 'IGNORE'; # because Unix domain sockets may die unexpectedly 13 14STDERR->autoflush(1); 15 16my $log = new Logger; 17 18 19# Start vde_switch for each network required by the test. 20my %vlans; 21foreach my $vlan (split / /, $ENV{VLANS} || "") { 22 next if defined $vlans{$vlan}; 23 # Start vde_switch as a child process. We don't run it in daemon 24 # mode because we want the child process to be cleaned up when we 25 # die. Since we have to make sure that the control socket is 26 # ready, we send a dummy command to vde_switch (via stdin) and 27 # wait for a reply. Note that vde_switch requires stdin to be a 28 # TTY, so we create one. 29 $log->log("starting VDE switch for network $vlan"); 30 my $socket = Cwd::abs_path "./vde$vlan.ctl"; 31 my $pty = new IO::Pty; 32 my ($stdoutR, $stdoutW); pipe $stdoutR, $stdoutW; 33 my $pid = fork(); die "cannot fork" unless defined $pid; 34 if ($pid == 0) { 35 dup2(fileno($pty->slave), 0); 36 dup2(fileno($stdoutW), 1); 37 exec "vde_switch -s $socket" or _exit(1); 38 } 39 close $stdoutW; 40 print $pty "version\n"; 41 readline $stdoutR or die "cannot start vde_switch"; 42 $ENV{"QEMU_VDE_SOCKET_$vlan"} = $socket; 43 $vlans{$vlan} = $pty; 44 die unless -e "$socket/ctl"; 45} 46 47 48my %vms; 49my $context = ""; 50 51sub createMachine { 52 my ($args) = @_; 53 my $vm = Machine->new({%{$args}, log => $log, redirectSerial => ($ENV{USE_SERIAL} // "0") ne "1"}); 54 $vms{$vm->name} = $vm; 55 $context .= "my \$" . $vm->name . " = \$vms{'" . $vm->name . "'}; "; 56 return $vm; 57} 58 59foreach my $vmScript (@ARGV) { 60 my $vm = createMachine({startCommand => $vmScript}); 61} 62 63 64sub startAll { 65 $log->nest("starting all VMs", sub { 66 $_->start foreach values %vms; 67 }); 68} 69 70 71# Wait until all VMs have terminated. 72sub joinAll { 73 $log->nest("waiting for all VMs to finish", sub { 74 $_->waitForShutdown foreach values %vms; 75 }); 76} 77 78 79# In interactive tests, this allows the non-interactive test script to 80# be executed conveniently. 81sub testScript { 82 eval "$context $ENV{testScript};\n"; 83 warn $@ if $@; 84} 85 86 87my $nrTests = 0; 88my $nrSucceeded = 0; 89 90 91sub subtest { 92 my ($name, $coderef) = @_; 93 $log->nest("subtest: $name", sub { 94 $nrTests++; 95 eval { &$coderef }; 96 if ($@) { 97 $log->log("error: $@", { error => 1 }); 98 } else { 99 $nrSucceeded++; 100 } 101 }); 102} 103 104 105sub runTests { 106 if (defined $ENV{tests}) { 107 $log->nest("running the VM test script", sub { 108 eval "$context $ENV{tests}"; 109 if ($@) { 110 $log->log("error: $@", { error => 1 }); 111 die $@; 112 } 113 }, { expanded => 1 }); 114 } else { 115 my $term = Term::ReadLine->new('nixos-vm-test'); 116 $term->ReadHistory; 117 while (defined ($_ = $term->readline("> "))) { 118 eval "$context $_\n"; 119 warn $@ if $@; 120 } 121 $term->WriteHistory; 122 } 123 124 # Copy the kernel coverage data for each machine, if the kernel 125 # has been compiled with coverage instrumentation. 126 $log->nest("collecting coverage data", sub { 127 foreach my $vm (values %vms) { 128 my $gcovDir = "/sys/kernel/debug/gcov"; 129 130 next unless $vm->isUp(); 131 132 my ($status, $out) = $vm->execute("test -e $gcovDir"); 133 next if $status != 0; 134 135 # Figure out where to put the *.gcda files so that the 136 # report generator can find the corresponding kernel 137 # sources. 138 my $kernelDir = $vm->mustSucceed("echo \$(dirname \$(readlink -f /run/current-system/kernel))/.build/linux-*"); 139 chomp $kernelDir; 140 my $coverageDir = "/tmp/xchg/coverage-data/$kernelDir"; 141 142 # Copy all the *.gcda files. 143 $vm->execute("for d in $gcovDir/nix/store/*/.build/linux-*; do for i in \$(cd \$d && find -name '*.gcda'); do echo \$i; mkdir -p $coverageDir/\$(dirname \$i); cp -v \$d/\$i $coverageDir/\$i; done; done"); 144 } 145 }); 146 147 $log->nest("syncing", sub { 148 foreach my $vm (values %vms) { 149 next unless $vm->isUp(); 150 $vm->execute("sync"); 151 } 152 }); 153 154 if ($nrTests != 0) { 155 $log->log("$nrSucceeded out of $nrTests tests succeeded", 156 ($nrSucceeded < $nrTests ? { error => 1 } : { })); 157 } 158} 159 160 161# Create an empty raw virtual disk with the given name and size (in 162# MiB). 163sub createDisk { 164 my ($name, $size) = @_; 165 system("qemu-img create -f raw $name ${size}M") == 0 166 or die "cannot create image of size $size"; 167} 168 169 170END { 171 $log->nest("cleaning up", sub { 172 foreach my $vm (values %vms) { 173 if ($vm->{pid}) { 174 $log->log("killing " . $vm->{name} . " (pid " . $vm->{pid} . ")"); 175 kill 9, $vm->{pid}; 176 } 177 } 178 }); 179 $log->close(); 180} 181 182 183runTests; 184 185exit ($nrSucceeded < $nrTests ? 1 : 0);