diff --git a/src/script/hydra-eval-jobset b/src/script/hydra-eval-jobset index 108c59c8..8bafe07c 100755 --- a/src/script/hydra-eval-jobset +++ b/src/script/hydra-eval-jobset @@ -799,7 +799,13 @@ sub checkJobsetWrapped { foreach my $job (values %{$jobs}) { next unless $job->{constituents}; - my $x = $drvPathToId{$job->{drvPath}} or die; + + if (defined $job->{error}) { + die "aggregate job ‘$job->{jobName}’ failed with the error: $job->{error}\n"; + } + + my $x = $drvPathToId{$job->{drvPath}} or + die "aggregate job ‘$job->{jobName}’ has no corresponding build record.\n"; foreach my $drvPath (@{$job->{constituents}}) { my $constituent = $drvPathToId{$drvPath}; if (defined $constituent) { diff --git a/t/evaluator/evaluate-constituents-broken.t b/t/evaluator/evaluate-constituents-broken.t new file mode 100644 index 00000000..ed25d192 --- /dev/null +++ b/t/evaluator/evaluate-constituents-broken.t @@ -0,0 +1,32 @@ +use strict; +use warnings; +use Setup; +use Test2::V0; +use Hydra::Helper::Exec; + +my $ctx = test_context(); + +my $jobsetCtx = $ctx->makeJobset( + expression => 'constituents-broken.nix', +); +my $jobset = $jobsetCtx->{"jobset"}; + +my ($res, $stdout, $stderr) = captureStdoutStderr(60, + ("hydra-eval-jobset", $jobsetCtx->{"project"}->name, $jobset->name) +); +isnt($res, 0, "hydra-eval-jobset exits non-zero"); +ok(utf8::decode($stderr), "Stderr output is UTF8-clean"); +like( + $stderr, + qr/aggregate job ‘mixed_aggregate’ failed with the error: constituentA: does not exist/, + "The stderr record includes a relevant error message" +); + +$jobset->discard_changes; # refresh from DB +like( + $jobset->errormsg, + qr/aggregate job ‘mixed_aggregate’ failed with the error: constituentA: does not exist/, + "The jobset records a relevant error message" +); + +done_testing; diff --git a/t/jobs/constituents-broken.nix b/t/jobs/constituents-broken.nix new file mode 100644 index 00000000..0445a990 --- /dev/null +++ b/t/jobs/constituents-broken.nix @@ -0,0 +1,19 @@ +with import ./config.nix; +rec { + constituentA = null; + + constituentB = mkDerivation { + name = "empty-dir-B"; + builder = ./empty-dir-builder.sh; + }; + + mixed_aggregate = mkDerivation { + name = "mixed_aggregate"; + _hydraAggregate = true; + constituents = [ + "constituentA" + constituentB + ]; + builder = ./empty-dir-builder.sh; + }; +} diff --git a/t/lib/HydraTestContext.pm b/t/lib/HydraTestContext.pm index ade12280..237fcbe4 100644 --- a/t/lib/HydraTestContext.pm +++ b/t/lib/HydraTestContext.pm @@ -145,10 +145,47 @@ sub nix_state_dir { sub makeAndEvaluateJobset { my ($self, %opts) = @_; - my $expression = $opts{'expression'} || die "Mandatory 'expression' option not passed to makeAndEValuateJobset."; - my $should_build = $opts{'build'} // 0; + my $expression = $opts{'expression'} || die "Mandatory 'expression' option not passed to makeAndEvaluateJobset.\n"; my $jobsdir = $opts{'jobsdir'} // $self->jobsdir; + my $should_build = $opts{'build'} // 0; + my $jobsetCtx = $self->makeJobset( + expression => $expression, + jobsdir => $jobsdir, + ); + my $jobset = $jobsetCtx->{"jobset"}; + + evalSucceeds($jobset) or die "Evaluating jobs/$expression should exit with return code 0.\n"; + + my $builds = {}; + + for my $build ($jobset->builds) { + if ($should_build) { + runBuild($build) or die "Build '".$build->job."' from jobs/$expression should exit with return code 0.\n"; + $build->discard_changes(); + } + + $builds->{$build->job} = $build; + } + + return $builds; +} + +# Create a jobset. +# +# In return, you get a hash of the user, project, and jobset records. +# +# This always uses an `expression` from the `jobsdir` directory. +# +# Hash Parameters: +# +# * expression: The file in the jobsdir directory to evaluate +# * jobsdir: An alternative jobsdir to source the expression from +sub makeJobset { + my ($self, %opts) = @_; + + my $expression = $opts{'expression'} || die "Mandatory 'expression' option not passed to makeJobset.\n"; + my $jobsdir = $opts{'jobsdir'} // $self->jobsdir; # Create a new user for this test my $user = $self->db()->resultset('Users')->create({ @@ -174,23 +211,13 @@ sub makeAndEvaluateJobset { my $jobsetinput = $jobset->jobsetinputs->create({name => "jobs", type => "path"}); $jobsetinput->jobsetinputalts->create({altnr => 0, value => $jobsdir}); - evalSucceeds($jobset) or die "Evaluating jobs/$expression should exit with return code 0"; - - my $builds = {}; - - for my $build ($jobset->builds) { - if ($should_build) { - runBuild($build) or die "Build '".$build->job."' from jobs/$expression should exit with return code 0"; - $build->discard_changes(); - } - - $builds->{$build->job} = $build; - } - - return $builds; + return { + user => $user, + project => $project, + jobset => $jobset, + }; } - sub DESTROY { my ($self) = @_; @@ -200,7 +227,7 @@ sub DESTROY sub write_file { my ($path, $text) = @_; - open(my $fh, '>', $path) or die "Could not open file '$path' $!"; + open(my $fh, '>', $path) or die "Could not open file '$path' $!\n."; print $fh $text || ""; close $fh; }