#! /somewhere/perl -w

use strict;
use Machine;
use Term::ReadLine;
use IO::File;
use IO::Pty;
use Logger;
use Cwd;
use POSIX qw(_exit dup2);
use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);

$SIG{PIPE} = 'IGNORE'; # because Unix domain sockets may die unexpectedly

STDERR->autoflush(1);

my $log = new Logger;


# Start vde_switch for each network required by the test.
my %vlans;
foreach my $vlan (split / /, $ENV{VLANS} || "") {
    next if defined $vlans{$vlan};
    # Start vde_switch as a child process.  We don't run it in daemon
    # mode because we want the child process to be cleaned up when we
    # die.  Since we have to make sure that the control socket is
    # ready, we send a dummy command to vde_switch (via stdin) and
    # wait for a reply.  Note that vde_switch requires stdin to be a
    # TTY, so we create one.
    $log->log("starting VDE switch for network $vlan");
    my $socket = Cwd::abs_path "./vde$vlan.ctl";
    my $pty = new IO::Pty;
    my ($stdoutR, $stdoutW); pipe $stdoutR, $stdoutW;
    my $pid = fork(); die "cannot fork" unless defined $pid;
    if ($pid == 0) {
        dup2(fileno($pty->slave), 0);
        dup2(fileno($stdoutW), 1);
        exec "vde_switch -s $socket --dirmode 0700" or _exit(1);
    }
    close $stdoutW;
    print $pty "version\n";
    readline $stdoutR or die "cannot start vde_switch";
    $ENV{"QEMU_VDE_SOCKET_$vlan"} = $socket;
    $vlans{$vlan} = $pty;
    die unless -e "$socket/ctl";
}


my %vms;
my $context = "";

sub createMachine {
    my ($args) = @_;
    my $vm = Machine->new({%{$args}, log => $log, redirectSerial => ($ENV{USE_SERIAL} // "0") ne "1"});
    $vms{$vm->name} = $vm;
    $context .= "my \$" . $vm->name . " = \$vms{'" . $vm->name . "'}; ";
    return $vm;
}

foreach my $vmScript (@ARGV) {
    my $vm = createMachine({startCommand => $vmScript});
}


sub startAll {
    $log->nest("starting all VMs", sub {
        $_->start foreach values %vms;
    });
}


# Wait until all VMs have terminated.
sub joinAll {
    $log->nest("waiting for all VMs to finish", sub {
        $_->waitForShutdown foreach values %vms;
    });
}


# In interactive tests, this allows the non-interactive test script to
# be executed conveniently.
sub testScript {
    eval "$context $ENV{testScript};\n";
    warn $@ if $@;
}


my $nrTests = 0;
my $nrSucceeded = 0;


sub subtest {
    my ($name, $coderef) = @_;
    $log->nest("subtest: $name", sub {
        $nrTests++;
        eval { &$coderef };
        if ($@) {
            $log->log("error: $@", { error => 1 });
        } else {
            $nrSucceeded++;
        }
    });
}


sub runTests {
    if (defined $ENV{tests}) {
        $log->nest("running the VM test script", sub {
            eval "$context $ENV{tests}";
            if ($@) {
                $log->log("error: $@", { error => 1 });
                die $@;
            }
        }, { expanded => 1 });
    } else {
        my $term = Term::ReadLine->new('nixos-vm-test');
        $term->ReadHistory;
        while (defined ($_ = $term->readline("> "))) {
            eval "$context $_\n";
            warn $@ if $@;
        }
        $term->WriteHistory;
    }

    # Copy the kernel coverage data for each machine, if the kernel
    # has been compiled with coverage instrumentation.
    $log->nest("collecting coverage data", sub {
        foreach my $vm (values %vms) {
            my $gcovDir = "/sys/kernel/debug/gcov";

            next unless $vm->isUp();

            my ($status, $out) = $vm->execute("test -e $gcovDir");
            next if $status != 0;

            # Figure out where to put the *.gcda files so that the
            # report generator can find the corresponding kernel
            # sources.
            my $kernelDir = $vm->mustSucceed("echo \$(dirname \$(readlink -f /run/current-system/kernel))/.build/linux-*");
            chomp $kernelDir;
            my $coverageDir = "/tmp/xchg/coverage-data/$kernelDir";

            # Copy all the *.gcda files.
            $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");
        }
    });

    $log->nest("syncing", sub {
        foreach my $vm (values %vms) {
            next unless $vm->isUp();
            $vm->execute("sync");
        }
    });

    if ($nrTests != 0) {
        $log->log("$nrSucceeded out of $nrTests tests succeeded",
            ($nrSucceeded < $nrTests ? { error => 1 } : { }));
    }
}


# Create an empty raw virtual disk with the given name and size (in
# MiB).
sub createDisk {
    my ($name, $size) = @_;
    system("qemu-img create -f raw $name ${size}M") == 0
        or die "cannot create image of size $size";
}


END {
    $log->nest("cleaning up", sub {
        foreach my $vm (values %vms) {
            if ($vm->{pid}) {
                $log->log("killing " . $vm->{name} . " (pid " . $vm->{pid} . ")");
                kill 9, $vm->{pid};
            }
        }
    });
    $log->close();
}

my $now1 = clock_gettime(CLOCK_MONOTONIC);

runTests;

my $now2 = clock_gettime(CLOCK_MONOTONIC);

printf STDERR "test script finished in %.2fs\n", $now2 - $now1;

exit ($nrSucceeded < $nrTests ? 1 : 0);
