diff options
author | Eelco Dolstra <eelco.dolstra@logicblox.com> | 2013-10-10 13:28:20 +0200 |
---|---|---|
committer | Eelco Dolstra <eelco.dolstra@logicblox.com> | 2013-10-10 13:28:20 +0200 |
commit | 5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010 (patch) | |
tree | a6c0f605be6de3f372ae69905b331f9f75452da7 /nixos/lib | |
parent | 6070bc016bd2fd945b04347e25cfd3738622d2ac (diff) | |
download | nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.gz nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.bz2 nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.lz nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.xz nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.zst nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.zip |
Move all of NixOS to nixos/ in preparation of the repository merge
Diffstat (limited to 'nixos/lib')
-rw-r--r-- | nixos/lib/build-vms.nix | 87 | ||||
-rw-r--r-- | nixos/lib/channel-expr.nix | 6 | ||||
-rw-r--r-- | nixos/lib/eval-config.nix | 73 | ||||
-rw-r--r-- | nixos/lib/from-env.nix | 4 | ||||
-rw-r--r-- | nixos/lib/make-iso9660-image.nix | 60 | ||||
-rw-r--r-- | nixos/lib/make-iso9660-image.sh | 91 | ||||
-rw-r--r-- | nixos/lib/make-squashfs.nix | 30 | ||||
-rw-r--r-- | nixos/lib/make-system-tarball.nix | 38 | ||||
-rw-r--r-- | nixos/lib/make-system-tarball.sh | 58 | ||||
-rw-r--r-- | nixos/lib/qemu-flags.nix | 10 | ||||
-rw-r--r-- | nixos/lib/test-driver/Logger.pm | 70 | ||||
-rw-r--r-- | nixos/lib/test-driver/Machine.pm | 568 | ||||
-rw-r--r-- | nixos/lib/test-driver/log2html.xsl | 135 | ||||
-rw-r--r-- | nixos/lib/test-driver/logfile.css | 129 | ||||
-rw-r--r-- | nixos/lib/test-driver/test-driver.pl | 178 | ||||
-rw-r--r-- | nixos/lib/test-driver/treebits.js | 30 | ||||
-rw-r--r-- | nixos/lib/testing.nix | 244 | ||||
-rw-r--r-- | nixos/lib/utils.nix | 10 |
18 files changed, 1821 insertions, 0 deletions
diff --git a/nixos/lib/build-vms.nix b/nixos/lib/build-vms.nix new file mode 100644 index 000000000000..59f05bfd1043 --- /dev/null +++ b/nixos/lib/build-vms.nix @@ -0,0 +1,87 @@ +{ system, minimal ? false }: + +let pkgs = import <nixpkgs> { config = {}; inherit system; }; in + +with pkgs.lib; +with import ../lib/qemu-flags.nix; + +rec { + + inherit pkgs; + + + # Build a virtual network from an attribute set `{ machine1 = + # config1; ... machineN = configN; }', where `machineX' is the + # hostname and `configX' is a NixOS system configuration. Each + # machine is given an arbitrary IP address in the virtual network. + buildVirtualNetwork = + nodes: let nodesOut = mapAttrs (n: buildVM nodesOut) (assignIPAddresses nodes); in nodesOut; + + + buildVM = + nodes: configurations: + + import ./eval-config.nix { + inherit system; + modules = configurations ++ + [ ../modules/virtualisation/qemu-vm.nix + ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs + { key = "no-manual"; services.nixosManual.enable = false; } + ] ++ optional minimal ../modules/testing/minimal-kernel.nix; + extraArgs = { inherit nodes; }; + }; + + + # Given an attribute set { machine1 = config1; ... machineN = + # configN; }, sequentially assign IP addresses in the 192.168.1.0/24 + # range to each machine, and set the hostname to the attribute name. + assignIPAddresses = nodes: + + let + + machines = attrNames nodes; + + machinesNumbered = zipTwoLists machines (range 1 254); + + nodes_ = flip map machinesNumbered (m: nameValuePair m.first + [ ( { config, pkgs, nodes, ... }: + let + interfacesNumbered = zipTwoLists config.virtualisation.vlans (range 1 255); + interfaces = flip map interfacesNumbered ({ first, second }: + nameValuePair "eth${toString second}" + { ipAddress = "192.168.${toString first}.${toString m.second}"; + subnetMask = "255.255.255.0"; + }); + in + { key = "ip-address"; + config = + { networking.hostName = m.first; + + networking.interfaces = listToAttrs interfaces; + + networking.primaryIPAddress = + optionalString (interfaces != []) (head interfaces).value.ipAddress; + + # Put the IP addresses of all VMs in this machine's + # /etc/hosts file. If a machine has multiple + # interfaces, use the IP address corresponding to + # the first interface (i.e. the first network in its + # virtualisation.vlans option). + networking.extraHosts = flip concatMapStrings machines + (m: let config = (getAttr m nodes).config; in + optionalString (config.networking.primaryIPAddress != "") + ("${config.networking.primaryIPAddress} " + + "${config.networking.hostName}\n")); + + virtualisation.qemu.options = + flip map interfacesNumbered + ({ first, second }: qemuNICFlags second first m.second); + }; + } + ) + (getAttr m.first nodes) + ] ); + + in listToAttrs nodes_; + +} diff --git a/nixos/lib/channel-expr.nix b/nixos/lib/channel-expr.nix new file mode 100644 index 000000000000..453bdd506b88 --- /dev/null +++ b/nixos/lib/channel-expr.nix @@ -0,0 +1,6 @@ +{ system ? builtins.currentSystem }: + +{ pkgs = + (import nixpkgs/default.nix { inherit system; }) + // { recurseForDerivations = true; }; +} diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix new file mode 100644 index 000000000000..47e7d1a0eafa --- /dev/null +++ b/nixos/lib/eval-config.nix @@ -0,0 +1,73 @@ +# From an end-user configuration file (`configuration'), build a NixOS +# configuration object (`config') from which we can retrieve option +# values. + +{ system ? builtins.currentSystem +, pkgs ? null +, baseModules ? import ../modules/module-list.nix +, extraArgs ? {} +, modules +, nixpkgs ? <nixpkgs> +}: + +let extraArgs_ = extraArgs; pkgs_ = pkgs; system_ = system; in + +rec { + + # These are the NixOS modules that constitute the system configuration. + configComponents = modules ++ baseModules; + + # Merge the option definitions in all modules, forming the full + # system configuration. It's not checked for undeclared options. + systemModule = + pkgs.lib.fixMergeModules configComponents extraArgs; + + optionDefinitions = systemModule.config; + optionDeclarations = systemModule.options; + inherit (systemModule) options; + + # These are the extra arguments passed to every module. In + # particular, Nixpkgs is passed through the "pkgs" argument. + extraArgs = extraArgs_ // { + inherit pkgs modules baseModules; + modulesPath = ../modules; + pkgs_i686 = import nixpkgs { system = "i686-linux"; }; + utils = import ./utils.nix pkgs; + }; + + # Import Nixpkgs, allowing the NixOS option nixpkgs.config to + # specify the Nixpkgs configuration (e.g., to set package options + # such as firefox.enableGeckoMediaPlayer, or to apply global + # overrides such as changing GCC throughout the system), and the + # option nixpkgs.system to override the platform type. This is + # tricky, because we have to prevent an infinite recursion: "pkgs" + # is passed as an argument to NixOS modules, but the value of "pkgs" + # depends on config.nixpkgs.config, which we get from the modules. + # So we call ourselves here with "pkgs" explicitly set to an + # instance that doesn't depend on nixpkgs.config. + pkgs = + if pkgs_ != null + then pkgs_ + else import nixpkgs ( + let + system = if nixpkgsOptions.system != "" then nixpkgsOptions.system else system_; + nixpkgsOptions = (import ./eval-config.nix { + inherit system extraArgs modules; + # For efficiency, leave out most NixOS modules; they don't + # define nixpkgs.config, so it's pointless to evaluate them. + baseModules = [ ../modules/misc/nixpkgs.nix ]; + pkgs = import nixpkgs { system = system_; config = {}; }; + }).optionDefinitions.nixpkgs; + in + { + inherit system; + inherit (nixpkgsOptions) config; + }); + + # Optionally check wether all config values have corresponding + # option declarations. + config = + let doCheck = optionDefinitions.environment.checkConfigurationOptions; in + assert doCheck -> pkgs.lib.checkModule "" systemModule; + systemModule.config; +} diff --git a/nixos/lib/from-env.nix b/nixos/lib/from-env.nix new file mode 100644 index 000000000000..6bd71e40e9a1 --- /dev/null +++ b/nixos/lib/from-env.nix @@ -0,0 +1,4 @@ +# TODO: remove this file. There is lib.maybeEnv now +name: default: +let value = builtins.getEnv name; in +if value == "" then default else value diff --git a/nixos/lib/make-iso9660-image.nix b/nixos/lib/make-iso9660-image.nix new file mode 100644 index 000000000000..5ad546e9534d --- /dev/null +++ b/nixos/lib/make-iso9660-image.nix @@ -0,0 +1,60 @@ +{ stdenv, perl, cdrkit, pathsFromGraph + +, # The file name of the resulting ISO image. + isoName ? "cd.iso" + +, # The files and directories to be placed in the ISO file system. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. + contents + +, # In addition to `contents', the closure of the store paths listed + # in `packages' are also placed in the Nix store of the CD. This is + # a list of attribute sets {object, symlink} where `object' if a + # store path whose closure will be copied, and `symlink' is a + # symlink to `object' that will be added to the CD. + storeContents ? [] + +, # Whether this should be an El-Torito bootable CD. + bootable ? false + +, # Whether this should be an efi-bootable El-Torito CD. + efiBootable ? false + +, # The path (in the ISO file system) of the boot image. + bootImage ? "" + +, # The path (in the ISO file system) of the efi boot image. + efiBootImage ? "" + +, # Whether to compress the resulting ISO image with bzip2. + compressImage ? false + +, # The volume ID. + volumeID ? "" + +}: + +assert bootable -> bootImage != ""; +assert efiBootable -> efiBootImage != ""; + +stdenv.mkDerivation { + name = "iso9660-image"; + builder = ./make-iso9660-image.sh; + buildInputs = [perl cdrkit]; + + inherit isoName bootable bootImage compressImage volumeID pathsFromGraph efiBootImage efiBootable; + + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + # !!! should use XML. + objects = map (x: x.object) storeContents; + symlinks = map (x: x.symlink) storeContents; + + # For obtaining the closure of `storeContents'. + exportReferencesGraph = + map (x: [("closure-" + baseNameOf x.object) x.object]) storeContents; +} diff --git a/nixos/lib/make-iso9660-image.sh b/nixos/lib/make-iso9660-image.sh new file mode 100644 index 000000000000..89b681ed2cd5 --- /dev/null +++ b/nixos/lib/make-iso9660-image.sh @@ -0,0 +1,91 @@ +source $stdenv/setup + +sources_=($sources) +targets_=($targets) + +objects=($objects) +symlinks=($symlinks) + + +# Remove the initial slash from a path, since genisofs likes it that way. +stripSlash() { + res="$1" + if test "${res:0:1}" = /; then res=${res:1}; fi +} + +stripSlash "$bootImage"; bootImage="$res" + + +if test -n "$bootable"; then + + # The -boot-info-table option modifies the $bootImage file, so + # find it in `contents' and make a copy of it (since the original + # is read-only in the Nix store...). + for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + if test "$res" = "$bootImage"; then + echo "copying the boot image ${sources_[$i]}" + cp "${sources_[$i]}" boot.img + chmod u+w boot.img + sources_[$i]=boot.img + fi + done + + bootFlags="-b $bootImage -c .boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table" +fi + +if test -n "$efiBootable"; then + bootFlags="$bootFlags -eltorito-alt-boot -e $efiBootImage -no-emul-boot" +fi + +touch pathlist + + +# Add the individual files. +for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + echo "$res=${sources_[$i]}" >> pathlist +done + + +# Add the closures of the top-level store objects. +storePaths=$(perl $pathsFromGraph closure-*) +for i in $storePaths; do + echo "${i:1}=$i" >> pathlist +done + + +# Also include a manifest of the closures in a format suitable for +# nix-store --load-db. +if [ -n "$object" ]; then + printRegistration=1 perl $pathsFromGraph closure-* > nix-path-registration + echo "nix-path-registration=nix-path-registration" >> pathlist +fi + + +# Add symlinks to the top-level store objects. +for ((n = 0; n < ${#objects[*]}; n++)); do + object=${objects[$n]} + symlink=${symlinks[$n]} + if test "$symlink" != "none"; then + mkdir -p $(dirname ./$symlink) + ln -s $object ./$symlink + echo "$symlink=./$symlink" >> pathlist + fi +done + +# !!! what does this do? +cat pathlist | sed -e 's/=\(.*\)=\(.*\)=/\\=\1=\2\\=/' | tee pathlist.safer + + +ensureDir $out/iso +genCommand="genisoimage -iso-level 4 -r -J $bootFlags -hide-rr-moved -graft-points -path-list pathlist.safer ${volumeID:+-V $volumeID}" +if test -z "$compressImage"; then + $genCommand -o $out/iso/$isoName +else + $genCommand | bzip2 > $out/iso/$isoName.bz2 +fi + + +ensureDir $out/nix-support +echo $system > $out/nix-support/system diff --git a/nixos/lib/make-squashfs.nix b/nixos/lib/make-squashfs.nix new file mode 100644 index 000000000000..3b640334e17a --- /dev/null +++ b/nixos/lib/make-squashfs.nix @@ -0,0 +1,30 @@ +{ stdenv, squashfsTools, perl, pathsFromGraph + +, # The root directory of the squashfs filesystem is filled with the + # closures of the Nix store paths listed here. + storeContents ? [] +}: + +stdenv.mkDerivation { + name = "squashfs.img"; + + buildInputs = [perl squashfsTools]; + + # For obtaining the closure of `storeContents'. + exportReferencesGraph = + map (x: [("closure-" + baseNameOf x) x]) storeContents; + + buildCommand = + '' + # Add the closures of the top-level store objects. + storePaths=$(perl ${pathsFromGraph} closure-*) + + # Also include a manifest of the closures in a format suitable + # for nix-store --load-db. + printRegistration=1 perl ${pathsFromGraph} closure-* > nix-path-registration + + # Generate the squashfs image. + mksquashfs nix-path-registration $storePaths $out \ + -keep-as-directory -all-root + ''; +} diff --git a/nixos/lib/make-system-tarball.nix b/nixos/lib/make-system-tarball.nix new file mode 100644 index 000000000000..8fed9a348827 --- /dev/null +++ b/nixos/lib/make-system-tarball.nix @@ -0,0 +1,38 @@ +{ stdenv, perl, xz, pathsFromGraph + +, # The file name of the resulting tarball + fileName ? "nixos-system-${stdenv.system}" + +, # The files and directories to be placed in the tarball. + # This is a list of attribute sets {source, target} where `source' + # is the file system object (regular file or directory) to be + # grafted in the file system at path `target'. + contents + +, # In addition to `contents', the closure of the store paths listed + # in `packages' are also placed in the Nix store of the tarball. This is + # a list of attribute sets {object, symlink} where `object' if a + # store path whose closure will be copied, and `symlink' is a + # symlink to `object' that will be added to the tarball. + storeContents ? [] +}: + +stdenv.mkDerivation { + name = "tarball"; + builder = ./make-system-tarball.sh; + buildInputs = [perl xz]; + + inherit fileName pathsFromGraph; + + # !!! should use XML. + sources = map (x: x.source) contents; + targets = map (x: x.target) contents; + + # !!! should use XML. + objects = map (x: x.object) storeContents; + symlinks = map (x: x.symlink) storeContents; + + # For obtaining the closure of `storeContents'. + exportReferencesGraph = + map (x: [("closure-" + baseNameOf x.object) x.object]) storeContents; +} diff --git a/nixos/lib/make-system-tarball.sh b/nixos/lib/make-system-tarball.sh new file mode 100644 index 000000000000..aadd0f6428c8 --- /dev/null +++ b/nixos/lib/make-system-tarball.sh @@ -0,0 +1,58 @@ +source $stdenv/setup +set -x + +sources_=($sources) +targets_=($targets) + +echo $objects +objects=($objects) +symlinks=($symlinks) + + +# Remove the initial slash from a path, since genisofs likes it that way. +stripSlash() { + res="$1" + if test "${res:0:1}" = /; then res=${res:1}; fi +} + +touch pathlist + +# Add the individual files. +for ((i = 0; i < ${#targets_[@]}; i++)); do + stripSlash "${targets_[$i]}" + mkdir -p "$(dirname "$res")" + cp -a "${sources_[$i]}" "$res" +done + + +# Add the closures of the top-level store objects. +mkdir -p nix/store +storePaths=$(perl $pathsFromGraph closure-*) +for i in $storePaths; do + cp -a "$i" "${i:1}" +done + + +# TODO tar ruxo +# Also include a manifest of the closures in a format suitable for +# nix-store --load-db. +printRegistration=1 perl $pathsFromGraph closure-* > nix-path-registration + +# Add symlinks to the top-level store objects. +for ((n = 0; n < ${#objects[*]}; n++)); do + object=${objects[$n]} + symlink=${symlinks[$n]} + if test "$symlink" != "none"; then + mkdir -p $(dirname ./$symlink) + ln -s $object ./$symlink + fi +done + +ensureDir $out/tarball + +tar cvJf $out/tarball/$fileName.tar.xz * + +ensureDir $out/nix-support +echo $system > $out/nix-support/system +echo "file system-tarball $out/tarball/$fileName.tar.xz" > $out/nix-support/hydra-build-products + diff --git a/nixos/lib/qemu-flags.nix b/nixos/lib/qemu-flags.nix new file mode 100644 index 000000000000..de355b08918c --- /dev/null +++ b/nixos/lib/qemu-flags.nix @@ -0,0 +1,10 @@ +# QEMU flags shared between various Nix expressions. + +{ + + qemuNICFlags = nic: net: machine: + [ "-net nic,vlan=${toString nic},macaddr=52:54:00:12:${toString net}:${toString machine},model=virtio" + "-net vde,vlan=${toString nic},sock=$QEMU_VDE_SOCKET_${toString net}" + ]; + +} diff --git a/nixos/lib/test-driver/Logger.pm b/nixos/lib/test-driver/Logger.pm new file mode 100644 index 000000000000..6e62fdfd7708 --- /dev/null +++ b/nixos/lib/test-driver/Logger.pm @@ -0,0 +1,70 @@ +package Logger; + +use strict; +use Thread::Queue; +use XML::Writer; + +sub new { + my ($class) = @_; + + my $logFile = defined $ENV{LOGFILE} ? "$ENV{LOGFILE}" : "/dev/null"; + my $log = new XML::Writer(OUTPUT => new IO::File(">$logFile")); + + my $self = { + log => $log, + logQueue => Thread::Queue->new() + }; + + $self->{log}->startTag("logfile"); + + bless $self, $class; + return $self; +} + +sub close { + my ($self) = @_; + $self->{log}->endTag("logfile"); + $self->{log}->end; +} + +sub drainLogQueue { + my ($self) = @_; + while (defined (my $item = $self->{logQueue}->dequeue_nb())) { + $self->{log}->dataElement("line", sanitise($item->{msg}), 'machine' => $item->{machine}, 'type' => 'serial'); + } +} + +sub maybePrefix { + my ($msg, $attrs) = @_; + $msg = $attrs->{machine} . ": " . $msg if defined $attrs->{machine}; + return $msg; +} + +sub nest { + my ($self, $msg, $coderef, $attrs) = @_; + print STDERR maybePrefix("$msg\n", $attrs); + $self->{log}->startTag("nest"); + $self->{log}->dataElement("head", $msg, %{$attrs}); + $self->drainLogQueue(); + eval { &$coderef }; + my $res = $@; + $self->drainLogQueue(); + $self->{log}->endTag("nest"); + die $@ if $@; +} + +sub sanitise { + my ($s) = @_; + $s =~ s/[[:cntrl:]\xff]//g; + return $s; +} + +sub log { + my ($self, $msg, $attrs) = @_; + chomp $msg; + print STDERR maybePrefix("$msg\n", $attrs); + $self->drainLogQueue(); + $self->{log}->dataElement("line", $msg, %{$attrs}); +} + +1; diff --git a/nixos/lib/test-driver/Machine.pm b/nixos/lib/test-driver/Machine.pm new file mode 100644 index 000000000000..a28214ea934f --- /dev/null +++ b/nixos/lib/test-driver/Machine.pm @@ -0,0 +1,568 @@ +package Machine; + +use strict; +use threads; +use Socket; +use IO::Handle; +use POSIX qw(dup2); +use FileHandle; +use Cwd; +use File::Basename; +use File::Path qw(make_path); + + +my $showGraphics = defined $ENV{'DISPLAY'}; + +my $sharedDir; + + +sub new { + my ($class, $args) = @_; + + my $startCommand = $args->{startCommand}; + + my $name = $args->{name}; + if (!$name) { + $startCommand =~ /run-(.*)-vm$/ if defined $startCommand; + $name = $1 || "machine"; + } + + if (!$startCommand) { + # !!! merge with qemu-vm.nix. + $startCommand = + "qemu-kvm -m 384 " . + "-net nic,model=virtio \$QEMU_OPTS "; + my $iface = $args->{hdaInterface} || "virtio"; + $startCommand .= "-drive file=" . Cwd::abs_path($args->{hda}) . ",if=$iface,boot=on,werror=report " + if defined $args->{hda}; + $startCommand .= "-cdrom $args->{cdrom} " + if defined $args->{cdrom}; + $startCommand .= $args->{qemuFlags} || ""; + } else { + $startCommand = Cwd::abs_path $startCommand; + } + + my $tmpDir = $ENV{'TMPDIR'} || "/tmp"; + unless (defined $sharedDir) { + $sharedDir = $tmpDir . "/xchg-shared"; + make_path($sharedDir, { mode => 0700, owner => $< }); + } + + my $allowReboot = 0; + $allowReboot = $args->{allowReboot} if defined $args->{allowReboot}; + + my $self = { + startCommand => $startCommand, + name => $name, + allowReboot => $allowReboot, + booted => 0, + pid => 0, + connected => 0, + socket => undef, + stateDir => "$tmpDir/vm-state-$name", + monitor => undef, + log => $args->{log}, + redirectSerial => $args->{redirectSerial} // 1, + }; + + mkdir $self->{stateDir}, 0700; + + bless $self, $class; + return $self; +} + + +sub log { + my ($self, $msg) = @_; + $self->{log}->log($msg, { machine => $self->{name} }); +} + + +sub nest { + my ($self, $msg, $coderef, $attrs) = @_; + $self->{log}->nest($msg, $coderef, { %{$attrs || {}}, machine => $self->{name} }); +} + + +sub name { + my ($self) = @_; + return $self->{name}; +} + + +sub stateDir { + my ($self) = @_; + return $self->{stateDir}; +} + + +sub start { + my ($self) = @_; + return if $self->{booted}; + + $self->log("starting vm"); + + # Create a socket pair for the serial line input/output of the VM. + my ($serialP, $serialC); + socketpair($serialP, $serialC, PF_UNIX, SOCK_STREAM, 0) or die; + + # Create a Unix domain socket to which QEMU's monitor will connect. + my $monitorPath = $self->{stateDir} . "/monitor"; + unlink $monitorPath; + my $monitorS; + socket($monitorS, PF_UNIX, SOCK_STREAM, 0) or die; + bind($monitorS, sockaddr_un($monitorPath)) or die "cannot bind monitor socket: $!"; + listen($monitorS, 1) or die; + + # Create a Unix domain socket to which the root shell in the guest will connect. + my $shellPath = $self->{stateDir} . "/shell"; + unlink $shellPath; + my $shellS; + socket($shellS, PF_UNIX, SOCK_STREAM, 0) or die; + bind($shellS, sockaddr_un($shellPath)) or die "cannot bind shell socket: $!"; + listen($shellS, 1) or die; + + # Start the VM. + my $pid = fork(); + die if $pid == -1; + + if ($pid == 0) { + close $serialP; + close $monitorS; + close $shellS; + if ($self->{redirectSerial}) { + open NUL, "</dev/null" or die; + dup2(fileno(NUL), fileno(STDIN)); + dup2(fileno($serialC), fileno(STDOUT)); + dup2(fileno($serialC), fileno(STDERR)); + } + $ENV{TMPDIR} = $self->{stateDir}; + $ENV{SHARED_DIR} = $sharedDir; + $ENV{USE_TMPDIR} = 1; + $ENV{QEMU_OPTS} = + ($self->{allowReboot} ? "" : "-no-reboot ") . + "-monitor unix:./monitor -chardev socket,id=shell,path=./shell " . + "-device virtio-serial -device virtconsole,chardev=shell " . + ($showGraphics ? "-serial stdio" : "-nographic") . " " . ($ENV{QEMU_OPTS} || ""); + chdir $self->{stateDir} or die; + exec $self->{startCommand}; + die "running VM script: $!"; + } + + # Process serial line output. + close $serialC; + + threads->create(\&processSerialOutput, $self, $serialP)->detach; + + sub processSerialOutput { + my ($self, $serialP) = @_; + while (<$serialP>) { + chomp; + s/\r$//; + print STDERR $self->{name}, "# $_\n"; + $self->{log}->{logQueue}->enqueue({msg => $_, machine => $self->{name}}); # !!! + } + } + + eval { + local $SIG{CHLD} = sub { die "QEMU died prematurely\n"; }; + + # Wait until QEMU connects to the monitor. + accept($self->{monitor}, $monitorS) or die; + + # Wait until QEMU connects to the root shell socket. QEMU + # does so immediately; this doesn't mean that the root shell + # has connected yet inside the guest. + accept($self->{socket}, $shellS) or die; + $self->{socket}->autoflush(1); + }; + die "$@" if $@; + + $self->waitForMonitorPrompt; + + $self->log("QEMU running (pid $pid)"); + + $self->{pid} = $pid; + $self->{booted} = 1; +} + + +# Send a command to the monitor and wait for it to finish. TODO: QEMU +# also has a JSON-based monitor interface now, but it doesn't support +# all commands yet. We should use it once it does. +sub sendMonitorCommand { + my ($self, $command) = @_; + $self->log("sending monitor command: $command"); + syswrite $self->{monitor}, "$command\n"; + return $self->waitForMonitorPrompt; +} + + +# Wait until the monitor sends "(qemu) ". +sub waitForMonitorPrompt { + my ($self) = @_; + my $res = ""; + my $s; + while (sysread($self->{monitor}, $s, 1024)) { + $res .= $s; + last if $res =~ s/\(qemu\) $//; + } + return $res; +} + + +# Call the given code reference repeatedly, with 1 second intervals, +# until it returns 1 or a timeout is reached. +sub retry { + my ($coderef) = @_; + my $n; + for ($n = 0; $n < 900; $n++) { + return if &$coderef; + sleep 1; + } + die "action timed out after $n seconds"; +} + + +sub connect { + my ($self) = @_; + return if $self->{connected}; + + $self->nest("waiting for the VM to finish booting", sub { + + $self->start; + + local $SIG{ALRM} = sub { die "timed out waiting for the VM to connect\n"; }; + alarm 300; + readline $self->{socket} or die "the VM quit before connecting\n"; + alarm 0; + + $self->log("connected to guest root shell"); + $self->{connected} = 1; + + }); +} + + +sub waitForShutdown { + my ($self) = @_; + return unless $self->{booted}; + + $self->nest("waiting for the VM to power off", sub { + waitpid $self->{pid}, 0; + $self->{pid} = 0; + $self->{booted} = 0; + $self->{connected} = 0; + }); +} + + +sub isUp { + my ($self) = @_; + return $self->{booted} && $self->{connected}; +} + + +sub execute_ { + my ($self, $command) = @_; + + $self->connect; + + print { $self->{socket} } ("( $command ); echo '|!=EOF' \$?\n"); + + my $out = ""; + + while (1) { + my $line = readline($self->{socket}); + die "connection to VM lost unexpectedly" unless defined $line; + #$self->log("got line: $line"); + if ($line =~ /^(.*)\|\!\=EOF\s+(\d+)$/) { + $out .= $1; + $self->log("exit status $2"); + return ($2, $out); + } + $out .= $line; + } +} + + +sub execute { + my ($self, $command) = @_; + my @res; + $self->nest("running command: $command", sub { + @res = $self->execute_($command); + }); + return @res; +} + + +sub succeed { + my ($self, @commands) = @_; + + my $res; + foreach my $command (@commands) { + $self->nest("must succeed: $command", sub { + my ($status, $out) = $self->execute_($command); + if ($status != 0) { + $self->log("output: $out"); + die "command `$command' did not succeed (exit code $status)\n"; + } + $res .= $out; + }); + } + + return $res; +} + + +sub mustSucceed { + succeed @_; +} + + +sub waitUntilSucceeds { + my ($self, $command) = @_; + $self->nest("waiting for success: $command", sub { + retry sub { + my ($status, $out) = $self->execute($command); + return 1 if $status == 0; + }; + }); +} + + +sub waitUntilFails { + my ($self, $command) = @_; + $self->nest("waiting for failure: $command", sub { + retry sub { + my ($status, $out) = $self->execute($command); + return 1 if $status != 0; + }; + }); +} + + +sub fail { + my ($self, $command) = @_; + $self->nest("must fail: $command", sub { + my ($status, $out) = $self->execute_($command); + die "command `$command' unexpectedly succeeded" + if $status == 0; + }); +} + + +sub mustFail { + fail @_; +} + + +sub getUnitInfo { + my ($self, $unit) = @_; + my ($status, $lines) = $self->execute("systemctl --no-pager show '$unit'"); + return undef if $status != 0; + my $info = {}; + foreach my $line (split '\n', $lines) { + $line =~ /^([^=]+)=(.*)$/ or next; + $info->{$1} = $2; + } + return $info; +} + + +# Wait for a systemd unit to reach the "active" state. +sub waitForUnit { + my ($self, $unit) = @_; + $self->nest("waiting for unit ‘$unit’", sub { + retry sub { + my $info = $self->getUnitInfo($unit); + my $state = $info->{ActiveState}; + die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed"; + return 1 if $state eq "active"; + }; + }); +} + + +sub waitForJob { + my ($self, $jobName) = @_; + return $self->waitForUnit($jobName); +} + + +# Wait until the specified file exists. +sub waitForFile { + my ($self, $fileName) = @_; + $self->nest("waiting for file ‘$fileName’", sub { + retry sub { + my ($status, $out) = $self->execute("test -e $fileName"); + return 1 if $status == 0; + } + }); +} + +sub startJob { + my ($self, $jobName) = @_; + $self->execute("systemctl start $jobName"); + # FIXME: check result +} + +sub stopJob { + my ($self, $jobName) = @_; + $self->execute("systemctl stop $jobName"); +} + + +# Wait until the machine is listening on the given TCP port. +sub waitForOpenPort { + my ($self, $port) = @_; + $self->nest("waiting for TCP port $port", sub { + retry sub { + my ($status, $out) = $self->execute("nc -z localhost $port"); + return 1 if $status == 0; + } + }); +} + + +# Wait until the machine is not listening on the given TCP port. +sub waitForClosedPort { + my ($self, $port) = @_; + retry sub { + my ($status, $out) = $self->execute("nc -z localhost $port"); + return 1 if $status != 0; + } +} + + +sub shutdown { + my ($self) = @_; + return unless $self->{booted}; + + print { $self->{socket} } ("poweroff\n"); + + $self->waitForShutdown; +} + + +sub crash { + my ($self) = @_; + return unless $self->{booted}; + + $self->log("forced crash"); + + $self->sendMonitorCommand("quit"); + + $self->waitForShutdown; +} + + +# Make the machine unreachable by shutting down eth1 (the multicast +# interface used to talk to the other VMs). We keep eth0 up so that +# the test driver can continue to talk to the machine. +sub block { + my ($self) = @_; + $self->sendMonitorCommand("set_link virtio-net-pci.1 off"); +} + + +# Make the machine reachable. +sub unblock { + my ($self) = @_; + $self->sendMonitorCommand("set_link virtio-net-pci.1 on"); +} + + +# Take a screenshot of the X server on :0.0. +sub screenshot { + my ($self, $filename) = @_; + my $dir = $ENV{'out'} || Cwd::abs_path("."); + $filename = "$dir/${filename}.png" if $filename =~ /^\w+$/; + my $tmp = "${filename}.ppm"; + my $name = basename($filename); + $self->nest("making screenshot ‘$name’", sub { + $self->sendMonitorCommand("screendump $tmp"); + system("convert $tmp ${filename}") == 0 + or die "cannot convert screenshot"; + unlink $tmp; + }, { image => $name } ); +} + + +# Wait until it is possible to connect to the X server. Note that +# testing the existence of /tmp/.X11-unix/X0 is insufficient. +sub waitForX { + my ($self, $regexp) = @_; + $self->nest("waiting for the X11 server", sub { + retry sub { + my ($status, $out) = $self->execute("xwininfo -root > /dev/null 2>&1"); + return 1 if $status == 0; + } + }); +} + + +sub getWindowNames { + my ($self) = @_; + my $res = $self->mustSucceed( + q{xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'}); + return split /\n/, $res; +} + + +sub waitForWindow { + my ($self, $regexp) = @_; + $self->nest("waiting for a window to appear", sub { + retry sub { + my @names = $self->getWindowNames; + foreach my $n (@names) { + return 1 if $n =~ /$regexp/; + } + } + }); +} + + +sub copyFileFromHost { + my ($self, $from, $to) = @_; + my $s = `cat $from` or die; + $self->mustSucceed("echo '$s' > $to"); # !!! escaping +} + + +sub sendKeys { + my ($self, @keys) = @_; + foreach my $key (@keys) { + $key = "spc" if $key eq " "; + $key = "ret" if $key eq "\n"; + $self->sendMonitorCommand("sendkey $key"); + } +} + + +sub sendChars { + my ($self, $chars) = @_; + $self->nest("sending keys ‘$chars’", sub { + $self->sendKeys(split //, $chars); + }); +} + + +# Sleep N seconds (in virtual guest time, not real time). +sub sleep { + my ($self, $time) = @_; + $self->succeed("sleep $time"); +} + + +# Forward a TCP port on the host to a TCP port on the guest. Useful +# during interactive testing. +sub forwardPort { + my ($self, $hostPort, $guestPort) = @_; + $hostPort = 8080 unless defined $hostPort; + $guestPort = 80 unless defined $guestPort; + $self->sendMonitorCommand("hostfwd_add tcp::$hostPort-:$guestPort"); +} + + +1; diff --git a/nixos/lib/test-driver/log2html.xsl b/nixos/lib/test-driver/log2html.xsl new file mode 100644 index 000000000000..8e907d85ffac --- /dev/null +++ b/nixos/lib/test-driver/log2html.xsl @@ -0,0 +1,135 @@ +<?xml version="1.0"?> + +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + + <xsl:output method='html' encoding="UTF-8" + doctype-public="-//W3C//DTD HTML 4.01//EN" + doctype-system="http://www.w3.org/TR/html4/strict.dtd" /> + + <xsl:template match="logfile"> + <html> + <head> + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script> + <script type="text/javascript" src="treebits.js" /> + <link rel="stylesheet" href="logfile.css" type="text/css" /> + <title>Log File</title> + </head> + <body> + <h1>VM build log</h1> + <p> + <a href="javascript:" class="logTreeExpandAll">Expand all</a> | + <a href="javascript:" class="logTreeCollapseAll">Collapse all</a> + </p> + <ul class='toplevel'> + <xsl:for-each select='line|nest'> + <li> + <xsl:apply-templates select='.'/> + </li> + </xsl:for-each> + </ul> + + <xsl:if test=".//*[@image]"> + <h1>Screenshots</h1> + <ul class="vmScreenshots"> + <xsl:for-each select='.//*[@image]'> + <li><a href="{@image}"><xsl:value-of select="@image" /></a></li> + </xsl:for-each> + </ul> + </xsl:if> + + </body> + </html> + </xsl:template> + + + <xsl:template match="nest"> + + <!-- The tree should be collapsed by default if all children are + unimportant or if the header is unimportant. --> + <xsl:variable name="collapsed" select="not(./head[@expanded]) and count(.//*[@error]) = 0"/> + + <xsl:variable name="style"><xsl:if test="$collapsed">display: none;</xsl:if></xsl:variable> + + <xsl:if test="line|nest"> + <a href="javascript:" class="logTreeToggle"> + <xsl:choose> + <xsl:when test="$collapsed"><xsl:text>+</xsl:text></xsl:when> + <xsl:otherwise><xsl:text>-</xsl:text></xsl:otherwise> + </xsl:choose> + </a> + <xsl:text> </xsl:text> + </xsl:if> + + <xsl:apply-templates select='head'/> + + <!-- Be careful to only generate <ul>s if there are <li>s, otherwise it’s malformed. --> + <xsl:if test="line|nest"> + + <ul class='nesting' style="{$style}"> + <xsl:for-each select='line|nest'> + + <!-- Is this the last line? If so, mark it as such so that it + can be rendered differently. --> + <xsl:variable name="class"><xsl:choose><xsl:when test="position() != last()">line</xsl:when><xsl:otherwise>lastline</xsl:otherwise></xsl:choose></xsl:variable> + + <li class='{$class}'> + <span class='lineconn' /> + <span class='linebody'> + <xsl:apply-templates select='.'/> + </span> + </li> + </xsl:for-each> + </ul> + </xsl:if> + + </xsl:template> + + + <xsl:template match="head|line"> + <code> + <xsl:if test="@error"> + <xsl:attribute name="class">errorLine</xsl:attribute> + </xsl:if> + <xsl:if test="@warning"> + <xsl:attribute name="class">warningLine</xsl:attribute> + </xsl:if> + <xsl:if test="@priority = 3"> + <xsl:attribute name="class">prio3</xsl:attribute> + </xsl:if> + + <xsl:if test="@type = 'serial'"> + <xsl:attribute name="class">serial</xsl:attribute> + </xsl:if> + + <xsl:if test="@machine"> + <xsl:choose> + <xsl:when test="@type = 'serial'"> + <span class="machine"><xsl:value-of select="@machine"/># </span> + </xsl:when> + <xsl:otherwise> + <span class="machine"><xsl:value-of select="@machine"/>: </span> + </xsl:otherwise> + </xsl:choose> + </xsl:if> + + <xsl:choose> + <xsl:when test="@image"> + <a href="{@image}"><xsl:apply-templates/></a> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates/> + </xsl:otherwise> + </xsl:choose> + </code> + </xsl:template> + + + <xsl:template match="storeref"> + <em class='storeref'> + <span class='popup'><xsl:apply-templates/></span> + <span class='elided'>/...</span><xsl:apply-templates select='name'/><xsl:apply-templates select='path'/> + </em> + </xsl:template> + +</xsl:stylesheet> diff --git a/nixos/lib/test-driver/logfile.css b/nixos/lib/test-driver/logfile.css new file mode 100644 index 000000000000..a54d8504a867 --- /dev/null +++ b/nixos/lib/test-driver/logfile.css @@ -0,0 +1,129 @@ +body { + font-family: sans-serif; + background: white; +} + +h1 +{ + color: #005aa0; + font-size: 180%; +} + +a { + text-decoration: none; +} + + +ul.nesting, ul.toplevel { + padding: 0; + margin: 0; +} + +ul.toplevel { + list-style-type: none; +} + +.line, .head { + padding-top: 0em; +} + +ul.nesting li.line, ul.nesting li.lastline { + position: relative; + list-style-type: none; +} + +ul.nesting li.line { + padding-left: 2.0em; +} + +ul.nesting li.lastline { + padding-left: 2.1em; /* for the 0.1em border-left in .lastline > .lineconn */ +} + +li.line { + border-left: 0.1em solid #6185a0; +} + +li.line > span.lineconn, li.lastline > span.lineconn { + position: absolute; + height: 0.65em; + left: 0em; + width: 1.5em; + border-bottom: 0.1em solid #6185a0; +} + +li.lastline > span.lineconn { + border-left: 0.1em solid #6185a0; +} + + +em.storeref { + color: #500000; + position: relative; + width: 100%; +} + +em.storeref:hover { + background-color: #eeeeee; +} + +*.popup { + display: none; +/* background: url('http://losser.st-lab.cs.uu.nl/~mbravenb/menuback.png') repeat; */ + background: #ffffcd; + border: solid #555555 1px; + position: absolute; + top: 0em; + left: 0em; + margin: 0; + padding: 0; + z-index: 100; +} + +em.storeref:hover span.popup { + display: inline; + width: 40em; +} + + +.logTreeToggle { + text-decoration: none; + font-family: monospace; + font-size: larger; +} + +.errorLine { + color: #ff0000; + font-weight: bold; +} + +.warningLine { + color: darkorange; + font-weight: bold; +} + +.prio3 { + font-style: italic; +} + +code { + white-space: pre-wrap; +} + +.serial { + color: #56115c; +} + +.machine { + color: #002399; + font-style: italic; +} + +ul.vmScreenshots { + padding-left: 1em; +} + +ul.vmScreenshots li { + font-family: monospace; + list-style: square; +} diff --git a/nixos/lib/test-driver/test-driver.pl b/nixos/lib/test-driver/test-driver.pl new file mode 100644 index 000000000000..c6a707cdf6b9 --- /dev/null +++ b/nixos/lib/test-driver/test-driver.pl @@ -0,0 +1,178 @@ +#! /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); + +$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" 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; + return $vm; +} + +foreach my $vmScript (@ARGV) { + my $vm = createMachine({startCommand => $vmScript}); + $context .= "my \$" . $vm->name . " = \$vms{'" . $vm->name . "'}; "; +} + + +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"); + } + }); + + 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(); +} + + +runTests; + +exit ($nrSucceeded < $nrTests ? 1 : 0); diff --git a/nixos/lib/test-driver/treebits.js b/nixos/lib/test-driver/treebits.js new file mode 100644 index 000000000000..9754093dfd07 --- /dev/null +++ b/nixos/lib/test-driver/treebits.js @@ -0,0 +1,30 @@ +$(document).ready(function() { + + /* When a toggle is clicked, show or hide the subtree. */ + $(".logTreeToggle").click(function() { + if ($(this).siblings("ul:hidden").length != 0) { + $(this).siblings("ul").show(); + $(this).text("-"); + } else { + $(this).siblings("ul").hide(); + $(this).text("+"); + } + }); + + /* Implementation of the expand all link. */ + $(".logTreeExpandAll").click(function() { + $(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() { + $(this).siblings("ul").show(); + $(this).text("-"); + }); + }); + + /* Implementation of the collapse all link. */ + $(".logTreeCollapseAll").click(function() { + $(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() { + $(this).siblings("ul").hide(); + $(this).text("+"); + }); + }); + +}); diff --git a/nixos/lib/testing.nix b/nixos/lib/testing.nix new file mode 100644 index 000000000000..7be0903ed3a8 --- /dev/null +++ b/nixos/lib/testing.nix @@ -0,0 +1,244 @@ +{ system, minimal ? false }: + +with import ./build-vms.nix { inherit system minimal; }; +with pkgs; + +rec { + + inherit pkgs; + + + testDriver = stdenv.mkDerivation { + name = "nixos-test-driver"; + + buildInputs = [ makeWrapper perl ]; + + unpackPhase = "true"; + + installPhase = + '' + mkdir -p $out/bin + cp ${./test-driver/test-driver.pl} $out/bin/nixos-test-driver + chmod u+x $out/bin/nixos-test-driver + + libDir=$out/lib/perl5/site_perl + mkdir -p $libDir + cp ${./test-driver/Machine.pm} $libDir/Machine.pm + cp ${./test-driver/Logger.pm} $libDir/Logger.pm + + wrapProgram $out/bin/nixos-test-driver \ + --prefix PATH : "${pkgs.qemu_kvm}/bin:${pkgs.vde2}/bin:${imagemagick}/bin:${coreutils}/bin" \ + --prefix PERL5LIB : "${lib.makePerlPath [ perlPackages.TermReadLineGnu perlPackages.XMLWriter perlPackages.IOTty ]}:$out/lib/perl5/site_perl" + ''; + }; + + + # Run an automated test suite in the given virtual network. + # `driver' is the script that runs the network. + runTests = driver: + stdenv.mkDerivation { + name = "vm-test-run"; + + requiredSystemFeatures = [ "kvm" "nixos-test" ]; + + buildInputs = [ pkgs.libxslt ]; + + buildCommand = + '' + mkdir -p $out/nix-support + + LOGFILE=$out/log.xml tests='eval $ENV{testScript}; die $@ if $@;' ${driver}/bin/nixos-test-driver || failed=1 + + # Generate a pretty-printed log. + xsltproc --output $out/log.html ${./test-driver/log2html.xsl} $out/log.xml + ln -s ${./test-driver/logfile.css} $out/logfile.css + ln -s ${./test-driver/treebits.js} $out/treebits.js + + touch $out/nix-support/hydra-build-products + echo "report testlog $out log.html" >> $out/nix-support/hydra-build-products + + for i in */xchg/coverage-data; do + mkdir -p $out/coverage-data + mv $i $out/coverage-data/$(dirname $(dirname $i)) + done + + [ -z "$failed" ] || touch $out/nix-support/failed + ''; # */ + }; + + + # Generate a coverage report from the coverage data produced by + # runTests. + makeReport = x: runCommand "report" { buildInputs = [rsync]; } + '' + mkdir -p $TMPDIR/gcov/ + + for d in ${x}/coverage-data/*; do + echo "doing $d" + [ -n "$(ls -A "$d")" ] || continue + + for i in $(cd $d/nix/store && ls); do + if ! test -e $TMPDIR/gcov/nix/store/$i; then + echo "copying $i" + mkdir -p $TMPDIR/gcov/$(echo $i | cut -c34-) + rsync -rv /nix/store/$i/.build/* $TMPDIR/gcov/ + fi + done + + chmod -R u+w $TMPDIR/gcov + + find $TMPDIR/gcov -name "*.gcda" -exec rm {} \; + + for i in $(cd $d/nix/store && ls); do + rsync -rv $d/nix/store/$i/.build/* $TMPDIR/gcov/ + done + + find $TMPDIR/gcov -name "*.gcda" -exec chmod 644 {} \; + + echo "producing info..." + ${pkgs.lcov}/bin/geninfo --ignore-errors source,gcov $TMPDIR/gcov --output-file $TMPDIR/app.info + cat $TMPDIR/app.info >> $TMPDIR/full.info + done + + echo "making report..." + mkdir -p $out/coverage + ${pkgs.lcov}/bin/genhtml --show-details $TMPDIR/full.info -o $out/coverage + cp $TMPDIR/full.info $out/coverage/ + + mkdir -p $out/nix-support + cat ${x}/nix-support/hydra-build-products >> $out/nix-support/hydra-build-products + echo "report coverage $out/coverage" >> $out/nix-support/hydra-build-products + [ ! -e ${x}/nix-support/failed ] || touch $out/nix-support/failed + ''; # */ + + + makeTest = testFun: complete (call testFun); + makeTests = testsFun: lib.mapAttrs (name: complete) (call testsFun); + + apply = makeTest; # compatibility + call = f: f { inherit pkgs system; }; + + complete = t: t // rec { + nodes = buildVirtualNetwork ( + if t ? nodes then t.nodes else + if t ? machine then { machine = t.machine; } + else { } ); + + testScript = + # Call the test script with the computed nodes. + if builtins.isFunction t.testScript + then t.testScript { inherit nodes; } + else t.testScript; + + vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes); + + vms = map (m: m.config.system.build.vm) (lib.attrValues nodes); + + # Generate onvenience wrappers for running the test driver + # interactively with the specified network, and for starting the + # VMs from the command line. + driver = runCommand "nixos-test-driver" + { buildInputs = [ makeWrapper]; + inherit testScript; + preferLocalBuild = true; + } + '' + mkdir -p $out/bin + echo "$testScript" > $out/test-script + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/ + vms="$(for i in ${toString vms}; do echo $i/bin/run-*-vm; done)" + wrapProgram $out/bin/nixos-test-driver \ + --add-flags "$vms" \ + --run "testScript=\"\$(cat $out/test-script)\"" \ + --set testScript '"$testScript"' \ + --set VLANS '"${toString vlans}"' + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms + wrapProgram $out/bin/nixos-run-vms \ + --add-flags "$vms" \ + --set tests '"startAll; joinAll;"' \ + --set VLANS '"${toString vlans}"' \ + ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"} + ''; # " + + test = runTests driver; + + report = makeReport test; + }; + + + runInMachine = + { drv + , machine + , preBuild ? "" + , postBuild ? "" + , ... # ??? + }: + let + vm = buildVM { } + [ machine + { key = "hostname"; networking.hostName = "client"; } + ]; + + buildrunner = writeText "vm-build" '' + source $1 + + ${coreutils}/bin/mkdir -p $TMPDIR + cd $TMPDIR + + $origBuilder $origArgs + + exit $? + ''; + + testscript = '' + startAll; + ${preBuild} + $client->succeed("env -i ${pkgs.bash}/bin/bash ${buildrunner} /tmp/xchg/saved-env >&2"); + ${postBuild} + ''; + + vmRunCommand = writeText "vm-run" '' + ${coreutils}/bin/mkdir $out + ${coreutils}/bin/mkdir -p vm-state-client/xchg + export > vm-state-client/xchg/saved-env + export tests='${testscript}' + ${testDriver}/bin/nixos-test-driver ${vm.config.system.build.vm}/bin/run-*-vm + ''; # */ + + in + lib.overrideDerivation drv (attrs: { + requiredSystemFeatures = [ "kvm" ]; + builder = "${bash}/bin/sh"; + args = ["-e" vmRunCommand]; + origArgs = attrs.args; + origBuilder = attrs.builder; + }); + + + runInMachineWithX = { require ? [], ... } @ args: + let + client = + { config, pkgs, ... }: + { + inherit require; + virtualisation.memorySize = 1024; + services.xserver.enable = true; + services.xserver.displayManager.slim.enable = false; + services.xserver.displayManager.auto.enable = true; + services.xserver.windowManager.default = "icewm"; + services.xserver.windowManager.icewm.enable = true; + services.xserver.desktopManager.default = "none"; + }; + in + runInMachine ({ + machine = client; + preBuild = + '' + $client->waitForX; + ''; + } // args); + + + simpleTest = as: (makeTest ({ ... }: as)).test; + +} diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix new file mode 100644 index 000000000000..35c56e8c32bb --- /dev/null +++ b/nixos/lib/utils.nix @@ -0,0 +1,10 @@ +pkgs: with pkgs.lib; + +rec { + + # Escape a path according to the systemd rules, e.g. /dev/xyzzy + # becomes dev-xyzzy. FIXME: slow. + escapeSystemdPath = s: + replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"] (substring 1 (stringLength s) s); + +} |