test-driver: support testing user units

It is quite complicated to test services using the test-driver when
declaring user services with `systemd.user.services` such as many
X11-based services like `xautolock.service`.

This change adds an optional `$user` parameter to each systemd-related
function in the test-driver and runs `systemctl --user` commands using
`su -l $user -c ...` and sets the `XDG_RUNTIME_DIR` variable
accordingly and a new function named `systemctl` which is able to run a
systemd command with or without a specified user.

The change can be confirmed with a simple VM declaration like this:

```
import ./nixos/tests/make-test.nix ({ pkgs, lib }:

with lib;

{
name = "systemd-user-test";

nodes.machine = {
imports = [ ./nixos/tests/common/user-account.nix ];

services.xserver.enable = true;
services.xserver.displayManager.auto.enable = true;
services.xserver.displayManager.auto.user = "bob";
services.xserver.xautolock.enable = true;
};

testScript = ''
$machine->start;
$machine->waitForX;

$machine->waitForUnit("xautolock.service", "bob");
$machine->stopJob("xautolock.service", "bob");
$machine->startJob("xautolock.service", "bob");
$machine->systemctl("list-jobs --no-pager", "bob");
$machine->systemctl("show 'xautolock.service' --no-pager", "bob");
'';
})
```

+19 -9
+19 -9
nixos/lib/test-driver/Machine.pm
··· 362 363 364 sub getUnitInfo { 365 - my ($self, $unit) = @_; 366 - my ($status, $lines) = $self->execute("systemctl --no-pager show '$unit'"); 367 return undef if $status != 0; 368 my $info = {}; 369 foreach my $line (split '\n', $lines) { ··· 371 $info->{$1} = $2; 372 } 373 return $info; 374 } 375 376 # Fail if the given systemd unit is not in the "active" state. ··· 387 388 # Wait for a systemd unit to reach the "active" state. 389 sub waitForUnit { 390 - my ($self, $unit) = @_; 391 $self->nest("waiting for unit ‘$unit’", sub { 392 retry sub { 393 - my $info = $self->getUnitInfo($unit); 394 my $state = $info->{ActiveState}; 395 die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed"; 396 if ($state eq "inactive") { 397 # If there are no pending jobs, then assume this unit 398 # will never reach active state. 399 - my ($status, $jobs) = $self->execute("systemctl list-jobs --full 2>&1"); 400 if ($jobs =~ /No jobs/) { # FIXME: fragile 401 # Handle the case where the unit may have started 402 # between the previous getUnitInfo() and ··· 430 } 431 432 sub startJob { 433 - my ($self, $jobName) = @_; 434 - $self->execute("systemctl start $jobName"); 435 # FIXME: check result 436 } 437 438 sub stopJob { 439 - my ($self, $jobName) = @_; 440 - $self->execute("systemctl stop $jobName"); 441 } 442 443
··· 362 363 364 sub getUnitInfo { 365 + my ($self, $unit, $user) = @_; 366 + my ($status, $lines) = $self->systemctl("--no-pager show \"$unit\"", $user); 367 return undef if $status != 0; 368 my $info = {}; 369 foreach my $line (split '\n', $lines) { ··· 371 $info->{$1} = $2; 372 } 373 return $info; 374 + } 375 + 376 + sub systemctl { 377 + my ($self, $q, $user) = @_; 378 + if ($user) { 379 + $q =~ s/'/\\'/g; 380 + return $self->execute("su -l $user -c \$'XDG_RUNTIME_DIR=/run/user/`id -u` systemctl --user $q'"); 381 + } 382 + 383 + return $self->execute("systemctl $q"); 384 } 385 386 # Fail if the given systemd unit is not in the "active" state. ··· 397 398 # Wait for a systemd unit to reach the "active" state. 399 sub waitForUnit { 400 + my ($self, $unit, $user) = @_; 401 $self->nest("waiting for unit ‘$unit’", sub { 402 retry sub { 403 + my $info = $self->getUnitInfo($unit, $user); 404 my $state = $info->{ActiveState}; 405 die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed"; 406 if ($state eq "inactive") { 407 # If there are no pending jobs, then assume this unit 408 # will never reach active state. 409 + my ($status, $jobs) = $self->systemctl("list-jobs --full 2>&1", $user); 410 if ($jobs =~ /No jobs/) { # FIXME: fragile 411 # Handle the case where the unit may have started 412 # between the previous getUnitInfo() and ··· 440 } 441 442 sub startJob { 443 + my ($self, $jobName, $user) = @_; 444 + $self->systemctl("start $jobName", $user); 445 # FIXME: check result 446 } 447 448 sub stopJob { 449 + my ($self, $jobName, $user) = @_; 450 + $self->systemctl("stop $jobName", $user); 451 } 452 453