diff --git a/src/lib/Hydra/Controller/JobsetEval.pm b/src/lib/Hydra/Controller/JobsetEval.pm
index 6f6e7eab..30179d49 100644
--- a/src/lib/Hydra/Controller/JobsetEval.pm
+++ b/src/lib/Hydra/Controller/JobsetEval.pm
@@ -6,6 +6,7 @@ use warnings;
use base 'Hydra::Base::Controller::NixChannel';
use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils;
+use Hydra::Helper::BuildDiff;
use List::SomeUtils qw(uniq);
@@ -63,63 +64,19 @@ sub view_GET {
$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 @builds2 = defined $eval2 ? $eval2->builds->search($filter, { columns => [@buildListColumns] }) : ();
- @builds = sort { cmpBuilds($a, $b) } @builds;
- @builds2 = sort { cmpBuilds($a, $b) } @builds2;
-
- $c->stash->{stillSucceed} = [];
- $c->stash->{stillFail} = [];
- $c->stash->{nowSucceed} = [];
- $c->stash->{nowFail} = [];
- $c->stash->{new} = [];
- $c->stash->{removed} = [];
- $c->stash->{unfinished} = [];
- $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;
- }
- }
+ my $diff = buildDiff([@builds], [@builds2]);
+ $c->stash->{stillSucceed} = $diff->{stillSucceed};
+ $c->stash->{stillFail} = $diff->{stillFail};
+ $c->stash->{nowSucceed} = $diff->{nowSucceed};
+ $c->stash->{nowFail} = $diff->{nowFail};
+ $c->stash->{new} = $diff->{new};
+ $c->stash->{removed} = $diff->{removed};
+ $c->stash->{unfinished} = $diff->{unfinished};
+ $c->stash->{aborted} = $diff->{aborted};
+ $c->stash->{failed} = $diff->{failed};
$c->stash->{full} = ($c->req->params->{full} || "0") eq "1";
diff --git a/src/lib/Hydra/Helper/BuildDiff.pm b/src/lib/Hydra/Helper/BuildDiff.pm
new file mode 100644
index 00000000..cd8c7691
--- /dev/null
+++ b/src/lib/Hydra/Helper/BuildDiff.pm
@@ -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;
\ No newline at end of file
diff --git a/src/root/jobset-eval.tt b/src/root/jobset-eval.tt
index 25315c66..8a8d92e4 100644
--- a/src/root/jobset-eval.tt
+++ b/src/root/jobset-eval.tt
@@ -51,7 +51,7 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
[% IF unfinished.size > 0 %]
Cancel all scheduled builds
[% END %]
- [% IF aborted.size > 0 || stillFail.size > 0 || nowFail.size > 0 %]
+ [% IF aborted.size > 0 || stillFail.size > 0 || nowFail.size > 0 || failed.size > 0 %]
Restart all failed builds
[% END %]
[% IF aborted.size > 0 %]
diff --git a/t/Controller/JobsetEval/fetch.t b/t/Controller/JobsetEval/fetch.t
new file mode 100644
index 00000000..b129de75
--- /dev/null
+++ b/t/Controller/JobsetEval/fetch.t
@@ -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;
diff --git a/t/Helper/BuildDiff.t b/t/Helper/BuildDiff.t
new file mode 100644
index 00000000..243bb596
--- /dev/null
+++ b/t/Helper/BuildDiff.t
@@ -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;