Tests: restructure to more closely mirror the sources
t/ had lots of directories and files mirroring src/lib/Hydra. This moves those files under t/Hydra
This commit is contained in:
88
t/Hydra/Config/hydra-notify.t
Normal file
88
t/Hydra/Config/hydra-notify.t
Normal file
@ -0,0 +1,88 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(hydra_config => q|
|
||||
<hydra_notify>
|
||||
<prometheus>
|
||||
listen_address = 127.0.0.1
|
||||
port = 9199
|
||||
</prometheus>
|
||||
</hydra_notify>
|
||||
|);
|
||||
|
||||
require Hydra::Helper::Nix;
|
||||
use Test2::V0;
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig(Hydra::Helper::Nix::getHydraConfig()), {
|
||||
'listen_address' => "127.0.0.1",
|
||||
'port' => 9199
|
||||
}, "Reading specific configuration from the hydra.conf works");
|
||||
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => ":)"
|
||||
}), undef, "Invalid (hydra_notify is a string) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => []
|
||||
}), undef, "Invalid (hydra_notify is a list) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {}
|
||||
}), undef, "Invalid (hydra_notify is an empty hash) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {
|
||||
"prometheus" => ":)"
|
||||
}
|
||||
}), undef, "Invalid (hydra_notify.prometheus is a string) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {
|
||||
"prometheus" => {}
|
||||
}
|
||||
}), undef, "Invalid (hydra_notify.prometheus is an empty hash) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {
|
||||
"prometheus" => {
|
||||
"listen_address" => "0.0.0.0"
|
||||
}
|
||||
}
|
||||
}), undef, "Invalid (hydra_notify.prometheus.port is missing) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {
|
||||
"prometheus" => {
|
||||
"port" => 1234
|
||||
}
|
||||
}
|
||||
}), undef, "Invalid (hydra_notify.prometheus.listen_address is missing) configuration options are undef");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {
|
||||
"prometheus" => {
|
||||
"listen_address" => "127.0.0.1",
|
||||
"port" => 1234
|
||||
}
|
||||
}
|
||||
}), {
|
||||
"listen_address" => "127.0.0.1",
|
||||
"port" => 1234
|
||||
}, "Fully specified hydra_notify.prometheus config is valid and returned");
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraNotifyPrometheusConfig({
|
||||
"hydra_notify" => {
|
||||
"prometheus" => {
|
||||
"listen_address" => "127.0.0.1",
|
||||
"port" => 1234,
|
||||
"extra_keys" => "meh",
|
||||
}
|
||||
}
|
||||
}), {
|
||||
"listen_address" => "127.0.0.1",
|
||||
"port" => 1234
|
||||
}, "extra configuration in hydra_notify.prometheus is not returned");
|
||||
|
||||
done_testing;
|
27
t/Hydra/Config/include.t
Normal file
27
t/Hydra/Config/include.t
Normal file
@ -0,0 +1,27 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(
|
||||
use_external_destination_store => 0,
|
||||
hydra_config => "include foo.conf"
|
||||
);
|
||||
|
||||
write_file($ctx{'tmpdir'} . "/foo.conf", q|
|
||||
<foo>
|
||||
include bar.conf
|
||||
</foo>
|
||||
|);
|
||||
|
||||
write_file($ctx{'tmpdir'} . "/bar.conf", q|
|
||||
bar = baz
|
||||
|);
|
||||
|
||||
require Hydra::Helper::Nix;
|
||||
use Test2::V0;
|
||||
|
||||
is(Hydra::Helper::Nix::getHydraConfig(), {
|
||||
foo => { bar => "baz" }
|
||||
}, "Nested includes work.");
|
||||
|
||||
done_testing;
|
62
t/Hydra/Config/statsd.t
Normal file
62
t/Hydra/Config/statsd.t
Normal file
@ -0,0 +1,62 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(hydra_config => q|
|
||||
<statsd>
|
||||
host = foo.bar
|
||||
port = 18125
|
||||
</statsd>
|
||||
|);
|
||||
|
||||
require Hydra::Helper::Nix;
|
||||
use Test2::V0;
|
||||
|
||||
is(Hydra::Helper::Nix::getStatsdConfig(Hydra::Helper::Nix::getHydraConfig()), {
|
||||
'host' => "foo.bar",
|
||||
'port' => 18125
|
||||
}, "Reading specific configuration from the hydra.conf works");
|
||||
|
||||
is(Hydra::Helper::Nix::getStatsdConfig(), {
|
||||
'host' => "localhost",
|
||||
'port' => 8125
|
||||
}, "A totally empty configuration yields default options");
|
||||
|
||||
is(Hydra::Helper::Nix::getStatsdConfig({
|
||||
"statsd" => {
|
||||
|
||||
}
|
||||
}), {
|
||||
'host' => "localhost",
|
||||
'port' => 8125
|
||||
}, "A empty statsd block yields default options");
|
||||
|
||||
is(Hydra::Helper::Nix::getStatsdConfig({
|
||||
"statsd" => {
|
||||
'host' => "statsdhost"
|
||||
}
|
||||
}), {
|
||||
'host' => "statsdhost",
|
||||
'port' => 8125
|
||||
}, "An overridden statsd host propogates, but the other defaults are returned");
|
||||
|
||||
is(Hydra::Helper::Nix::getStatsdConfig({
|
||||
"statsd" => {
|
||||
'port' => 5218
|
||||
}
|
||||
}), {
|
||||
'host' => "localhost",
|
||||
'port' => 5218
|
||||
}, "An overridden statsd port propogates, but the other defaults are returned");
|
||||
|
||||
is(Hydra::Helper::Nix::getStatsdConfig({
|
||||
"statsd" => {
|
||||
'host' => 'my.statsd.host',
|
||||
'port' => 5218
|
||||
}
|
||||
}), {
|
||||
'host' => "my.statsd.host",
|
||||
'port' => 5218
|
||||
}, "An overridden statsd port and host propogate");
|
||||
|
||||
done_testing;
|
133
t/Hydra/Controller/Admin/clear-queue-non-current.t
Normal file
133
t/Hydra/Controller/Admin/clear-queue-non-current.t
Normal file
@ -0,0 +1,133 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use JSON::MaybeXS qw(decode_json encode_json);
|
||||
use File::Copy;
|
||||
|
||||
my %ctx = test_init(
|
||||
hydra_config => q|
|
||||
# No caching for PathInput plugin, otherwise we get wrong values
|
||||
# (as it has a 30s window where no changes to the file are considered).
|
||||
path_input_cache_validity_seconds = 0
|
||||
|
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => 'tests', displayname => 'Tests', owner => 'alice'});
|
||||
|
||||
my $scratchdir = $ctx{tmpdir} . "/scratch";
|
||||
my $jobset = createBaseJobset("basic", "default.nix", $scratchdir);
|
||||
|
||||
subtest "Create and evaluate our job at version 1" => sub {
|
||||
mkdir $scratchdir or die "mkdir($scratchdir): $!\n";
|
||||
|
||||
# Note: this recreates the raw derivation and skips
|
||||
# the generated config.nix because we never actually
|
||||
# build anything.
|
||||
open(my $fh, ">", "$scratchdir/default.nix");
|
||||
print $fh <<EOF;
|
||||
{
|
||||
example = derivation {
|
||||
builder = "./builder.sh";
|
||||
name = "example";
|
||||
system = builtins.currentSystem;
|
||||
version = 1;
|
||||
};
|
||||
}
|
||||
EOF
|
||||
close($fh);
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating our default.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating our default.nix should result in 1 builds");
|
||||
};
|
||||
|
||||
subtest "Update and evaluate our job to version 2" => sub {
|
||||
open(my $fh, ">", "$scratchdir/default.nix");
|
||||
print $fh <<EOF;
|
||||
{
|
||||
example = derivation {
|
||||
builder = "./builder.sh";
|
||||
name = "example";
|
||||
system = builtins.currentSystem;
|
||||
version = 2;
|
||||
};
|
||||
}
|
||||
EOF
|
||||
close($fh);
|
||||
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating our default.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 2, "Evaluating our default.nix should result in 1 more build, resulting in 2 queued builds");
|
||||
};
|
||||
|
||||
my ($firstBuild, $secondBuild, @builds) = queuedBuildsForJobset($jobset)->search(
|
||||
{},
|
||||
{ order_by => { -asc => 'id' }}
|
||||
);
|
||||
subtest "Validating the first build" => sub {
|
||||
isnt($firstBuild, undef, "We have our first build");
|
||||
is($firstBuild->id, 1, "The first build is ID 1");
|
||||
is($firstBuild->finished, 0, "The first build is not yet finished");
|
||||
is($firstBuild->buildstatus, undef, "The first build status is null");
|
||||
};
|
||||
|
||||
subtest "Validating the second build" => sub {
|
||||
isnt($secondBuild, undef, "We have our second build");
|
||||
is($secondBuild->id, 2, "The second build is ID 2");
|
||||
is($secondBuild->finished, 0, "The second build is not yet finished");
|
||||
is($secondBuild->buildstatus, undef, "The second build status is null");
|
||||
};
|
||||
|
||||
is(@builds, 0, "No other builds were created");
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302, "Logging in gets a 302");
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
subtest 'Cancel queued, non-current builds' => sub {
|
||||
my $restart = request(PUT '/admin/clear-queue-non-current',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Referer => '/admin/example-referer',
|
||||
Cookie => $cookie,
|
||||
);
|
||||
is($restart->code, 302, "Canceling 302's back to the build");
|
||||
is($restart->header("location"), "/admin/example-referer", "We're redirected back to the referer");
|
||||
};
|
||||
|
||||
subtest "Validating the first build is canceled" => sub {
|
||||
my $build = $db->resultset('Builds')->find($firstBuild->id);
|
||||
is($build->finished, 1, "Build should be 'finished'.");
|
||||
is($build->buildstatus, 4, "Build should be canceled.");
|
||||
};
|
||||
|
||||
subtest "Validating the second build is not canceled" => sub {
|
||||
my $build = $db->resultset('Builds')->find($secondBuild->id);
|
||||
is($build->finished, 0, "Build should be unfinished.");
|
||||
is($build->buildstatus, undef, "Build status should be null.");
|
||||
};
|
||||
|
||||
done_testing;
|
67
t/Hydra/Controller/Build/cancel.t
Normal file
67
t/Hydra/Controller/Build/cancel.t
Normal file
@ -0,0 +1,67 @@
|
||||
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);
|
||||
|
||||
# This test verifies that creating, reading, updating, and deleting a jobset via
|
||||
# the HTTP API works as expected.
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => 'tests', displayname => 'Tests', owner => 'alice'});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
my ($build, @builds) = queuedBuildsForJobset($jobset);
|
||||
is($build->finished, 0, "Unbuilt build should not be finished.");
|
||||
is($build->buildstatus, undef, "Unbuilt build should be undefined.");
|
||||
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302, "Logging in gets a 302");
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
|
||||
subtest 'Cancel the build' => sub {
|
||||
my $restart = request(PUT '/build/' . $build->id . '/cancel',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
);
|
||||
is($restart->code, 302, "Restarting 302's back to the build");
|
||||
is($restart->header("location"), "http://localhost/build/" . $build->id);
|
||||
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build 'fails' from jobs/basic.nix should be 'finished'.");
|
||||
is($newbuild->buildstatus, 4, "Build 'fails' from jobs/basic.nix should be canceled.");
|
||||
};
|
||||
|
||||
done_testing;
|
47
t/Hydra/Controller/Build/constituents.t
Normal file
47
t/Hydra/Controller/Build/constituents.t
Normal file
@ -0,0 +1,47 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use JSON::MaybeXS qw(decode_json encode_json);
|
||||
use Data::Dumper;
|
||||
use URI;
|
||||
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;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
my $jobset = createBaseJobset("aggregate", "aggregate.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/aggregate.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/aggregate.nix should result in 3 builds");
|
||||
for my $build (queuedBuildsForJobset($jobset)) {
|
||||
ok(runBuild($build), "Build '".$build->job."' from jobs/aggregate.nix should exit with return code 0");
|
||||
}
|
||||
|
||||
my $build_redirect = request(GET '/job/tests/aggregate/aggregate/latest-finished');
|
||||
|
||||
my $url = URI->new($build_redirect->header('location'))->path . "/constituents";
|
||||
my $constituents = request(GET $url,
|
||||
Accept => 'application/json',
|
||||
);
|
||||
|
||||
ok($constituents->is_success, "Getting the constituent builds");
|
||||
my $data = decode_json($constituents->content);
|
||||
|
||||
my ($buildA) = grep { $_->{nixname} eq "empty-dir-a" } @$data;
|
||||
my ($buildB) = grep { $_->{nixname} eq "empty-dir-b" } @$data;
|
||||
|
||||
is($buildA->{job}, "a");
|
||||
is($buildB->{job}, "b");
|
||||
|
||||
done_testing;
|
34
t/Hydra/Controller/Build/evals.t
Normal file
34
t/Hydra/Controller/Build/evals.t
Normal file
@ -0,0 +1,34 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
use HTTP::Request::Common;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
my ($build, @builds) = queuedBuildsForJobset($jobset);
|
||||
|
||||
ok(runBuild($build), "Build '".$build->job."' from jobs/basic.nix should exit with return code 0");
|
||||
|
||||
subtest "/build/ID/evals" => sub {
|
||||
my $evals = request(GET '/build/' . $build->id . '/evals');
|
||||
ok($evals->is_success, "The page listing evaluations this build is part of returns 200.");
|
||||
};
|
||||
|
||||
done_testing;
|
76
t/Hydra/Controller/Build/restart.t
Normal file
76
t/Hydra/Controller/Build/restart.t
Normal file
@ -0,0 +1,76 @@
|
||||
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);
|
||||
|
||||
# This test verifies that creating, reading, updating, and deleting a jobset via
|
||||
# the HTTP API works as expected.
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => 'tests', displayname => 'Tests', owner => 'alice'});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
my $failing;
|
||||
for my $build (queuedBuildsForJobset($jobset)) {
|
||||
ok(runBuild($build), "Build '".$build->job."' from jobs/basic.nix should exit with return code 0");
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build '".$build->job."' from jobs/basic.nix should be finished.");
|
||||
|
||||
if ($build->job eq "fails") {
|
||||
is($newbuild->buildstatus, 1, "Build 'fails' from jobs/basic.nix should have buildstatus 1.");
|
||||
$failing = $build;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
isnt($failing, undef, "We should have the failing build to restart");
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302, "Logging in gets a 302");
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
|
||||
subtest 'Restart the failing build' => sub {
|
||||
my $restart = request(PUT '/build/' . $failing->id . '/restart',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
);
|
||||
is($restart->code, 302, "Restarting 302's back to the build");
|
||||
is($restart->header("location"), "http://localhost/build/" . $failing->id);
|
||||
|
||||
my $newbuild = $db->resultset('Builds')->find($failing->id);
|
||||
is($newbuild->finished, 0, "Build 'fails' from jobs/basic.nix should not be finished.");
|
||||
};
|
||||
|
||||
done_testing;
|
65
t/Hydra/Controller/Jobset/channel.t
Normal file
65
t/Hydra/Controller/Jobset/channel.t
Normal file
@ -0,0 +1,65 @@
|
||||
use feature 'unicode_strings';
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use IO::Uncompress::Bunzip2 qw(bunzip2);
|
||||
use Archive::Tar;
|
||||
use JSON::MaybeXS qw(decode_json);
|
||||
use Data::Dumper;
|
||||
my %ctx = test_init(
|
||||
use_external_destination_store => 0
|
||||
);
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
# Most basic test case, no parameters
|
||||
my $jobset = createBaseJobset("nested-attributes", "nested-attributes.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset));
|
||||
is(nrQueuedBuildsForJobset($jobset), 4);
|
||||
|
||||
for my $build (queuedBuildsForJobset($jobset)) {
|
||||
ok(runBuild($build), "Build '".$build->job."' should exit with return code 0");
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build '".$build->job."' should be finished.");
|
||||
is($newbuild->buildstatus, 0, "Build '".$build->job."' should have buildstatus 0.");
|
||||
}
|
||||
|
||||
my $compressed = get('/jobset/tests/nested-attributes/channel/latest/nixexprs.tar.bz2');
|
||||
my $tarcontent;
|
||||
bunzip2(\$compressed => \$tarcontent);
|
||||
open(my $tarfh, "<", \$tarcontent);
|
||||
my $tar = Archive::Tar->new($tarfh);
|
||||
|
||||
my $defaultnix = $ctx{"tmpdir"} . "/channel-default.nix";
|
||||
$tar->extract_file("channel/default.nix", $defaultnix);
|
||||
|
||||
print STDERR $tar->get_content("channel/default.nix");
|
||||
|
||||
(my $status, my $stdout, my $stderr) = Hydra::Helper::Nix::captureStdoutStderr(5, "nix-env", "--json", "--query", "--available", "--attr-path", "--file", $defaultnix);
|
||||
is($stderr, "", "Stderr should be empty");
|
||||
is($status, 0, "Querying the packages should succeed");
|
||||
|
||||
my $packages = decode_json($stdout);
|
||||
my $keys = [sort keys %$packages];
|
||||
is($keys, [
|
||||
"packageset-nested",
|
||||
"packageset.deeper.deeper.nested",
|
||||
"packageset.nested",
|
||||
"packageset.nested2",
|
||||
]);
|
||||
is($packages->{"packageset-nested"}->{"name"}, "actually-top-level");
|
||||
is($packages->{"packageset.nested"}->{"name"}, "actually-nested");
|
||||
|
||||
done_testing;
|
35
t/Hydra/Controller/Jobset/evals.t
Normal file
35
t/Hydra/Controller/Jobset/evals.t
Normal file
@ -0,0 +1,35 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
use HTTP::Request::Common;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
|
||||
subtest "/jobset/PROJECT/JOBSET" => sub {
|
||||
my $jobset = request(GET '/jobset/' . $project->name . '/' . $jobset->name);
|
||||
ok($jobset->is_success, "The page showing the jobset returns 200.");
|
||||
};
|
||||
|
||||
subtest "/jobset/PROJECT/JOBSET/evals" => sub {
|
||||
my $jobsetevals = request(GET '/jobset/' . $project->name . '/' . $jobset->name . '/evals');
|
||||
ok($jobsetevals->is_success, "The page showing the jobset evals returns 200.");
|
||||
};
|
||||
|
||||
done_testing;
|
205
t/Hydra/Controller/Jobset/http.t
Normal file
205
t/Hydra/Controller/Jobset/http.t
Normal file
@ -0,0 +1,205 @@
|
||||
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);
|
||||
|
||||
# This test verifies that creating, reading, updating, and deleting a jobset via
|
||||
# the HTTP API works as expected.
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => 'tests', displayname => 'Tests', owner => 'alice'});
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302);
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
|
||||
subtest 'Create new jobset "job" as flake type' => sub {
|
||||
my $jobsetcreate = request(PUT '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
Content => encode_json({
|
||||
enabled => 2,
|
||||
visible => JSON::MaybeXS::true,
|
||||
name => "job",
|
||||
type => 1,
|
||||
description => "test jobset",
|
||||
flake => "github:nixos/nix",
|
||||
checkinterval => 0,
|
||||
schedulingshares => 100,
|
||||
keepnr => 3
|
||||
})
|
||||
);
|
||||
ok($jobsetcreate->is_success);
|
||||
is($jobsetcreate->header("location"), "http://localhost/jobset/tests/job");
|
||||
};
|
||||
|
||||
|
||||
subtest 'Read newly-created jobset "job"' => sub {
|
||||
my $jobsetinfo = request(GET '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
);
|
||||
ok($jobsetinfo->is_success);
|
||||
is(decode_json($jobsetinfo->content), {
|
||||
checkinterval => 0,
|
||||
description => "test jobset",
|
||||
emailoverride => "",
|
||||
enabled => 2,
|
||||
enableemail => JSON::MaybeXS::false,
|
||||
errortime => undef,
|
||||
errormsg => "",
|
||||
fetcherrormsg => "",
|
||||
flake => "github:nixos/nix",
|
||||
visible => JSON::MaybeXS::true,
|
||||
inputs => {},
|
||||
keepnr => 3,
|
||||
lastcheckedtime => undef,
|
||||
name => "job",
|
||||
nixexprinput => "",
|
||||
nixexprpath => "",
|
||||
project => "tests",
|
||||
schedulingshares => 100,
|
||||
starttime => undef,
|
||||
triggertime => undef,
|
||||
type => 1
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
subtest 'Update jobset "job" to legacy type' => sub {
|
||||
my $jobsetupdate = request(PUT '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
Content => encode_json({
|
||||
enabled => 3,
|
||||
visible => JSON::MaybeXS::true,
|
||||
name => "job",
|
||||
type => 0,
|
||||
nixexprinput => "ofborg",
|
||||
nixexprpath => "release.nix",
|
||||
inputs => {
|
||||
ofborg => {
|
||||
name => "ofborg",
|
||||
type => "git",
|
||||
value => "https://github.com/NixOS/ofborg.git released"
|
||||
}
|
||||
},
|
||||
description => "test jobset",
|
||||
checkinterval => 0,
|
||||
schedulingshares => 50,
|
||||
keepnr => 1
|
||||
})
|
||||
);
|
||||
ok($jobsetupdate->is_success);
|
||||
|
||||
# Read newly-updated jobset "job"
|
||||
my $jobsetinfo = request(GET '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
);
|
||||
ok($jobsetinfo->is_success);
|
||||
is(decode_json($jobsetinfo->content), {
|
||||
checkinterval => 0,
|
||||
description => "test jobset",
|
||||
emailoverride => "",
|
||||
enabled => 3,
|
||||
enableemail => JSON::MaybeXS::false,
|
||||
errortime => undef,
|
||||
errormsg => "",
|
||||
fetcherrormsg => "",
|
||||
flake => "",
|
||||
visible => JSON::MaybeXS::true,
|
||||
inputs => {
|
||||
ofborg => {
|
||||
name => "ofborg",
|
||||
type => "git",
|
||||
emailresponsible => JSON::MaybeXS::false,
|
||||
value => "https://github.com/NixOS/ofborg.git released"
|
||||
}
|
||||
},
|
||||
keepnr => 1,
|
||||
lastcheckedtime => undef,
|
||||
name => "job",
|
||||
nixexprinput => "ofborg",
|
||||
nixexprpath => "release.nix",
|
||||
project => "tests",
|
||||
schedulingshares => 50,
|
||||
starttime => undef,
|
||||
triggertime => undef,
|
||||
type => 0
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
subtest 'Update jobset "job" to have an invalid input type' => sub {
|
||||
my $jobsetupdate = request(PUT '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
Content => encode_json({
|
||||
enabled => 3,
|
||||
visible => JSON::MaybeXS::true,
|
||||
name => "job",
|
||||
type => 0,
|
||||
nixexprinput => "ofborg",
|
||||
nixexprpath => "release.nix",
|
||||
inputs => {
|
||||
ofborg => {
|
||||
name => "ofborg",
|
||||
type => "123",
|
||||
value => "https://github.com/NixOS/ofborg.git released"
|
||||
}
|
||||
},
|
||||
description => "test jobset",
|
||||
checkinterval => 0,
|
||||
schedulingshares => 50,
|
||||
keepnr => 1
|
||||
})
|
||||
);
|
||||
ok(!$jobsetupdate->is_success);
|
||||
ok($jobsetupdate->content =~ m/Invalid input type.*valid types:/);
|
||||
};
|
||||
|
||||
|
||||
subtest 'Delete jobset "job"' => sub {
|
||||
my $jobsetinfo = request(DELETE '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
Cookie => $cookie
|
||||
);
|
||||
ok($jobsetinfo->is_success);
|
||||
|
||||
# Jobset "job" should no longer exist.
|
||||
$jobsetinfo = request(GET '/jobset/tests/job',
|
||||
Accept => 'application/json',
|
||||
);
|
||||
ok(!$jobsetinfo->is_success);
|
||||
};
|
||||
|
||||
done_testing;
|
123
t/Hydra/Controller/Jobset/type-constraints.t
Normal file
123
t/Hydra/Controller/Jobset/type-constraints.t
Normal file
@ -0,0 +1,123 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Data::Dumper;
|
||||
use Test2::V0;
|
||||
use Test2::Compare qw(compare strict_convert);
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# This test checks a matrix of jobset configuration options for constraint violations.
|
||||
|
||||
my @types = ( 0, 1, 2 );
|
||||
my @nixexprinputs = ( undef, "input" );
|
||||
my @nixexprpaths = ( undef, "path" );
|
||||
my @flakes = ( undef, "flake" );
|
||||
|
||||
my @expected_failing;
|
||||
my @expected_succeeding = (
|
||||
{
|
||||
"name" => "test",
|
||||
"emailoverride" => "",
|
||||
"type" => 0,
|
||||
"nixexprinput" => "input",
|
||||
"nixexprpath" => "path",
|
||||
"flake" => undef,
|
||||
},
|
||||
{
|
||||
"name" => "test",
|
||||
"emailoverride" => "",
|
||||
"type" => 1,
|
||||
"nixexprinput" => undef,
|
||||
"nixexprpath" => undef,
|
||||
"flake" => "flake",
|
||||
},
|
||||
);
|
||||
|
||||
# Checks if two Perl hashes (in scalar context) contain the same data.
|
||||
# Returns 0 if they are different and 1 if they are the same.
|
||||
sub test_scenario_matches {
|
||||
my ($first, $second) = @_;
|
||||
|
||||
my $ret = compare($first, $second, \&strict_convert);
|
||||
|
||||
if (defined $ret == 1) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
# Construct a matrix of parameters that should violate the Jobsets table's constraints.
|
||||
foreach my $type (@types) {
|
||||
foreach my $nixexprinput (@nixexprinputs) {
|
||||
foreach my $nixexprpath (@nixexprpaths) {
|
||||
foreach my $flake (@flakes) {
|
||||
my $hash = {
|
||||
"name" => "test",
|
||||
"emailoverride" => "",
|
||||
"type" => $type,
|
||||
"nixexprinput" => $nixexprinput,
|
||||
"nixexprpath" => $nixexprpath,
|
||||
"flake" => $flake,
|
||||
};
|
||||
|
||||
push(@expected_failing, $hash) if (!grep { test_scenario_matches($_, $hash) } @expected_succeeding);
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
# Validate that the list of parameters that should fail the constraints do indeed fail.
|
||||
subtest "Expected constraint failures" => sub {
|
||||
my $count = 1;
|
||||
foreach my $case (@expected_failing) {
|
||||
subtest "Case $count: " . Dumper ($case) => sub {
|
||||
dies {
|
||||
# Necessary, otherwise cases will fail because the `->create`
|
||||
# will throw an exception due to an expected constraint failure
|
||||
# (which will cause the `ok()` to be skipped, leading to no
|
||||
# assertions in the subtest).
|
||||
is(1, 1);
|
||||
|
||||
ok(
|
||||
!$project->jobsets->create($case),
|
||||
"Expected jobset to violate constraints"
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
$count++;
|
||||
};
|
||||
};
|
||||
|
||||
# Validate that the list of parameters that should not fail the constraints do indeed succeed.
|
||||
subtest "Expected constraint successes" => sub {
|
||||
my $count = 1;
|
||||
foreach my $case (@expected_succeeding) {
|
||||
subtest "Case $count: " . Dumper ($case) => sub {
|
||||
my $jobset = $project->jobsets->create($case);
|
||||
|
||||
ok(
|
||||
$jobset,
|
||||
"Expected jobset to not violate constraints"
|
||||
);
|
||||
|
||||
# Delete the jobset so the next jobset won't violate the name constraint.
|
||||
$jobset->delete;
|
||||
};
|
||||
|
||||
$count++;
|
||||
};
|
||||
};
|
||||
|
||||
done_testing;
|
67
t/Hydra/Controller/JobsetEval/cancel.t
Normal file
67
t/Hydra/Controller/JobsetEval/cancel.t
Normal file
@ -0,0 +1,67 @@
|
||||
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);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => 'tests', displayname => 'Tests', owner => 'alice'});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
my ($eval, @evals) = $jobset->jobsetevals;
|
||||
isnt($eval, undef, "We have an evaluation to restart");
|
||||
|
||||
my ($build, @builds) = queuedBuildsForJobset($jobset);
|
||||
is($build->finished, 0, "Unbuilt build should not be finished.");
|
||||
is($build->buildstatus, undef, "Unbuilt build should be undefined.");
|
||||
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302, "Logging in gets a 302");
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
|
||||
subtest 'Cancel the JobsetEval builds' => sub {
|
||||
my $restart = request(PUT '/eval/' . $eval->id . '/cancel',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
);
|
||||
is($restart->code, 302, "Canceling 302's back to the build");
|
||||
is($restart->header("location"), "http://localhost/eval/" . $eval->id, "We're redirected back to the eval page");
|
||||
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build 'fails' from jobs/basic.nix should be 'finished'.");
|
||||
is($newbuild->buildstatus, 4, "Build 'fails' from jobs/basic.nix should be canceled.");
|
||||
};
|
||||
|
||||
done_testing;
|
100
t/Hydra/Controller/JobsetEval/restart.t
Normal file
100
t/Hydra/Controller/JobsetEval/restart.t
Normal file
@ -0,0 +1,100 @@
|
||||
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);
|
||||
|
||||
# This test verifies that creating, reading, updating, and deleting a jobset via
|
||||
# the HTTP API works as expected.
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => 'tests', displayname => 'Tests', owner => 'alice'});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
my ($eval, @evals) = $jobset->jobsetevals;
|
||||
my ($abortedBuild, $failedBuild, @builds) = queuedBuildsForJobset($jobset);
|
||||
|
||||
isnt($eval, undef, "We have an evaluation to restart");
|
||||
|
||||
# Make the build be aborted
|
||||
isnt($abortedBuild, undef, "We should have the aborted build to restart");
|
||||
$abortedBuild->update({
|
||||
finished => 1,
|
||||
buildstatus => 3,
|
||||
stoptime => 1,
|
||||
starttime => 1,
|
||||
});
|
||||
|
||||
# Make the build be failed
|
||||
isnt($failedBuild, undef, "We should have the failed build to restart");
|
||||
$failedBuild->update({
|
||||
finished => 1,
|
||||
buildstatus => 5,
|
||||
stoptime => 1,
|
||||
starttime => 1,
|
||||
});
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302, "Logging in gets a 302");
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
|
||||
subtest 'Restart all aborted JobsetEval builds' => sub {
|
||||
my $restart = request(PUT '/eval/' . $eval->id . '/restart-aborted',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
);
|
||||
is($restart->code, 302, "Restarting 302's back to the build");
|
||||
is($restart->header("location"), "http://localhost/eval/" . $eval->id);
|
||||
|
||||
my $newAbortedBuild = $db->resultset('Builds')->find($abortedBuild->id);
|
||||
is($newAbortedBuild->finished, 0, "The aborted build is no longer finished");
|
||||
|
||||
my $newFailedBuild = $db->resultset('Builds')->find($failedBuild->id);
|
||||
is($newFailedBuild->finished, 1, "The failed build is still finished");
|
||||
};
|
||||
|
||||
subtest 'Restart all failed JobsetEval builds' => sub {
|
||||
my $restart = request(PUT '/eval/' . $eval->id . '/restart-failed',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
);
|
||||
is($restart->code, 302, "Restarting 302's back to the build");
|
||||
is($restart->header("location"), "http://localhost/eval/" . $eval->id);
|
||||
|
||||
my $newFailedBuild = $db->resultset('Builds')->find($failedBuild->id);
|
||||
is($newFailedBuild->finished, 0, "The failed build is no longer finished");
|
||||
};
|
||||
|
||||
done_testing;
|
30
t/Hydra/Controller/Root/evals.t
Normal file
30
t/Hydra/Controller/Root/evals.t
Normal file
@ -0,0 +1,30 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
use HTTP::Request::Common;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
|
||||
subtest "/evals" => sub {
|
||||
my $global = request(GET '/evals');
|
||||
ok($global->is_success, "The page showing the all evals returns 200.");
|
||||
};
|
||||
|
||||
done_testing;
|
48
t/Hydra/Controller/Root/narinfo.t
Normal file
48
t/Hydra/Controller/Root/narinfo.t
Normal file
@ -0,0 +1,48 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
use JSON::MaybeXS qw(decode_json);
|
||||
my %ctx = test_init(
|
||||
# Without this, the test will fail because a `file:` store is not treated as a
|
||||
# local store by `isLocalStore` in src/lib/Hydra/Helper/Nix.pm, and any
|
||||
# requests to /HASH.narinfo will fail.
|
||||
use_external_destination_store => 0
|
||||
);
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
use HTTP::Request::Common;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
for my $build (queuedBuildsForJobset($jobset)) {
|
||||
ok(runBuild($build), "Build '".$build->job."' from jobs/basic.nix should exit with return code 0");
|
||||
}
|
||||
|
||||
subtest "/HASH.narinfo" => sub {
|
||||
my $build_redirect = request(GET '/job/tests/basic/empty_dir/latest-finished');
|
||||
my $url = URI->new($build_redirect->header('location'))->path;
|
||||
my $json = request(GET $url, Accept => 'application/json');
|
||||
my $data = decode_json($json->content);
|
||||
my $outpath = $data->{buildoutputs}{out}{path};
|
||||
my ($hash) = $outpath =~ qr{/nix/store/([a-z0-9]{32}).*};
|
||||
my $narinfo_response = request(GET "/$hash.narinfo");
|
||||
ok($narinfo_response->is_success, "Getting the narinfo of a build");
|
||||
|
||||
my ($storepath) = $narinfo_response->content =~ qr{StorePath: (.*)};
|
||||
is($storepath, $outpath, "The returned store path is the same as the out path")
|
||||
};
|
||||
|
||||
done_testing;
|
30
t/Hydra/Controller/Root/queue-runner-status.t
Normal file
30
t/Hydra/Controller/Root/queue-runner-status.t
Normal file
@ -0,0 +1,30 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
use HTTP::Request::Common;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
|
||||
subtest "/queue-runner-status" => sub {
|
||||
my $global = request(GET '/queue-runner-status');
|
||||
ok($global->is_success, "The page showing the the queue runner status 200's.");
|
||||
};
|
||||
|
||||
done_testing;
|
31
t/Hydra/Controller/metrics.t
Normal file
31
t/Hydra/Controller/metrics.t
Normal file
@ -0,0 +1,31 @@
|
||||
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 HTTP::Request::Common;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
request(GET '/');
|
||||
my $metrics = request(GET '/metrics');
|
||||
ok($metrics->is_success);
|
||||
|
||||
like(
|
||||
$metrics->content,
|
||||
qr/http_requests_total\{action="index",code="200",controller="Hydra::Controller::Root",method="GET"\} 1/,
|
||||
"Metrics are collected"
|
||||
);
|
||||
|
||||
done_testing;
|
140
t/Hydra/Controller/projects.t
Normal file
140
t/Hydra/Controller/projects.t
Normal file
@ -0,0 +1,140 @@
|
||||
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 HTTP::Request::Common;
|
||||
|
||||
use Test2::V0;
|
||||
require Catalyst::Test;
|
||||
Catalyst::Test->import('Hydra');
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# Create a user to log in to
|
||||
my $user = $db->resultset('Users')->create({ username => 'alice', emailaddress => 'root@invalid.org', password => '!' });
|
||||
$user->setPassword('foobar');
|
||||
$user->userroles->update_or_create({ role => 'admin' });
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "Tests", owner => "root"});
|
||||
|
||||
# Login and save cookie for future requests
|
||||
my $req = request(POST '/login',
|
||||
Referer => 'http://localhost/',
|
||||
Content => {
|
||||
username => 'alice',
|
||||
password => 'foobar'
|
||||
}
|
||||
);
|
||||
is($req->code, 302);
|
||||
my $cookie = $req->header("set-cookie");
|
||||
|
||||
subtest "Read project 'tests'" => sub {
|
||||
my $projectinfo = request(GET '/project/tests',
|
||||
Accept => 'application/json',
|
||||
);
|
||||
|
||||
ok($projectinfo->is_success);
|
||||
is(decode_json($projectinfo->content), {
|
||||
description => "",
|
||||
displayname => "Tests",
|
||||
enabled => JSON::MaybeXS::true,
|
||||
hidden => JSON::MaybeXS::false,
|
||||
homepage => "",
|
||||
jobsets => [],
|
||||
name => "tests",
|
||||
owner => "root"
|
||||
});
|
||||
};
|
||||
|
||||
subtest "Transitioning from declarative project to normal" => sub {
|
||||
subtest "Make project declarative" => sub {
|
||||
my $projectupdate = request(PUT '/project/tests',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
Content => encode_json({
|
||||
enabled => JSON::MaybeXS::true,
|
||||
visible => JSON::MaybeXS::true,
|
||||
name => "tests",
|
||||
displayname => "Tests",
|
||||
declarative => {
|
||||
file => "bogus",
|
||||
type => "boolean",
|
||||
value => "false"
|
||||
}
|
||||
})
|
||||
);
|
||||
ok($projectupdate->is_success);
|
||||
};
|
||||
|
||||
subtest "Project has '.jobsets' jobset" => sub {
|
||||
my $projectinfo = request(GET '/project/tests',
|
||||
Accept => 'application/json',
|
||||
);
|
||||
|
||||
ok($projectinfo->is_success);
|
||||
is(decode_json($projectinfo->content), {
|
||||
description => "",
|
||||
displayname => "Tests",
|
||||
enabled => JSON::MaybeXS::true,
|
||||
hidden => JSON::MaybeXS::false,
|
||||
homepage => "",
|
||||
jobsets => [".jobsets"],
|
||||
name => "tests",
|
||||
owner => "root",
|
||||
declarative => {
|
||||
file => "bogus",
|
||||
type => "boolean",
|
||||
value => "false"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
subtest "Make project normal" => sub {
|
||||
my $projectupdate = request(PUT '/project/tests',
|
||||
Accept => 'application/json',
|
||||
Content_Type => 'application/json',
|
||||
Cookie => $cookie,
|
||||
Content => encode_json({
|
||||
enabled => JSON::MaybeXS::true,
|
||||
visible => JSON::MaybeXS::true,
|
||||
name => "tests",
|
||||
displayname => "Tests",
|
||||
declarative => {
|
||||
file => "",
|
||||
type => "boolean",
|
||||
value => "false"
|
||||
}
|
||||
})
|
||||
);
|
||||
ok($projectupdate->is_success);
|
||||
};
|
||||
|
||||
subtest "Project doesn't have '.jobsets' jobset" => sub {
|
||||
my $projectinfo = request(GET '/project/tests',
|
||||
Accept => 'application/json',
|
||||
);
|
||||
|
||||
ok($projectinfo->is_success);
|
||||
is(decode_json($projectinfo->content), {
|
||||
description => "",
|
||||
displayname => "Tests",
|
||||
enabled => JSON::MaybeXS::true,
|
||||
hidden => JSON::MaybeXS::false,
|
||||
homepage => "",
|
||||
jobsets => [],
|
||||
name => "tests",
|
||||
owner => "root"
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
done_testing;
|
23
t/Hydra/Event.t
Normal file
23
t/Hydra/Event.t
Normal file
@ -0,0 +1,23 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Hydra::Event;
|
||||
|
||||
use Test2::V0;
|
||||
use Test2::Tools::Exception;
|
||||
|
||||
subtest "Event: new event" => sub {
|
||||
my $event = Hydra::Event->new_event("build_started", "19");
|
||||
is($event->{'payload'}, "19");
|
||||
is($event->{'channel_name'}, "build_started");
|
||||
is($event->{'event'}->{'build_id'}, 19);
|
||||
};
|
||||
|
||||
subtest "Payload type: bogus" => sub {
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("bogus", "") },
|
||||
qr/Invalid channel name/,
|
||||
"bogus channel"
|
||||
);
|
||||
};
|
||||
|
||||
done_testing;
|
115
t/Hydra/Event/BuildFinished.t
Normal file
115
t/Hydra/Event/BuildFinished.t
Normal file
@ -0,0 +1,115 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
use Hydra::Event;
|
||||
use Hydra::Event::BuildFinished;
|
||||
|
||||
use Test2::V0;
|
||||
use Test2::Tools::Exception;
|
||||
use Test2::Tools::Mock qw(mock_obj);
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
subtest "Parsing" => sub {
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_finished", "") },
|
||||
qr/at least one argument/,
|
||||
"empty payload"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_finished", "abc123") },
|
||||
qr/should be integers/,
|
||||
"build ID should be an integer"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_finished", "123\tabc123") },
|
||||
qr/should be integers/,
|
||||
"dependent ID should be an integer"
|
||||
);
|
||||
is(
|
||||
Hydra::Event::parse_payload("build_finished", "123"),
|
||||
Hydra::Event::BuildFinished->new(123, []),
|
||||
"no dependent builds"
|
||||
);
|
||||
is(
|
||||
Hydra::Event::parse_payload("build_finished", "123\t456"),
|
||||
Hydra::Event::BuildFinished->new(123, [456]),
|
||||
"one dependent build"
|
||||
);
|
||||
is(
|
||||
Hydra::Event::parse_payload("build_finished", "123\t456\t789\t012\t345"),
|
||||
Hydra::Event::BuildFinished->new(123, [456, 789, 12, 345]),
|
||||
"four dependent builds"
|
||||
);
|
||||
};
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
subtest "interested" => sub {
|
||||
my $event = Hydra::Event::BuildFinished->new(123, []);
|
||||
|
||||
subtest "A plugin which does not implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => ();
|
||||
|
||||
is($event->interestedIn($plugin), 0, "The plugin is not interesting.");
|
||||
};
|
||||
|
||||
subtest "A plugin which does implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"buildFinished" => sub {}
|
||||
]
|
||||
);
|
||||
|
||||
is($event->interestedIn($plugin), 1, "The plugin is interesting.");
|
||||
};
|
||||
};
|
||||
|
||||
subtest "load" => sub {
|
||||
my ($build, $dependent_a, $dependent_b) = $db->resultset('Builds')->search(
|
||||
{ },
|
||||
{ limit => 3 }
|
||||
)->all;
|
||||
|
||||
my $event = Hydra::Event::BuildFinished->new($build->id, [$dependent_a->id, $dependent_b->id]);
|
||||
|
||||
$event->load($db);
|
||||
|
||||
is($event->{"build"}->id, $build->id, "The build record matches.");
|
||||
is($event->{"dependents"}[0]->id, $dependent_a->id, "The dependent_a record matches.");
|
||||
is($event->{"dependents"}[1]->id, $dependent_b->id, "The dependent_b record matches.");
|
||||
|
||||
# Create a fake "plugin" with a buildFinished sub, the sub sets this
|
||||
# global passedBuild and passedDependents variables for verifying.
|
||||
my $passedBuild;
|
||||
my $passedDependents;
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"buildFinished" => sub {
|
||||
my ($self, $build, $dependents) = @_;
|
||||
$passedBuild = $build;
|
||||
$passedDependents = $dependents;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
$event->execute($db, $plugin);
|
||||
|
||||
is($passedBuild->id, $build->id, "The plugin's buildFinished hook is called with a matching build");
|
||||
is($passedDependents->[0]->id, $dependent_a->id, "The plugin's buildFinished hook is called with a matching dependent_a");
|
||||
is($passedDependents->[1]->id, $dependent_b->id, "The plugin's buildFinished hook is called with a matching dependent_b");
|
||||
};
|
||||
|
||||
done_testing;
|
91
t/Hydra/Event/BuildQueued.t
Normal file
91
t/Hydra/Event/BuildQueued.t
Normal file
@ -0,0 +1,91 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Hydra::Event;
|
||||
use Hydra::Event::BuildQueued;
|
||||
use Test2::V0;
|
||||
use Test2::Tools::Exception;
|
||||
use Test2::Tools::Mock qw(mock_obj);
|
||||
|
||||
my $ctx = test_context();
|
||||
|
||||
my $db = $ctx->db();
|
||||
|
||||
my $builds = $ctx->makeAndEvaluateJobset(
|
||||
expression => "basic.nix"
|
||||
);
|
||||
|
||||
subtest "Parsing build_queued" => sub {
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_queued", "") },
|
||||
qr/one argument/,
|
||||
"empty payload"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_queued", "abc123\tabc123") },
|
||||
qr/only one argument/,
|
||||
"two arguments"
|
||||
);
|
||||
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_queued", "abc123") },
|
||||
qr/should be an integer/,
|
||||
"not an integer"
|
||||
);
|
||||
is(
|
||||
Hydra::Event::parse_payload("build_queued", "19"),
|
||||
Hydra::Event::BuildQueued->new(19),
|
||||
"Valid parse"
|
||||
);
|
||||
};
|
||||
|
||||
subtest "interested" => sub {
|
||||
my $event = Hydra::Event::BuildQueued->new(123, []);
|
||||
|
||||
subtest "A plugin which does not implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => ();
|
||||
|
||||
is($event->interestedIn($plugin), 0, "The plugin is not interesting.");
|
||||
};
|
||||
|
||||
subtest "A plugin which does implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"buildQueued" => sub {}
|
||||
]
|
||||
);
|
||||
|
||||
is($event->interestedIn($plugin), 1, "The plugin is interesting.");
|
||||
};
|
||||
};
|
||||
|
||||
subtest "load" => sub {
|
||||
my $build = $builds->{"empty_dir"};
|
||||
|
||||
my $event = Hydra::Event::BuildQueued->new($build->id);
|
||||
|
||||
$event->load($db);
|
||||
|
||||
is($event->{"build"}->id, $build->id, "The build record matches.");
|
||||
|
||||
# Create a fake "plugin" with a buildQueued sub, the sub sets this
|
||||
# global passedBuild variable.
|
||||
my $passedBuild;
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"buildQueued" => sub {
|
||||
my ($self, $build) = @_;
|
||||
$passedBuild = $build;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
$event->execute($db, $plugin);
|
||||
|
||||
is($passedBuild->id, $build->id, "The plugin's buildQueued hook is called with the proper build");
|
||||
};
|
||||
|
||||
done_testing;
|
100
t/Hydra/Event/BuildStarted.t
Normal file
100
t/Hydra/Event/BuildStarted.t
Normal file
@ -0,0 +1,100 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
use Hydra::Event;
|
||||
use Hydra::Event::BuildStarted;
|
||||
|
||||
use Test2::V0;
|
||||
use Test2::Tools::Exception;
|
||||
use Test2::Tools::Mock qw(mock_obj);
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
subtest "Parsing build_started" => sub {
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_started", "") },
|
||||
qr/one argument/,
|
||||
"empty payload"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_started", "abc123\tabc123") },
|
||||
qr/only one argument/,
|
||||
"two arguments"
|
||||
);
|
||||
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("build_started", "abc123") },
|
||||
qr/should be an integer/,
|
||||
"not an integer"
|
||||
);
|
||||
is(
|
||||
Hydra::Event::parse_payload("build_started", "19"),
|
||||
Hydra::Event::BuildStarted->new(19),
|
||||
"Valid parse"
|
||||
);
|
||||
};
|
||||
|
||||
subtest "interested" => sub {
|
||||
my $event = Hydra::Event::BuildStarted->new(123, []);
|
||||
|
||||
subtest "A plugin which does not implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => ();
|
||||
|
||||
is($event->interestedIn($plugin), 0, "The plugin is not interesting.");
|
||||
};
|
||||
|
||||
subtest "A plugin which does implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"buildStarted" => sub {}
|
||||
]
|
||||
);
|
||||
|
||||
is($event->interestedIn($plugin), 1, "The plugin is interesting.");
|
||||
};
|
||||
};
|
||||
|
||||
subtest "load" => sub {
|
||||
my $build = $db->resultset('Builds')->search(
|
||||
{ },
|
||||
{ limit => 1 }
|
||||
)->next;
|
||||
|
||||
my $event = Hydra::Event::BuildStarted->new($build->id);
|
||||
|
||||
$event->load($db);
|
||||
|
||||
is($event->{"build"}->id, $build->id, "The build record matches.");
|
||||
|
||||
# Create a fake "plugin" with a buildStarted sub, the sub sets this
|
||||
# global passedBuild variable.
|
||||
my $passedBuild;
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"buildStarted" => sub {
|
||||
my ($self, $build) = @_;
|
||||
$passedBuild = $build;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
$event->execute($db, $plugin);
|
||||
|
||||
is($passedBuild->id, $build->id, "The plugin's buildStarted hook is called with the proper build");
|
||||
};
|
||||
|
||||
done_testing;
|
124
t/Hydra/Event/StepFinished.t
Normal file
124
t/Hydra/Event/StepFinished.t
Normal file
@ -0,0 +1,124 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
use Hydra::Event;
|
||||
use Hydra::Event::BuildStarted;
|
||||
|
||||
use Test2::V0;
|
||||
use Test2::Tools::Exception;
|
||||
use Test2::Tools::Mock qw(mock_obj);
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
my $jobset = createBaseJobset("basic", "basic.nix", $ctx{jobsdir});
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/basic.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 3, "Evaluating jobs/basic.nix should result in 3 builds");
|
||||
|
||||
for my $build (queuedBuildsForJobset($jobset)) {
|
||||
ok(runBuild($build), "Build '".$build->job."' from jobs/basic.nix should exit with return code 0");
|
||||
}
|
||||
|
||||
|
||||
|
||||
subtest "Parsing step_finished" => sub {
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("step_finished", "") },
|
||||
qr/three arguments/,
|
||||
"empty payload"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("step_finished", "abc123") },
|
||||
qr/three arguments/,
|
||||
"one argument"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("step_finished", "abc123\tabc123") },
|
||||
qr/three arguments/,
|
||||
"two arguments"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("step_finished", "abc123\tabc123\tabc123\tabc123") },
|
||||
qr/three arguments/,
|
||||
"four arguments"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("step_finished", "abc123\t123\t/path/to/log") },
|
||||
qr/should be an integer/,
|
||||
"not an integer: first position"
|
||||
);
|
||||
like(
|
||||
dies { Hydra::Event::parse_payload("step_finished", "123\tabc123\t/path/to/log") },
|
||||
qr/should be an integer/,
|
||||
"not an integer: second argument"
|
||||
);
|
||||
is(
|
||||
Hydra::Event::parse_payload("step_finished", "123\t456\t/path/to/logfile"),
|
||||
Hydra::Event::StepFinished->new(123, 456, "/path/to/logfile")
|
||||
);
|
||||
};
|
||||
|
||||
subtest "interested" => sub {
|
||||
my $event = Hydra::Event::StepFinished->new(123, []);
|
||||
|
||||
subtest "A plugin which does not implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => ();
|
||||
|
||||
is($event->interestedIn($plugin), 0, "The plugin is not interesting.");
|
||||
};
|
||||
|
||||
subtest "A plugin which does implement the API" => sub {
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"stepFinished" => sub {}
|
||||
]
|
||||
);
|
||||
|
||||
is($event->interestedIn($plugin), 1, "The plugin is interesting.");
|
||||
};
|
||||
};
|
||||
|
||||
subtest "load" => sub {
|
||||
|
||||
my $step = $db->resultset('BuildSteps')->search(
|
||||
{ },
|
||||
{ limit => 1 }
|
||||
)->next;
|
||||
my $build = $step->build;
|
||||
|
||||
my $event = Hydra::Event::StepFinished->new($build->id, $step->stepnr, "/foo/bar/baz");
|
||||
|
||||
$event->load($db);
|
||||
is($event->{"step"}->get_column("build"), $build->id, "The build record matches.");
|
||||
|
||||
# Create a fake "plugin" with a stepFinished sub, the sub sets this
|
||||
# "global" passedStep, passedLogPath variables.
|
||||
my $passedStep;
|
||||
my $passedLogPath;
|
||||
my $plugin = {};
|
||||
my $mock = mock_obj $plugin => (
|
||||
add => [
|
||||
"stepFinished" => sub {
|
||||
my ($self, $step, $log_path) = @_;
|
||||
$passedStep = $step;
|
||||
$passedLogPath = $log_path;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
$event->execute($db, $plugin);
|
||||
|
||||
is($passedStep->get_column("build"), $build->id, "The plugin's stepFinished hook is called with a step from the expected build");
|
||||
is($passedStep->stepnr, $step->stepnr, "The plugin's stepFinished hook is called with the proper step of the build");
|
||||
is($passedLogPath, "/foo/bar/baz", "The plugin's stepFinished hook is called with the proper log path");
|
||||
};
|
||||
|
||||
done_testing;
|
28
t/Hydra/Helper/CatalystUtils.t
Normal file
28
t/Hydra/Helper/CatalystUtils.t
Normal file
@ -0,0 +1,28 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Test2::V0;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
|
||||
subtest "trim" => sub {
|
||||
my %values = (
|
||||
"" => "",
|
||||
"🌮" => '🌮',
|
||||
" 🌮" => '🌮',
|
||||
"🌮 " => '🌮',
|
||||
" 🌮 " => '🌮',
|
||||
"\n🌮 " => '🌮',
|
||||
"\n\t🌮\n\n\t" => '🌮',
|
||||
);
|
||||
|
||||
for my $input (keys %values) {
|
||||
my $value = $values{$input};
|
||||
is(trim($input), $value, "Trim the value: " . $input);
|
||||
}
|
||||
|
||||
my $uninitialized;
|
||||
|
||||
is(trim($uninitialized), '', "Trimming an uninitialized value");
|
||||
};
|
||||
|
||||
done_testing;
|
68
t/Hydra/Helper/Nix.t
Normal file
68
t/Hydra/Helper/Nix.t
Normal file
@ -0,0 +1,68 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use File::Temp;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Helper::Nix;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $dir = File::Temp->newdir();
|
||||
my $machines = "$dir/machines";
|
||||
|
||||
$ENV{'NIX_REMOTE_SYSTEMS'} = $machines;
|
||||
|
||||
open(my $fh, '>', $machines) or die "Could not open file '$machines' $!";
|
||||
print $fh q|
|
||||
# foobar
|
||||
root@ip x86_64-darwin /sshkey 15 15 big-parallel,kvm,nixos-test - base64key
|
||||
|
||||
# Macs
|
||||
# root@bar x86_64-darwin /sshkey 6 1 big-parallel
|
||||
root@baz aarch64-darwin /sshkey 4 1 big-parallel
|
||||
|
||||
root@bux i686-linux,x86_64-linux /var/sshkey 1 1 kvm,nixos-test benchmark
|
||||
root@lotsofspace i686-linux,x86_64-linux /var/sshkey 1 1 kvm,nixos-test benchmark
|
||||
|
||||
|;
|
||||
close $fh;
|
||||
|
||||
is(Hydra::Helper::Nix::getMachines(), {
|
||||
'root@ip' => {
|
||||
'systemTypes' => ["x86_64-darwin"],
|
||||
'sshKeys' => '/sshkey',
|
||||
'maxJobs' => 15,
|
||||
'speedFactor' => 15,
|
||||
'supportedFeatures' => ["big-parallel", "kvm", "nixos-test" ],
|
||||
'mandatoryFeatures' => [ ],
|
||||
},
|
||||
'root@baz' => {
|
||||
'systemTypes' => [ "aarch64-darwin" ],
|
||||
'sshKeys' => '/sshkey',
|
||||
'maxJobs' => 4,
|
||||
'speedFactor' => 1,
|
||||
'supportedFeatures' => ["big-parallel"],
|
||||
'mandatoryFeatures' => [],
|
||||
},
|
||||
'root@bux' => {
|
||||
'systemTypes' => [ "i686-linux", "x86_64-linux" ],
|
||||
'sshKeys' => '/var/sshkey',
|
||||
'maxJobs' => 1,
|
||||
'speedFactor' => 1,
|
||||
'supportedFeatures' => [ "kvm", "nixos-test", "benchmark" ],
|
||||
'mandatoryFeatures' => [ "benchmark" ],
|
||||
},
|
||||
'root@lotsofspace' => {
|
||||
'systemTypes' => [ "i686-linux", "x86_64-linux" ],
|
||||
'sshKeys' => '/var/sshkey',
|
||||
'maxJobs' => 1,
|
||||
'speedFactor' => 1,
|
||||
'supportedFeatures' => [ "kvm", "nixos-test", "benchmark" ],
|
||||
'mandatoryFeatures' => [ "benchmark" ],
|
||||
},
|
||||
|
||||
}, ":)");
|
||||
|
||||
done_testing;
|
54
t/Hydra/Helper/attributeset.t
Normal file
54
t/Hydra/Helper/attributeset.t
Normal file
@ -0,0 +1,54 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
use Test2::V0;
|
||||
use Hydra::Helper::AttributeSet;
|
||||
|
||||
|
||||
subtest "splitting an attribute path in to its component parts" => sub {
|
||||
my %values = (
|
||||
"" => [''],
|
||||
"." => ['', ''],
|
||||
"...." => ['', '', '', '', ''],
|
||||
"foobar" => ['foobar'],
|
||||
"foo.bar" => ['foo', 'bar'],
|
||||
"🌮" => ['🌮'],
|
||||
|
||||
# not supported: 'foo."bar.baz".tux' => [ 'foo', 'bar.baz', 'tux' ]
|
||||
# the edge cases are fairly significant around escaping and unescaping.
|
||||
);
|
||||
|
||||
for my $input (keys %values) {
|
||||
my @value = @{$values{$input}};
|
||||
my @components = Hydra::Helper::AttributeSet::splitPath($input);
|
||||
is(\@components, \@value, "Splitting the attribute path: " . $input);
|
||||
}
|
||||
};
|
||||
|
||||
my $attrs = Hydra::Helper::AttributeSet->new();
|
||||
$attrs->registerValue("foo");
|
||||
$attrs->registerValue("bar.baz.tux");
|
||||
$attrs->registerValue("bar.baz.bux.foo.bar.baz");
|
||||
|
||||
my @enumerated = $attrs->enumerate();
|
||||
is(
|
||||
\@enumerated,
|
||||
[
|
||||
# "foo": skipped since we're registering values, and we
|
||||
# only want to track nested attribute sets.
|
||||
|
||||
# "bar.baz.tux": expand the path
|
||||
"bar",
|
||||
"bar.baz",
|
||||
|
||||
#"bar.baz.bux.foo.bar.baz": expand the path, but only register new
|
||||
# attribute set names.
|
||||
"bar.baz.bux",
|
||||
"bar.baz.bux.foo",
|
||||
"bar.baz.bux.foo.bar",
|
||||
],
|
||||
"Attribute set paths are registered."
|
||||
);
|
||||
|
||||
done_testing;
|
45
t/Hydra/Helper/escape.t
Normal file
45
t/Hydra/Helper/escape.t
Normal file
@ -0,0 +1,45 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Data::Dumper;
|
||||
use Test2::V0;
|
||||
use Hydra::Helper::Escape;
|
||||
|
||||
subtest "checking individual attribute set elements" => sub {
|
||||
my %values = (
|
||||
"" => '""',
|
||||
"." => '"."',
|
||||
"foobar" => '"foobar"',
|
||||
"foo.bar" => '"foo.bar"',
|
||||
"🌮" => '"🌮"',
|
||||
'foo"bar' => '"foo\"bar"',
|
||||
'foo\\bar' => '"foo\\\\bar"',
|
||||
'$bar' => '"\\$bar"',
|
||||
);
|
||||
|
||||
for my $input (keys %values) {
|
||||
my $value = $values{$input};
|
||||
is(escapeString($input), $value, "Escaping the value: " . $input);
|
||||
}
|
||||
};
|
||||
|
||||
subtest "escaping path components of a nested attribute" => sub {
|
||||
my %values = (
|
||||
"" => '""',
|
||||
"." => '"".""',
|
||||
"...." => '""."".""."".""',
|
||||
"foobar" => '"foobar"',
|
||||
"foo.bar" => '"foo"."bar"',
|
||||
"🌮" => '"🌮"',
|
||||
'foo"bar' => '"foo\"bar"',
|
||||
'foo\\bar' => '"foo\\\\bar"',
|
||||
'$bar' => '"\\$bar"',
|
||||
);
|
||||
|
||||
for my $input (keys %values) {
|
||||
my $value = $values{$input};
|
||||
is(escapeAttributePath($input), $value, "Escaping the attribute path: " . $input);
|
||||
}
|
||||
};
|
||||
|
||||
done_testing;
|
19
t/Hydra/Math.t
Normal file
19
t/Hydra/Math.t
Normal file
@ -0,0 +1,19 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
use Hydra::Math qw(exponential_backoff);
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
subtest "exponential_backoff" => sub {
|
||||
is(exponential_backoff(0), 1);
|
||||
is(exponential_backoff(1), 2);
|
||||
is(exponential_backoff(2), 4);
|
||||
is(exponential_backoff(9), 512);
|
||||
is(exponential_backoff(10), 1024);
|
||||
is(exponential_backoff(11), 1024, "we're clamped to 1024 seconds");
|
||||
is(exponential_backoff(11000), 1024, "we're clamped to 1024 seconds");
|
||||
};
|
||||
|
||||
done_testing;
|
63
t/Hydra/Plugin/RunCommand/basic.t
Normal file
63
t/Hydra/Plugin/RunCommand/basic.t
Normal file
@ -0,0 +1,63 @@
|
||||
use feature 'unicode_strings';
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::MaybeXS;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(
|
||||
hydra_config => q|
|
||||
<runcommand>
|
||||
command = cp "$HYDRA_JSON" "$HYDRA_DATA/joboutput.json"
|
||||
</runcommand>
|
||||
|);
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
# Most basic test case, no parameters
|
||||
my $jobset = createBaseJobset("basic", "runcommand.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/runcommand.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating jobs/runcommand.nix should result in 1 build1");
|
||||
|
||||
(my $build) = queuedBuildsForJobset($jobset);
|
||||
|
||||
is($build->job, "metrics", "The only job should be metrics");
|
||||
ok(runBuild($build), "Build should exit with return code 0");
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build should be finished.");
|
||||
is($newbuild->buildstatus, 0, "Build should have buildstatus 0.");
|
||||
|
||||
ok(sendNotifications(), "Notifications execute successfully.");
|
||||
|
||||
my $dat = do {
|
||||
my $filename = $ENV{'HYDRA_DATA'} . "/joboutput.json";
|
||||
open(my $json_fh, "<", $filename)
|
||||
or die("Can't open \"$filename\": $!\n");
|
||||
local $/;
|
||||
my $json = JSON::MaybeXS->new;
|
||||
$json->decode(<$json_fh>)
|
||||
};
|
||||
|
||||
subtest "Validate the file parsed and at least one field matches" => sub {
|
||||
is($dat->{build}, $newbuild->id, "The build event matches our expected ID.");
|
||||
};
|
||||
|
||||
subtest "Validate a run log was created" => sub {
|
||||
my $runlog = $build->runcommandlogs->find({});
|
||||
ok($runlog->did_succeed(), "The process did succeed.");
|
||||
is($runlog->job_matcher, "*:*:*", "An unspecified job matcher is defaulted to *:*:*");
|
||||
is($runlog->command, 'cp "$HYDRA_JSON" "$HYDRA_DATA/joboutput.json"', "The executed command is saved.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is also recent.");
|
||||
is($runlog->exit_code, 0, "This command should have succeeded.");
|
||||
};
|
||||
|
||||
done_testing;
|
52
t/Hydra/Plugin/RunCommand/errno.t
Normal file
52
t/Hydra/Plugin/RunCommand/errno.t
Normal file
@ -0,0 +1,52 @@
|
||||
use feature 'unicode_strings';
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::MaybeXS;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(
|
||||
hydra_config => q|
|
||||
<runcommand>
|
||||
command = invalid-command-this-does-not-exist
|
||||
</runcommand>
|
||||
|);
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
# Most basic test case, no parameters
|
||||
my $jobset = createBaseJobset("basic", "runcommand.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/runcommand.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating jobs/runcommand.nix should result in 1 build1");
|
||||
|
||||
(my $build) = queuedBuildsForJobset($jobset);
|
||||
|
||||
is($build->job, "metrics", "The only job should be metrics");
|
||||
ok(runBuild($build), "Build should exit with return code 0");
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build should be finished.");
|
||||
is($newbuild->buildstatus, 0, "Build should have buildstatus 0.");
|
||||
|
||||
ok(sendNotifications(), "Notifications execute successfully.");
|
||||
|
||||
subtest "Validate a run log was created" => sub {
|
||||
my $runlog = $build->runcommandlogs->find({});
|
||||
ok(!$runlog->did_succeed(), "The process did not succeed.");
|
||||
ok($runlog->did_fail_with_exec_error(), "The process failed to start due to an exec error.");
|
||||
is($runlog->job_matcher, "*:*:*", "An unspecified job matcher is defaulted to *:*:*");
|
||||
is($runlog->command, 'invalid-command-this-does-not-exist', "The executed command is saved.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is also recent.");
|
||||
is($runlog->exit_code, undef, "This command should not have executed.");
|
||||
is($runlog->error_number, 2, "This command failed to exec.");
|
||||
};
|
||||
|
||||
done_testing;
|
133
t/Hydra/Plugin/RunCommand/json.t
Normal file
133
t/Hydra/Plugin/RunCommand/json.t
Normal file
@ -0,0 +1,133 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::MaybeXS;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(
|
||||
hydra_config => q|
|
||||
<runcommand>
|
||||
command = cp "$HYDRA_JSON" "$HYDRA_DATA/joboutput.json"
|
||||
</runcommand>
|
||||
|);
|
||||
|
||||
use Test2::V0;
|
||||
use Hydra::Plugin::RunCommand;
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
|
||||
# Most basic test case, no parameters
|
||||
my $jobset = createBaseJobset("basic", "runcommand.nix", $ctx{jobsdir});
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating jobs/runcommand.nix should exit with return code 0");
|
||||
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating jobs/runcommand.nix should result in 1 build1");
|
||||
|
||||
(my $build) = queuedBuildsForJobset($jobset);
|
||||
|
||||
is($build->job, "metrics", "The only job should be metrics");
|
||||
ok(runBuild($build), "Build should exit with return code 0");
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build should be finished.");
|
||||
is($newbuild->buildstatus, 0, "Build should have buildstatus 0.");
|
||||
|
||||
$build = $newbuild;
|
||||
|
||||
my $dat = Hydra::Plugin::RunCommand::makeJsonPayload("buildFinished", $build);
|
||||
|
||||
subtest "Validate the top level fields match" => sub {
|
||||
is($dat->{build}, $build->id, "The build event matches our expected ID.");
|
||||
is($dat->{buildStatus}, 0, "The build status matches.");
|
||||
is($dat->{event}, "buildFinished", "The build event matches.");
|
||||
is($dat->{finished}, JSON::MaybeXS::true, "The build finished.");
|
||||
is($dat->{project}, "tests", "The project matches.");
|
||||
is($dat->{jobset}, "basic", "The jobset matches.");
|
||||
is($dat->{job}, "metrics", "The job matches.");
|
||||
is($dat->{nixName}, "my-build-product", "The nixName matches.");
|
||||
is($dat->{system}, $build->system, "The system matches.");
|
||||
is($dat->{drvPath}, $build->drvpath, "The derivation path matches.");
|
||||
is($dat->{timestamp}, $build->timestamp, "The result has a timestamp field.");
|
||||
is($dat->{startTime}, $build->starttime, "The result has a startTime field.");
|
||||
is($dat->{stopTime}, $build->stoptime, "The result has a stopTime field.");
|
||||
is($dat->{homepage}, "https://github.com/NixOS/hydra", "The homepage is passed.");
|
||||
is($dat->{description}, "An example meta property.", "The description is passed.");
|
||||
is($dat->{license}, "GPL", "The license is passed.");
|
||||
};
|
||||
|
||||
|
||||
subtest "Validate the outputs match" => sub {
|
||||
is(scalar(@{$dat->{outputs}}), 2, "There are exactly two outputs");
|
||||
|
||||
subtest "output: out" => sub {
|
||||
my ($output) = grep { $_->{name} eq "out" } @{$dat->{outputs}};
|
||||
my $expectedoutput = $build->buildoutputs->find({name => "out"});
|
||||
|
||||
is($output->{name}, "out", "Output is named corrrectly");
|
||||
is($output->{path}, $expectedoutput->path, "The output path matches the database's path.");
|
||||
};
|
||||
|
||||
subtest "output: bin" => sub {
|
||||
my ($output) = grep { $_->{name} eq "bin" } @{$dat->{outputs}};
|
||||
my $expectedoutput = $build->buildoutputs->find({name => "bin"});
|
||||
|
||||
is($output->{name}, "bin", "Output is named corrrectly");
|
||||
is($output->{path}, $expectedoutput->path, "The output path matches the database's path.");
|
||||
};
|
||||
};
|
||||
|
||||
subtest "Validate the metrics match" => sub {
|
||||
is(scalar(@{$dat->{metrics}}), 2, "There are exactly two metrics");
|
||||
|
||||
my ($lineCoverage) = grep { $_->{name} eq "lineCoverage" } @{$dat->{metrics}};
|
||||
my ($maxResident) = grep { $_->{name} eq "maxResident" } @{$dat->{metrics}};
|
||||
|
||||
subtest "verifying the lineCoverage metric" => sub {
|
||||
is($lineCoverage->{name}, "lineCoverage", "The name matches.");
|
||||
is($lineCoverage->{value}, 18, "The value matches.");
|
||||
is($lineCoverage->{unit}, "%", "The unit matches.");
|
||||
};
|
||||
|
||||
subtest "verifying the maxResident metric" => sub {
|
||||
is($maxResident->{name}, "maxResident", "The name matches.");
|
||||
is($maxResident->{value}, 27, "The value matches.");
|
||||
is($maxResident->{unit}, "KiB", "The unit matches.");
|
||||
};
|
||||
};
|
||||
|
||||
subtest "Validate the products match" => sub {
|
||||
is(scalar(@{$dat->{outputs}}), 2, "There are exactly two outputs");
|
||||
|
||||
subtest "product: out" => sub {
|
||||
my ($product) = grep { $_->{name} eq "my-build-product" } @{$dat->{products}};
|
||||
my $expectedproduct = $build->buildproducts->find({name => "my-build-product"});
|
||||
|
||||
is($product->{name}, "my-build-product", "The build product is named correctly.");
|
||||
is($product->{subtype}, "", "The subtype is empty.");
|
||||
is($product->{productNr}, $expectedproduct->productnr, "The product number matches.");
|
||||
is($product->{defaultPath}, "", "The default path matches.");
|
||||
is($product->{path}, $expectedproduct->path, "The path matches the output.");
|
||||
is($product->{fileSize}, undef, "The fileSize is undefined for the nix-build output type.");
|
||||
is($product->{sha256hash}, undef, "The sha256hash is undefined for the nix-build output type.");
|
||||
};
|
||||
|
||||
subtest "output: bin" => sub {
|
||||
my ($product) = grep { $_->{name} eq "my-build-product-bin" } @{$dat->{products}};
|
||||
my $expectedproduct = $build->buildproducts->find({name => "my-build-product-bin"});
|
||||
|
||||
is($product->{name}, "my-build-product-bin", "The build product is named correctly.");
|
||||
is($product->{subtype}, "bin", "The subtype matches the output name");
|
||||
is($product->{productNr}, $expectedproduct->productnr, "The product number matches.");
|
||||
is($product->{defaultPath}, "", "The default path matches.");
|
||||
is($product->{path}, $expectedproduct->path, "The path matches the output.");
|
||||
is($product->{fileSize}, undef, "The fileSize is undefined for the nix-build output type.");
|
||||
is($product->{sha256hash}, undef, "The sha256hash is undefined for the nix-build output type.");
|
||||
};
|
||||
};
|
||||
|
||||
done_testing;
|
177
t/Hydra/Plugin/RunCommand/matcher.t
Normal file
177
t/Hydra/Plugin/RunCommand/matcher.t
Normal file
@ -0,0 +1,177 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Test2::V0;
|
||||
use Hydra::Plugin::RunCommand;
|
||||
|
||||
subtest "isEnabled" => sub {
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::isEnabled({}),
|
||||
"",
|
||||
"Disabled by default."
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::isEnabled({ config => {}}),
|
||||
"",
|
||||
"Disabled by default."
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::isEnabled({ config => { runcommand => {}}}),
|
||||
1,
|
||||
"Enabled if any runcommand blocks exist."
|
||||
);
|
||||
};
|
||||
|
||||
subtest "configSectionMatches" => sub {
|
||||
subtest "Expected matches" => sub {
|
||||
my @examples = (
|
||||
# Exact match
|
||||
["project:jobset:job", "project", "jobset", "job"],
|
||||
|
||||
# One wildcard
|
||||
["project:jobset:*", "project", "jobset", "job"],
|
||||
["project:*:job", "project", "jobset", "job"],
|
||||
["*:jobset:job", "project", "jobset", "job"],
|
||||
|
||||
# Two wildcards
|
||||
["project:*:*", "project", "jobset", "job"],
|
||||
["*:*:job", "project", "jobset", "job"],
|
||||
|
||||
# Three wildcards
|
||||
["*:*:*", "project", "jobset", "job"],
|
||||
|
||||
# Implicit wildcards
|
||||
["", "project", "jobset", "job"],
|
||||
["project", "project", "jobset", "job"],
|
||||
["project:jobset", "project", "jobset", "job"],
|
||||
);
|
||||
|
||||
for my $example (@examples) {
|
||||
my ($matcher, $project, $jobset, $job) = @$example;
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::configSectionMatches(
|
||||
$matcher, $project, $jobset, $job
|
||||
),
|
||||
1,
|
||||
"Expecting $matcher to match $project:$jobset:$job"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
subtest "Fails to match" => sub {
|
||||
my @examples = (
|
||||
# Literal string non-matches
|
||||
["project:jobset:job", "project", "jobset", "nonmatch"],
|
||||
["project:jobset:job", "project", "nonmatch", "job"],
|
||||
["project:jobset:job", "nonmatch", "jobset", "job"],
|
||||
|
||||
# Wildcard based non-matches
|
||||
["*:*:job", "project", "jobset", "nonmatch"],
|
||||
["*:jobset:*", "project", "nonmatch", "job"],
|
||||
["project:*:*", "nonmatch", "jobset", "job"],
|
||||
|
||||
# These wildcards are NOT regular expressions
|
||||
["*:*:j.*", "project", "jobset", "job"],
|
||||
[".*:.*:.*", "project", "nonmatch", "job"],
|
||||
);
|
||||
|
||||
for my $example (@examples) {
|
||||
my ($matcher, $project, $jobset, $job) = @$example;
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::configSectionMatches(
|
||||
$matcher, $project, $jobset, $job
|
||||
),
|
||||
0,
|
||||
"Expecting $matcher to not match $project:$jobset:$job"
|
||||
);
|
||||
}
|
||||
|
||||
like(
|
||||
dies {
|
||||
Hydra::Plugin::RunCommand::configSectionMatches(
|
||||
"foo:bar:baz:tux", "foo", "bar", "baz"
|
||||
),
|
||||
},
|
||||
qr/invalid section name/,
|
||||
"A matcher must have no more than 3 sections"
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
subtest "eventMatches" => sub {
|
||||
# This is probably a misfeature that isn't very useful but let's test
|
||||
# it anyway. At best this lets you make a RunCommand event not work
|
||||
# by specifying the "events" key. Note: By testing it I'm not promising
|
||||
# it'll keep working. In fact, I wouldn't be surprised if we chose to
|
||||
# delete this support since RunCommand never runs on any event other
|
||||
# than buildFinished.
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::eventMatches({}, "buildFinished"),
|
||||
1,
|
||||
"An unspecified events key matches"
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::eventMatches({ events => ""}, "buildFinished"),
|
||||
0,
|
||||
"An empty events key does not match"
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::eventMatches({ events => "foo bar buildFinished baz"}, "buildFinished"),
|
||||
1,
|
||||
"An events key with multiple events does match when buildFinished is present"
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::eventMatches({ events => "foo bar baz"}, "buildFinished"),
|
||||
0,
|
||||
"An events key with multiple events does not match when buildFinished is missing"
|
||||
);
|
||||
};
|
||||
|
||||
subtest "fanoutToCommands" => sub {
|
||||
my $config = {
|
||||
runcommand => [
|
||||
{
|
||||
job => "",
|
||||
command => "foo"
|
||||
},
|
||||
{
|
||||
job => "project:*:*",
|
||||
command => "bar"
|
||||
},
|
||||
{
|
||||
job => "project:jobset:nomatch",
|
||||
command => "baz"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
is(
|
||||
Hydra::Plugin::RunCommand::fanoutToCommands(
|
||||
$config,
|
||||
"buildFinished",
|
||||
"project",
|
||||
"jobset",
|
||||
"job"
|
||||
),
|
||||
[
|
||||
{
|
||||
matcher => "",
|
||||
command => "foo"
|
||||
},
|
||||
{
|
||||
matcher => "project:*:*",
|
||||
command => "bar"
|
||||
}
|
||||
],
|
||||
"fanoutToCommands returns a command per matching job"
|
||||
);
|
||||
};
|
||||
|
||||
done_testing;
|
76
t/Hydra/Plugin/gitea.t
Normal file
76
t/Hydra/Plugin/gitea.t
Normal file
@ -0,0 +1,76 @@
|
||||
use feature 'unicode_strings';
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::MaybeXS;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init(
|
||||
hydra_config => q|
|
||||
<gitea_authorization>
|
||||
root=d7f16a3412e01a43a414535b16007c6931d3a9c7
|
||||
</gitea_authorization>
|
||||
|);
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $scratch = "$ctx{tmpdir}/scratch";
|
||||
mkdir $scratch;
|
||||
|
||||
my $uri = "file://$scratch/git-repo";
|
||||
|
||||
my $jobset = createJobsetWithOneInput('gitea', 'git-input.nix', 'src', 'git', $uri, $ctx{jobsdir});
|
||||
|
||||
sub addStringInput {
|
||||
my ($jobset, $name, $value) = @_;
|
||||
my $input = $jobset->jobsetinputs->create({name => $name, type => "string"});
|
||||
$input->jobsetinputalts->create({value => $value, altnr => 0});
|
||||
}
|
||||
|
||||
addStringInput($jobset, "gitea_repo_owner", "root");
|
||||
addStringInput($jobset, "gitea_repo_name", "foo");
|
||||
addStringInput($jobset, "gitea_status_repo", "src");
|
||||
addStringInput($jobset, "gitea_http_url", "http://localhost:8282/gitea");
|
||||
|
||||
updateRepository('gitea', "$ctx{testdir}/jobs/git-update.sh", $scratch);
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating nix expression");
|
||||
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating jobs/runcommand.nix should result in 1 build1");
|
||||
|
||||
(my $build) = queuedBuildsForJobset($jobset);
|
||||
ok(runBuild($build), "Build should succeed with exit code 0");
|
||||
|
||||
my $filename = $ENV{'HYDRA_DATA'} . "/giteaout.json";
|
||||
my $pid;
|
||||
if (!defined($pid = fork())) {
|
||||
die "Cannot fork(): $!";
|
||||
} elsif ($pid == 0) {
|
||||
exec("python3 $ctx{jobsdir}/server.py $filename");
|
||||
} else {
|
||||
my $newbuild = $db->resultset('Builds')->find($build->id);
|
||||
is($newbuild->finished, 1, "Build should be finished.");
|
||||
is($newbuild->buildstatus, 0, "Build should have buildstatus 0.");
|
||||
ok(sendNotifications(), "Sent notifications");
|
||||
|
||||
kill('INT', $pid);
|
||||
}
|
||||
|
||||
open(my $fh, "<", $filename) or die ("Can't open(): $!\n");
|
||||
my $i = 0;
|
||||
my $uri = <$fh>;
|
||||
my $data = <$fh>;
|
||||
|
||||
ok(index($uri, "gitea/api/v1/repos/root/foo/statuses") != -1, "Correct URL");
|
||||
|
||||
my $json = JSON->new;
|
||||
my $content;
|
||||
$content = $json->decode($data);
|
||||
|
||||
is($content->{state}, "success", "Success notification");
|
||||
|
||||
done_testing;
|
80
t/Hydra/PostgresListener.t
Normal file
80
t/Hydra/PostgresListener.t
Normal file
@ -0,0 +1,80 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Hydra::PostgresListener;
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
my $dbh = $db->storage->dbh;
|
||||
|
||||
my $listener = Hydra::PostgresListener->new($dbh);
|
||||
|
||||
$listener->subscribe("foo");
|
||||
$listener->subscribe("bar");
|
||||
|
||||
is(undef, $listener->block_for_messages(0)->(), "There is no message");
|
||||
is(undef, $listener->block_for_messages(0)->(), "There is no message");
|
||||
is(undef, $listener->block_for_messages(0)->(), "There is no message");
|
||||
|
||||
$dbh->do("notify foo, ?", undef, "hi");
|
||||
my $event = $listener->block_for_messages(0)->();
|
||||
is($event->{'channel'}, "foo", "The channel matches");
|
||||
isnt($event->{'pid'}, undef, "The pid is set");
|
||||
is($event->{'payload'}, "hi", "The payload matches");
|
||||
|
||||
is(undef, $listener->block_for_messages(0)->(), "There is no message");
|
||||
|
||||
|
||||
like(
|
||||
dies {
|
||||
local $SIG{ALRM} = sub { die "timeout" };
|
||||
alarm 1;
|
||||
$listener->block_for_messages->();
|
||||
alarm 0;
|
||||
},
|
||||
qr/timeout/,
|
||||
"An unspecified block should block forever"
|
||||
);
|
||||
|
||||
like(
|
||||
dies {
|
||||
local $SIG{ALRM} = sub { die "timeout" };
|
||||
alarm 1;
|
||||
$listener->block_for_messages(2)->();
|
||||
alarm 0;
|
||||
},
|
||||
qr/timeout/,
|
||||
"A 2-second block goes longer than 1 second"
|
||||
);
|
||||
|
||||
ok(
|
||||
lives {
|
||||
local $SIG{ALRM} = sub { die "timeout" };
|
||||
alarm 2;
|
||||
is(undef, $listener->block_for_messages(1)->(), "A one second block returns undef data after timeout");
|
||||
alarm 0;
|
||||
},
|
||||
"A 1-second block expires within 2 seconds"
|
||||
);
|
||||
|
||||
subtest "with wacky channel names" => sub {
|
||||
my $channel = "foo! very weird channel names...; select * from t where 1 = 1";
|
||||
my $escapedChannel = $dbh->quote_identifier($channel);
|
||||
|
||||
$listener->subscribe($channel);
|
||||
|
||||
is(undef, $listener->block_for_messages(0)->(), "There is no message");
|
||||
|
||||
$dbh->do("notify $escapedChannel, ?", undef, "hi");
|
||||
my $event = $listener->block_for_messages(0)->();
|
||||
is($event->{'channel'}, $channel, "The channel matches");
|
||||
isnt($event->{'pid'}, undef, "The pid is set");
|
||||
is($event->{'payload'}, "hi", "The payload matches");
|
||||
};
|
||||
|
||||
done_testing;
|
139
t/Hydra/Schema/Result/RunCommandLogs.t
Normal file
139
t/Hydra/Schema/Result/RunCommandLogs.t
Normal file
@ -0,0 +1,139 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Test2::V0;
|
||||
|
||||
my $ctx = test_context();
|
||||
my $db = $ctx->db();
|
||||
|
||||
my $builds = $ctx->makeAndEvaluateJobset(
|
||||
expression => "basic.nix",
|
||||
);
|
||||
|
||||
my $build = $builds->{"empty_dir"};
|
||||
|
||||
sub new_run_log {
|
||||
return $db->resultset('RunCommandLogs')->create({
|
||||
job_matcher => "*:*:*",
|
||||
build_id => $build->get_column('id'),
|
||||
command => "bogus",
|
||||
});
|
||||
}
|
||||
|
||||
subtest "Not yet started" => sub {
|
||||
my $runlog = new_run_log();
|
||||
|
||||
is($runlog->start_time, undef, "The start time is undefined.");
|
||||
is($runlog->end_time, undef, "The start time is undefined.");
|
||||
is($runlog->exit_code, undef, "The exit code is undefined.");
|
||||
is($runlog->signal, undef, "The signal is undefined.");
|
||||
is($runlog->core_dumped, undef, "The core dump status is undefined.");
|
||||
};
|
||||
|
||||
subtest "Completing a process before it is started is invalid" => sub {
|
||||
my $runlog = new_run_log();
|
||||
|
||||
like(
|
||||
dies {
|
||||
$runlog->completed_with_child_error(0, 0);
|
||||
},
|
||||
qr/runcommandlogs_end_time_has_start_time/,
|
||||
"It is invalid to complete the process before it started"
|
||||
);
|
||||
};
|
||||
|
||||
subtest "Starting a process" => sub {
|
||||
my $runlog = new_run_log();
|
||||
$runlog->started();
|
||||
is($runlog->did_succeed(), undef, "The process has not yet succeeded.");
|
||||
ok($runlog->is_running(), "The process is running.");
|
||||
ok(!$runlog->did_fail_with_signal(), "The process was not killed by a signal.");
|
||||
ok(!$runlog->did_fail_with_exec_error(), "The process did not fail to start due to an exec error.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, undef, "The end time is undefined.");
|
||||
is($runlog->exit_code, undef, "The exit code is undefined.");
|
||||
is($runlog->signal, undef, "The signal is undefined.");
|
||||
is($runlog->core_dumped, undef, "The core dump status is undefined.");
|
||||
};
|
||||
|
||||
subtest "The process completed (success)" => sub {
|
||||
my $runlog = new_run_log();
|
||||
$runlog->started();
|
||||
$runlog->completed_with_child_error(0, 123);
|
||||
ok($runlog->did_succeed(), "The process did succeed.");
|
||||
ok(!$runlog->is_running(), "The process is not running.");
|
||||
ok(!$runlog->did_fail_with_signal(), "The process was not killed by a signal.");
|
||||
ok(!$runlog->did_fail_with_exec_error(), "The process did not fail to start due to an exec error.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is recent.");
|
||||
is($runlog->error_number, undef, "The error number is undefined");
|
||||
is($runlog->exit_code, 0, "The exit code is 0.");
|
||||
is($runlog->signal, undef, "The signal is undefined.");
|
||||
is($runlog->core_dumped, undef, "The core dump is undefined.");
|
||||
};
|
||||
|
||||
subtest "The process completed (errored)" => sub {
|
||||
my $runlog = new_run_log();
|
||||
$runlog->started();
|
||||
$runlog->completed_with_child_error(21760, 123);
|
||||
ok(!$runlog->did_succeed(), "The process did not succeed.");
|
||||
ok(!$runlog->is_running(), "The process is not running.");
|
||||
ok(!$runlog->did_fail_with_signal(), "The process was not killed by a signal.");
|
||||
ok(!$runlog->did_fail_with_exec_error(), "The process did not fail to start due to an exec error.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is recent.");
|
||||
is($runlog->error_number, undef, "The error number is undefined");
|
||||
is($runlog->exit_code, 85, "The exit code is 85.");
|
||||
is($runlog->signal, undef, "The signal is undefined.");
|
||||
is($runlog->core_dumped, undef, "The core dump is undefined.");
|
||||
};
|
||||
|
||||
subtest "The process completed (status 15, child error 0)" => sub {
|
||||
my $runlog = new_run_log();
|
||||
$runlog->started();
|
||||
$runlog->completed_with_child_error(15, 0);
|
||||
ok(!$runlog->did_succeed(), "The process did not succeed.");
|
||||
ok(!$runlog->is_running(), "The process is not running.");
|
||||
ok($runlog->did_fail_with_signal(), "The process was killed by a signal.");
|
||||
ok(!$runlog->did_fail_with_exec_error(), "The process did not fail to start due to an exec error.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is recent.");
|
||||
is($runlog->error_number, undef, "The error number is undefined");
|
||||
is($runlog->exit_code, undef, "The exit code is undefined.");
|
||||
is($runlog->signal, 15, "Signal 15 was sent.");
|
||||
is($runlog->core_dumped, 0, "There was no core dump.");
|
||||
};
|
||||
|
||||
subtest "The process completed (signaled)" => sub {
|
||||
my $runlog = new_run_log();
|
||||
$runlog->started();
|
||||
$runlog->completed_with_child_error(393, 234);
|
||||
ok(!$runlog->did_succeed(), "The process did not succeed.");
|
||||
ok(!$runlog->is_running(), "The process is not running.");
|
||||
ok($runlog->did_fail_with_signal(), "The process was killed by a signal.");
|
||||
ok(!$runlog->did_fail_with_exec_error(), "The process did not fail to start due to an exec error.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is recent.");
|
||||
is($runlog->error_number, undef, "The error number is undefined");
|
||||
is($runlog->exit_code, undef, "The exit code is undefined.");
|
||||
is($runlog->signal, 9, "The signal is 9.");
|
||||
is($runlog->core_dumped, 1, "The core dumped.");
|
||||
};
|
||||
|
||||
subtest "The process failed to start" => sub {
|
||||
my $runlog = new_run_log();
|
||||
$runlog->started();
|
||||
$runlog->completed_with_child_error(-1, 2);
|
||||
ok(!$runlog->did_succeed(), "The process did not succeed.");
|
||||
ok(!$runlog->is_running(), "The process is running.");
|
||||
ok(!$runlog->did_fail_with_signal(), "The process was not killed by a signal.");
|
||||
ok($runlog->did_fail_with_exec_error(), "The process failed to start due to an exec error.");
|
||||
is($runlog->start_time, within(time() - 1, 2), "The start time is recent.");
|
||||
is($runlog->end_time, within(time() - 1, 2), "The end time is recent.");
|
||||
is($runlog->error_number, 2, "The error number is saved");
|
||||
is($runlog->exit_code, undef, "The exit code is undefined.");
|
||||
is($runlog->signal, undef, "The signal is undefined.");
|
||||
is($runlog->core_dumped, undef, "The core dumped is not defined.");
|
||||
};
|
||||
|
||||
done_testing;
|
35
t/Hydra/Schema/Result/TaskRetries.t
Normal file
35
t/Hydra/Schema/Result/TaskRetries.t
Normal file
@ -0,0 +1,35 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $taskretries = $db->resultset('TaskRetries');
|
||||
|
||||
subtest "requeue" => sub {
|
||||
my $task = $taskretries->create({
|
||||
channel => "bogus",
|
||||
pluginname => "bogus",
|
||||
payload => "bogus",
|
||||
attempts => 1,
|
||||
retry_at => time(),
|
||||
});
|
||||
|
||||
$task->requeue();
|
||||
is($task->attempts, 2, "We should have stored a second retry");
|
||||
is($task->retry_at, within(time() + 4, 2), "Delayed two exponential backoff step");
|
||||
|
||||
$task->requeue();
|
||||
is($task->attempts, 3, "We should have stored a third retry");
|
||||
is($task->retry_at, within(time() + 8, 2), "Delayed a third exponential backoff step");
|
||||
};
|
||||
|
||||
done_testing;
|
107
t/Hydra/Schema/ResultSet/TaskRetries.t
Normal file
107
t/Hydra/Schema/ResultSet/TaskRetries.t
Normal file
@ -0,0 +1,107 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
use Hydra::Event;
|
||||
use Hydra::Task;
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $taskretries = $db->resultset('TaskRetries');
|
||||
|
||||
subtest "get_seconds_to_next_retry" => sub {
|
||||
subtest "Without any records in the database" => sub {
|
||||
is($taskretries->get_seconds_to_next_retry(), undef, "Without any records our next retry moment is forever away.");
|
||||
};
|
||||
|
||||
subtest "With only tasks whose retry timestamps are in the future" => sub {
|
||||
$taskretries->create({
|
||||
channel => "bogus",
|
||||
pluginname => "bogus",
|
||||
payload => "bogus",
|
||||
attempts => 1,
|
||||
retry_at => time() + 100,
|
||||
});
|
||||
is($taskretries->get_seconds_to_next_retry(), within(100, 2), "We should retry in roughly 100 seconds");
|
||||
};
|
||||
|
||||
subtest "With tasks whose retry timestamp are in the past" => sub {
|
||||
$taskretries->create({
|
||||
channel => "bogus",
|
||||
pluginname => "bogus",
|
||||
payload => "bogus",
|
||||
attempts => 1,
|
||||
retry_at => time() - 100,
|
||||
});
|
||||
is($taskretries->get_seconds_to_next_retry(), 0, "We should retry immediately");
|
||||
};
|
||||
|
||||
$taskretries->delete_all();
|
||||
};
|
||||
|
||||
subtest "get_retryable_taskretries_row" => sub {
|
||||
subtest "Without any records in the database" => sub {
|
||||
is($taskretries->get_retryable_taskretries_row(), undef, "Without any records we have no tasks to retry.");
|
||||
is($taskretries->get_retryable_task(), undef, "Without any records we have no tasks to retry.");
|
||||
};
|
||||
|
||||
subtest "With only tasks whose retry timestamps are in the future" => sub {
|
||||
$taskretries->create({
|
||||
channel => "bogus",
|
||||
pluginname => "bogus",
|
||||
payload => "bogus",
|
||||
attempts => 1,
|
||||
retry_at => time() + 100,
|
||||
});
|
||||
is($taskretries->get_retryable_taskretries_row(), undef, "We still have nothing to do");
|
||||
is($taskretries->get_retryable_task(), undef, "We still have nothing to do");
|
||||
};
|
||||
|
||||
subtest "With tasks whose retry timestamp are in the past" => sub {
|
||||
$taskretries->create({
|
||||
channel => "build_started",
|
||||
pluginname => "bogus plugin",
|
||||
payload => "123",
|
||||
attempts => 1,
|
||||
retry_at => time() - 100,
|
||||
});
|
||||
|
||||
my $row = $taskretries->get_retryable_taskretries_row();
|
||||
isnt($row, undef, "We should retry immediately");
|
||||
is($row->channel, "build_started", "Channel name should match");
|
||||
is($row->pluginname, "bogus plugin", "Plugin name should match");
|
||||
is($row->payload, "123", "Payload should match");
|
||||
is($row->attempts, 1, "We've had one attempt");
|
||||
|
||||
my $task = $taskretries->get_retryable_task();
|
||||
is($task->{"event"}->{"channel_name"}, "build_started");
|
||||
is($task->{"plugin_name"}, "bogus plugin");
|
||||
is($task->{"event"}->{"payload"}, "123");
|
||||
is($task->{"record"}->get_column("id"), $row->get_column("id"));
|
||||
};
|
||||
};
|
||||
|
||||
subtest "save_task" => sub {
|
||||
my $event = Hydra::Event->new_event("build_started", "1");
|
||||
my $task = Hydra::Task->new(
|
||||
$event,
|
||||
"FooPluginName",
|
||||
);
|
||||
|
||||
my $retry = $taskretries->save_task($task);
|
||||
|
||||
is($retry->channel, "build_started", "Channel name should match");
|
||||
is($retry->pluginname, "FooPluginName", "Plugin name should match");
|
||||
is($retry->payload, "1", "Payload should match");
|
||||
is($retry->attempts, 1, "We've had one attempt");
|
||||
is($retry->retry_at, within(time() + 1, 2), "The retry at should be approximately one second away");
|
||||
};
|
||||
|
||||
done_testing;
|
73
t/Hydra/Schema/Users.t
Normal file
73
t/Hydra/Schema/Users.t
Normal file
@ -0,0 +1,73 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
my %ctx = test_init();
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
use Test2::V0;
|
||||
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
# Hydra used to store passwords, by default, as plain unsalted sha1 hashes.
|
||||
# We now upgrade these badly stored passwords with much stronger algorithms
|
||||
# when the user logs in. Implementing this meant reimplementing our password
|
||||
# checking ourselves, so also ensure that basic password checking works.
|
||||
#
|
||||
# This test:
|
||||
#
|
||||
# 1. creates a user with the legacy password
|
||||
# 2. validates that the wrong password is not considered valid
|
||||
# 3. validates that the correct password is valid
|
||||
# 4. checks that the checking of the correct password transparently upgraded
|
||||
# the password's storage to a more secure algorithm.
|
||||
|
||||
# Starting the user with an unsalted sha1 password
|
||||
my $user = $db->resultset('Users')->create({
|
||||
"username" => "alice",
|
||||
"emailaddress" => 'alice@nixos.org',
|
||||
"password" => "8843d7f92416211de9ebb963ff4ce28125932878" # SHA1 of "foobar"
|
||||
});
|
||||
isnt($user, undef, "My user was created.");
|
||||
|
||||
ok(!$user->check_password("barbaz"), "Checking the password, barbaz, is not right");
|
||||
|
||||
is($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The unsalted sha1 is in the database.");
|
||||
ok($user->check_password("foobar"), "Checking the password, foobar, is right");
|
||||
isnt($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The user has had their password rehashed.");
|
||||
ok($user->check_password("foobar"), "Checking the password, foobar, is still right");
|
||||
|
||||
# All sha1 passwords will be upgraded when `hydra-init` is run, by passing the sha1 through
|
||||
# Argon2. Verify a rehashed sha1 validates too. This removes very weak password hashes
|
||||
# from the database without requiring users to log in.
|
||||
subtest "Hashing their sha1 as Argon2 still lets them log in with their password" => sub {
|
||||
$user->setPassword("8843d7f92416211de9ebb963ff4ce28125932878"); # SHA1 of "foobar"
|
||||
my $hashedHashPassword = $user->password;
|
||||
isnt($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The user has had their password's hash rehashed.");
|
||||
ok($user->check_password("foobar"), "Checking the password, foobar, is still right");
|
||||
isnt($user->password, $hashedHashPassword, "The user's hashed hash was replaced with just Argon2.");
|
||||
};
|
||||
|
||||
|
||||
subtest "Setting the user's passwordHash to a sha1 stores the password as a hashed sha1" => sub {
|
||||
$user->setPasswordHash("8843d7f92416211de9ebb963ff4ce28125932878");
|
||||
isnt($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The password was not saved in plain text.");
|
||||
|
||||
my $storedPassword = $user->password;
|
||||
ok($user->check_password("foobar"), "Their password validates");
|
||||
isnt($storedPassword, $user->password, "The password was upgraded.");
|
||||
};
|
||||
|
||||
subtest "Setting the user's passwordHash to an argon2 password stores the password as given" => sub {
|
||||
$user->setPasswordHash('$argon2id$v=19$m=262144,t=3,p=1$tMnV5paYjmIrUIb6hylaNA$M8/e0i3NGrjhOliVLa5LqQ');
|
||||
isnt($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The password was not saved in plain text.");
|
||||
is($user->password, '$argon2id$v=19$m=262144,t=3,p=1$tMnV5paYjmIrUIb6hylaNA$M8/e0i3NGrjhOliVLa5LqQ', "The password was saved as-is.");
|
||||
|
||||
my $storedPassword = $user->password;
|
||||
ok($user->check_password("foobar"), "Their password validates");
|
||||
is($storedPassword, $user->password, "The password was not upgraded.");
|
||||
};
|
||||
done_testing;
|
221
t/Hydra/TaskDispatcher.t
Normal file
221
t/Hydra/TaskDispatcher.t
Normal file
@ -0,0 +1,221 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
|
||||
use Hydra::TaskDispatcher;
|
||||
use Prometheus::Tiny::Shared;
|
||||
|
||||
use Test2::V0;
|
||||
use Test2::Tools::Mock qw(mock_obj);
|
||||
|
||||
my $db = "bogus db";
|
||||
my $prometheus = Prometheus::Tiny::Shared->new;
|
||||
|
||||
sub make_noop_plugin {
|
||||
my ($name) = @_;
|
||||
my $plugin = {
|
||||
"name" => $name,
|
||||
};
|
||||
my $mock_plugin = mock_obj $plugin => ();
|
||||
|
||||
return $mock_plugin;
|
||||
}
|
||||
|
||||
sub make_fake_event {
|
||||
my ($channel_name) = @_;
|
||||
|
||||
my $event = {
|
||||
channel_name => $channel_name,
|
||||
called_with => [],
|
||||
};
|
||||
my $mock_event = mock_obj $event => (
|
||||
add => [
|
||||
"execute" => sub {
|
||||
my ($self, $db, $plugin) = @_;
|
||||
push @{$self->{"called_with"}}, $plugin;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
return $mock_event;
|
||||
}
|
||||
|
||||
sub make_failing_event {
|
||||
my ($channel_name) = @_;
|
||||
|
||||
my $event = {
|
||||
channel_name => $channel_name,
|
||||
called_with => [],
|
||||
};
|
||||
my $mock_event = mock_obj $event => (
|
||||
add => [
|
||||
"execute" => sub {
|
||||
my ($self, $db, $plugin) = @_;
|
||||
push @{$self->{"called_with"}}, $plugin;
|
||||
die "Failing plugin."
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
return $mock_event;
|
||||
}
|
||||
|
||||
sub make_fake_record {
|
||||
my %attrs = @_;
|
||||
|
||||
my $record = {
|
||||
"attempts" => $attrs{"attempts"} || 0,
|
||||
"requeued" => 0,
|
||||
"deleted" => 0
|
||||
};
|
||||
|
||||
my $mock_record = mock_obj $record => (
|
||||
add => [
|
||||
"delete" => sub {
|
||||
my ($self, $db, $plugin) = @_;
|
||||
$self->{"deleted"} = 1;
|
||||
},
|
||||
"requeue" => sub {
|
||||
my ($self, $db, $plugin) = @_;
|
||||
$self->{"requeued"} = 1;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
return $mock_record;
|
||||
}
|
||||
|
||||
subtest "dispatch_event" => sub {
|
||||
subtest "every plugin gets called once, even if it fails all of them." => sub {
|
||||
my $plugins = [make_noop_plugin("bogus-1"), make_noop_plugin("bogus-2")];
|
||||
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, $plugins);
|
||||
|
||||
my $event = make_failing_event("bogus-channel");
|
||||
$dispatcher->dispatch_event($event);
|
||||
|
||||
is(@{$event->{"called_with"}}, 2, "Both plugins should be called");
|
||||
|
||||
my @expected_names = ( "bogus-1", "bogus-2" );
|
||||
my @actual_names = sort(
|
||||
$event->{"called_with"}[0]->name,
|
||||
$event->{"called_with"}[1]->name
|
||||
);
|
||||
is(
|
||||
\@actual_names,
|
||||
\@expected_names,
|
||||
"Both plugins should be executed, but not in any particular order."
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
subtest "dispatch_task" => sub {
|
||||
subtest "every plugin gets called once" => sub {
|
||||
my $bogus_plugin = make_noop_plugin("bogus-1");
|
||||
my $plugins = [$bogus_plugin, make_noop_plugin("bogus-2")];
|
||||
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, $plugins);
|
||||
|
||||
my $event = make_fake_event("bogus-channel");
|
||||
my $task = Hydra::Task->new($event, ref $bogus_plugin);
|
||||
is($dispatcher->dispatch_task($task), 1, "Calling dispatch_task returns truthy.");
|
||||
|
||||
is(@{$event->{"called_with"}}, 1, "Just one plugin should be called");
|
||||
|
||||
is(
|
||||
$event->{"called_with"}[0]->name,
|
||||
"bogus-1",
|
||||
"Just bogus-1 should be executed."
|
||||
);
|
||||
};
|
||||
|
||||
subtest "a task with an invalid plugin is not fatal" => sub {
|
||||
my $bogus_plugin = make_noop_plugin("bogus-1");
|
||||
my $plugins = [$bogus_plugin, make_noop_plugin("bogus-2")];
|
||||
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, $plugins);
|
||||
|
||||
my $event = make_fake_event("bogus-channel");
|
||||
my $task = Hydra::Task->new($event, "this-plugin-does-not-exist");
|
||||
is($dispatcher->dispatch_task($task), 0, "Calling dispatch_task returns falsey.");
|
||||
|
||||
is(@{$event->{"called_with"}}, 0, "No plugins are called");
|
||||
};
|
||||
|
||||
subtest "a failed run without a record saves the task for later" => sub {
|
||||
my $db = "bogus db";
|
||||
|
||||
my $record = make_fake_record();
|
||||
my $bogus_plugin = make_noop_plugin("bogus-1");
|
||||
my $task = {
|
||||
"event" => make_failing_event("fail-event"),
|
||||
"plugin_name" => ref $bogus_plugin,
|
||||
"record" => undef,
|
||||
};
|
||||
|
||||
my $save_hook_called = 0;
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, [$bogus_plugin],
|
||||
sub {
|
||||
$save_hook_called = 1;
|
||||
}
|
||||
);
|
||||
$dispatcher->dispatch_task($task);
|
||||
|
||||
is($save_hook_called, 1, "The record was requeued with the store hook.");
|
||||
};
|
||||
|
||||
subtest "a successful run from a record deletes the record" => sub {
|
||||
my $db = "bogus db";
|
||||
|
||||
my $record = make_fake_record();
|
||||
my $bogus_plugin = make_noop_plugin("bogus-1");
|
||||
my $task = {
|
||||
"event" => make_fake_event("success-event"),
|
||||
"plugin_name" => ref $bogus_plugin,
|
||||
"record" => $record,
|
||||
};
|
||||
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, [$bogus_plugin]);
|
||||
$dispatcher->dispatch_task($task);
|
||||
|
||||
is($record->{"deleted"}, 1, "The record was deleted.");
|
||||
};
|
||||
|
||||
subtest "a failed run from a record re-queues the task" => sub {
|
||||
my $db = "bogus db";
|
||||
|
||||
my $record = make_fake_record();
|
||||
my $bogus_plugin = make_noop_plugin("bogus-1");
|
||||
my $task = {
|
||||
"event" => make_failing_event("fail-event"),
|
||||
"plugin_name" => ref $bogus_plugin,
|
||||
"record" => $record,
|
||||
};
|
||||
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, [$bogus_plugin]);
|
||||
$dispatcher->dispatch_task($task);
|
||||
|
||||
is($record->{"requeued"}, 1, "The record was requeued.");
|
||||
};
|
||||
|
||||
subtest "a failed run from a record with a lot of attempts deletes the task" => sub {
|
||||
my $db = "bogus db";
|
||||
|
||||
my $record = make_fake_record(attempts => 101);
|
||||
|
||||
my $bogus_plugin = make_noop_plugin("bogus-1");
|
||||
my $task = {
|
||||
"event" => make_failing_event("fail-event"),
|
||||
"plugin_name" => ref $bogus_plugin,
|
||||
"record" => $record,
|
||||
};
|
||||
|
||||
my $dispatcher = Hydra::TaskDispatcher->new($db, $prometheus, [$bogus_plugin]);
|
||||
$dispatcher->dispatch_task($task);
|
||||
|
||||
is($record->{"deleted"}, 1, "The record was deleted.");
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
done_testing;
|
71
t/Hydra/View/TT.t
Normal file
71
t/Hydra/View/TT.t
Normal file
@ -0,0 +1,71 @@
|
||||
use feature 'unicode_strings';
|
||||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Test2::V0;
|
||||
|
||||
my $ctx = test_context();
|
||||
|
||||
require Hydra; # calls setup()
|
||||
require Hydra::View::TT;
|
||||
require Catalyst::Test;
|
||||
|
||||
my $db = $ctx->db;
|
||||
|
||||
|
||||
# The following lines are a cheap and hacky trick to get $c,
|
||||
# there is no other reason to call /.
|
||||
Catalyst::Test->import('Hydra');
|
||||
my ($_request, $c) = ctx_request('/');
|
||||
|
||||
|
||||
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
|
||||
my $jobset = createBaseJobset("example", "bogus.nix", $ctx->jobsdir);
|
||||
my $job = "myjob";
|
||||
|
||||
|
||||
is(
|
||||
Hydra::View::TT::linkToProject(undef, $c, $project),
|
||||
'<a href="http://localhost/project/tests">tests</a>',
|
||||
"linkToProject"
|
||||
);
|
||||
is(
|
||||
Hydra::View::TT::linkToJobset(undef, $c, $jobset),
|
||||
'<a href="http://localhost/project/tests">tests</a>'
|
||||
. ':<a href="http://localhost/jobset/tests/example">example</a>',
|
||||
"linkToJobset"
|
||||
);
|
||||
is(
|
||||
Hydra::View::TT::linkToJob(undef, $c, $jobset, $job),
|
||||
'<a href="http://localhost/project/tests">tests</a>'
|
||||
. ':<a href="http://localhost/jobset/tests/example">example</a>'
|
||||
. ':<a href="http://localhost/job/tests/example/myjob">myjob</a>',
|
||||
"linkToJob"
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::View::TT::makeNameLinksForJobset(undef, $c, $jobset),
|
||||
'<a href="http://localhost/project/tests">tests</a>'
|
||||
. ':example',
|
||||
"makeNameLinksForJobset"
|
||||
);
|
||||
is(
|
||||
Hydra::View::TT::makeNameLinksForJob(undef, $c, $jobset, $job),
|
||||
'<a href="http://localhost/project/tests">tests</a>'
|
||||
. ':<a href="http://localhost/jobset/tests/example">example</a>'
|
||||
. ':myjob',
|
||||
"makeNameLinksForJob"
|
||||
);
|
||||
|
||||
is(
|
||||
Hydra::View::TT::makeNameTextForJobset(undef, $c, $jobset),
|
||||
'tests:example',
|
||||
"makeNameTextForJobset"
|
||||
);
|
||||
is(
|
||||
Hydra::View::TT::makeNameTextForJob(undef, $c, $jobset, $job),
|
||||
'tests:example:myjob',
|
||||
"makeNameTextForJob"
|
||||
);
|
||||
|
||||
done_testing;
|
Reference in New Issue
Block a user