Merge pull request #1092 from DeterminateSystems/restart-failed-no-eval

Allow restarting failed builds even with no eval to compare to
This commit is contained in:
Graham Christensen 2022-01-10 21:26:08 -05:00 committed by GitHub
commit ff26ce0d06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 228 additions and 55 deletions

View file

@ -6,6 +6,7 @@ use warnings;
use base 'Hydra::Base::Controller::NixChannel'; use base 'Hydra::Base::Controller::NixChannel';
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils; use Hydra::Helper::CatalystUtils;
use Hydra::Helper::BuildDiff;
use List::SomeUtils qw(uniq); use List::SomeUtils qw(uniq);
@ -63,63 +64,19 @@ sub view_GET {
$c->stash->{otherEval} = $eval2 if defined $eval2; $c->stash->{otherEval} = $eval2 if defined $eval2;
sub cmpBuilds {
my ($left, $right) = @_;
return $left->get_column('job') cmp $right->get_column('job')
|| $left->get_column('system') cmp $right->get_column('system')
}
my @builds = $eval->builds->search($filter, { columns => [@buildListColumns] }); my @builds = $eval->builds->search($filter, { columns => [@buildListColumns] });
my @builds2 = defined $eval2 ? $eval2->builds->search($filter, { columns => [@buildListColumns] }) : (); my @builds2 = defined $eval2 ? $eval2->builds->search($filter, { columns => [@buildListColumns] }) : ();
@builds = sort { cmpBuilds($a, $b) } @builds; my $diff = buildDiff([@builds], [@builds2]);
@builds2 = sort { cmpBuilds($a, $b) } @builds2; $c->stash->{stillSucceed} = $diff->{stillSucceed};
$c->stash->{stillFail} = $diff->{stillFail};
$c->stash->{stillSucceed} = []; $c->stash->{nowSucceed} = $diff->{nowSucceed};
$c->stash->{stillFail} = []; $c->stash->{nowFail} = $diff->{nowFail};
$c->stash->{nowSucceed} = []; $c->stash->{new} = $diff->{new};
$c->stash->{nowFail} = []; $c->stash->{removed} = $diff->{removed};
$c->stash->{new} = []; $c->stash->{unfinished} = $diff->{unfinished};
$c->stash->{removed} = []; $c->stash->{aborted} = $diff->{aborted};
$c->stash->{unfinished} = []; $c->stash->{failed} = $diff->{failed};
$c->stash->{aborted} = [];
my $n = 0;
foreach my $build (@builds) {
my $aborted = $build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4);
my $d;
my $found = 0;
while ($n < scalar(@builds2)) {
my $build2 = $builds2[$n];
my $d = cmpBuilds($build, $build2);
last if $d == -1;
if ($d == 0) {
$n++;
$found = 1;
if ($aborted) {
# do nothing
} elsif ($build->finished == 0 || $build2->finished == 0) {
push @{$c->stash->{unfinished}}, $build;
} elsif ($build->buildstatus == 0 && $build2->buildstatus == 0) {
push @{$c->stash->{stillSucceed}}, $build;
} elsif ($build->buildstatus != 0 && $build2->buildstatus != 0) {
push @{$c->stash->{stillFail}}, $build;
} elsif ($build->buildstatus == 0 && $build2->buildstatus != 0) {
push @{$c->stash->{nowSucceed}}, $build;
} elsif ($build->buildstatus != 0 && $build2->buildstatus == 0) {
push @{$c->stash->{nowFail}}, $build;
} else { die; }
last;
}
push @{$c->stash->{removed}}, { job => $build2->get_column('job'), system => $build2->get_column('system') };
$n++;
}
if ($aborted) {
push @{$c->stash->{aborted}}, $build;
} else {
push @{$c->stash->{new}}, $build if !$found;
}
}
$c->stash->{full} = ($c->req->params->{full} || "0") eq "1"; $c->stash->{full} = ($c->req->params->{full} || "0") eq "1";

View file

@ -0,0 +1,82 @@
package Hydra::Helper::BuildDiff;
use utf8;
use strict;
use warnings;
our @ISA = qw(Exporter);
our @EXPORT = qw(
buildDiff
);
sub cmpBuilds {
my ($left, $right) = @_;
return $left->get_column('job') cmp $right->get_column('job')
|| $left->get_column('system') cmp $right->get_column('system')
}
sub buildDiff {
# $builds is the list of current builds
# $builds2 is the list of previous (to-be-compared-to) builds
my ($builds, $builds2) = @_;
$builds = [sort { cmpBuilds($a, $b) } @{$builds}];
$builds2 = [sort { cmpBuilds($a, $b) } @{$builds2}];
my $ret = {
stillSucceed => [],
stillFail => [],
nowSucceed => [],
nowFail => [],
new => [],
removed => [],
unfinished => [],
aborted => [],
failed => [],
};
my $n = 0;
foreach my $build (@{$builds}) {
my $aborted = $build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4);
my $d;
my $found = 0;
while ($n < scalar(@{$builds2})) {
my $build2 = @{$builds2}[$n];
my $d = cmpBuilds($build, $build2);
last if $d == -1;
if ($d == 0) {
$n++;
$found = 1;
if ($aborted) {
# do nothing
} elsif ($build->finished == 0 || $build2->finished == 0) {
push @{$ret->{unfinished}}, $build;
} elsif ($build->buildstatus == 0 && $build2->buildstatus == 0) {
push @{$ret->{stillSucceed}}, $build;
} elsif ($build->buildstatus != 0 && $build2->buildstatus != 0) {
push @{$ret->{stillFail}}, $build;
} elsif ($build->buildstatus == 0 && $build2->buildstatus != 0) {
push @{$ret->{nowSucceed}}, $build;
} elsif ($build->buildstatus != 0 && $build2->buildstatus == 0) {
push @{$ret->{nowFail}}, $build;
} else { die; }
last;
}
my $job_system = { job => $build2->get_column('job'), system => $build2->get_column('system') };
push @{$ret->{removed}}, $job_system;
$n++;
}
if ($aborted) {
push @{$ret->{aborted}}, $build;
} else {
push @{$ret->{new}}, $build if !$found;
}
if (defined $build->buildstatus && $build->buildstatus != 0) {
push @{$ret->{failed}}, $build;
}
}
return $ret;
}
1;

View file

@ -51,7 +51,7 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
[% IF unfinished.size > 0 %] [% IF unfinished.size > 0 %]
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('cancel'), [eval.id]) %]">Cancel all scheduled builds</a> <a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('cancel'), [eval.id]) %]">Cancel all scheduled builds</a>
[% END %] [% END %]
[% IF aborted.size > 0 || stillFail.size > 0 || nowFail.size > 0 %] [% IF aborted.size > 0 || stillFail.size > 0 || nowFail.size > 0 || failed.size > 0 %]
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('restart_failed'), [eval.id]) %]">Restart all failed builds</a> <a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('restart_failed'), [eval.id]) %]">Restart all failed builds</a>
[% END %] [% END %]
[% IF aborted.size > 0 %] [% IF aborted.size > 0 %]

View file

@ -0,0 +1,28 @@
use feature 'unicode_strings';
use strict;
use warnings;
use Setup;
use JSON::MaybeXS qw(decode_json encode_json);
my %ctx = test_init();
require Hydra::Schema;
require Hydra::Model::DB;
require Hydra::Helper::Nix;
use Test2::V0;
require Catalyst::Test;
Catalyst::Test->import('Hydra');
use HTTP::Request::Common qw(POST PUT GET DELETE);
my $db = Hydra::Model::DB->new;
hydra_setup($db);
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
my ($eval, @evals) = $jobset->jobsetevals;
my $fetch = request(GET '/eval/' . $eval->id);
is($fetch->code, 200, "eval page is 200");
done_testing;

106
t/Helper/BuildDiff.t Normal file
View file

@ -0,0 +1,106 @@
use strict;
use warnings;
use Setup;
use Test2::V0;
use Hydra::Helper::BuildDiff;
my $ctx = test_context();
my $builds = $ctx->makeAndEvaluateJobset(
expression => "basic.nix",
build => 1
);
subtest "empty diff" => sub {
my $ret = buildDiff([], []);
is(
$ret,
{
stillSucceed => [],
stillFail => [],
nowSucceed => [],
nowFail => [],
new => [],
removed => [],
unfinished => [],
aborted => [],
failed => [],
},
"empty list of jobs returns empty diff"
);
};
subtest "2 different jobs" => sub {
my $ret = buildDiff([$builds->{"succeed_with_failed"}], [$builds->{"empty_dir"}]);
is($ret->{stillSucceed}, [], "stillSucceed");
is($ret->{stillFail}, [], "stillFail");
is($ret->{nowSucceed}, [], "nowSucceed");
is($ret->{nowFail}, [], "nowFail");
is($ret->{unfinished}, [], "unfinished");
is($ret->{aborted}, [], "aborted");
is(scalar(@{$ret->{new}}), 1, "list of new jobs is 1 element long");
is(
$ret->{new}[0]->get_column('id'),
$builds->{"succeed_with_failed"}->get_column('id'),
"succeed_with_failed is a new job"
);
is(scalar(@{$ret->{failed}}), 1, "list of failed jobs is 1 element long");
is(
$ret->{failed}[0]->get_column('id'),
$builds->{"succeed_with_failed"}->get_column('id'),
"succeed_with_failed is a failed job"
);
is(
$ret->{removed},
[
{
job => $builds->{"empty_dir"}->get_column('job'),
system => $builds->{"empty_dir"}->get_column('system')
}
],
"empty_dir is a removed job"
);
};
subtest "failed job with no previous history" => sub {
my $ret = buildDiff([$builds->{"fails"}], []);
is(scalar(@{$ret->{failed}}), 1, "list of failed jobs is 1 element long");
is(
$ret->{failed}[0]->get_column('id'),
$builds->{"fails"}->get_column('id'),
"fails is a failed job"
);
};
subtest "not-yet-built job with no previous history" => sub {
my $builds = $ctx->makeAndEvaluateJobset(
expression => "build-products.nix",
build => 0
);
my $ret = buildDiff([$builds->{"simple"}], []);
is($ret->{stillSucceed}, [], "stillSucceed");
is($ret->{stillFail}, [], "stillFail");
is($ret->{nowSucceed}, [], "nowSucceed");
is($ret->{nowFail}, [], "nowFail");
is($ret->{removed}, [], "removed");
is($ret->{unfinished}, [], "unfinished");
is($ret->{aborted}, [], "aborted");
is($ret->{failed}, [], "failed");
is(scalar(@{$ret->{new}}), 1, "list of new jobs is 1 element long");
is(
$ret->{new}[0]->get_column('id'),
$builds->{"simple"}->get_column('id'),
"simple is a new job"
);
};
done_testing;