05-qt-jenkins-integrator.t 37.77 KiB
#!/usr/bin/env perl
#############################################################################
##
## Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
## Contact: http://www.qt-project.org/legal
##
## This file is part of the Quality Assurance module of the Qt Toolkit.
##
## $QT_BEGIN_LICENSE:LGPL$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and Digia.  For licensing terms and
## conditions see http://qt.digia.com/licensing.  For further information
## use the contact form at http://qt.digia.com/contact-us.
## GNU Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 2.1 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL included in the
## packaging of this file.  Please review the following information to
## ensure the GNU Lesser General Public License version 2.1 requirements
## will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
## In addition, as a special exception, Digia gives you certain additional
## rights.  These rights are described in the Digia Qt LGPL Exception
## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 3.0 as published by the Free Software
## Foundation and appearing in the file LICENSE.GPL included in the
## packaging of this file.  Please review the following information to
## ensure the GNU General Public License version 3.0 requirements will be
## met: http://www.gnu.org/copyleft/gpl.html.
## $QT_END_LICENSE$
#############################################################################
=head1 NAME
05-qt-jenkins-integrator.t - basic test for qt-jenkins-integrator
=head1 DESCRIPTION
This test exercises various CI state functions (do_state_*).
Each state is called individually with a certain set of parameters; behavior from
Jenkins or Gerrit is simulated/mocked, and the test verifies that the system would
transition to the next state as expected, with the expected arguments.
=cut
use strict;
use warnings;
use AnyEvent;
use Carp;
use Coro;
use English qw( -no_match_vars );
use Env::Path;
use File::Spec::Functions;
use File::Temp;
use FindBin;
use JSON;
use Test::Builder;
use Test::Exception;
use Test::More;
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
use Sub::Override; use URI; use lib catfile( $FindBin::Bin, qw(.. .. lib perl5) ); use QtQA::Test::More qw(create_mock_command is_or_like); use QtQA::WWW::Util qw(www_form_urlencoded); my $SCRIPT = catfile( $FindBin::Bin, qw(.. qt-jenkins-integrator.pl) ); my $PACKAGE = 'QtQA::GerritJenkinsIntegrator'; my $SOON = .1; # a small amount of time if ($OSNAME =~ m{win32}i) { plan skip_all => "$PACKAGE is not supported on $OSNAME"; } # expected query string when looking at the build queue my $QUEUE_JSON_QUERY_STRING = 'depth=2&tree=builds[number,actions[parameters[name,value]]]'; # expected query string when monitoring a build my $BUILD_JSON_QUERY_STRING = 'depth=2&tree=building,number,url,result,fullDisplayName,timestamp,duration,runs[building,number,url,result,fullDisplayName,timestamp,duration]'; # base configuration used in tests; override where appropriate my $GERRIT_BASE = 'ssh://gerrit.example.com'; my %CONFIG = ( Global => { # default is to poll very fast to keep test runtime down; # tests which are trying to exercise non-poll code paths should # locally increase these values. StagingQuietPeriod => $SOON, StagingMaximumWait => $SOON*10, StagingPollInterval => $SOON, JenkinsUrl => 'http://jenkins.example.com', JenkinsUser => 'jenkinsuser', JenkinsToken => 'jenkinstoken', JenkinsPollInterval => $SOON, JenkinsTriggerPollInterval => $SOON, JenkinsTriggerTimeout => $SOON*10, }, prjA => { GerritUrl => URI->new("$GERRIT_BASE/prj/prjA"), GerritBranch => 'mybranch', } ); my @logs; require_ok( $SCRIPT ); sub mock_cmd { my ($cmd, @sequence) = @_; my $tmpdir = File::Temp->newdir( 'qt-jenkins-integrator-test.XXXXXX', TMPDIR => 1 ); create_mock_command( name => $cmd, directory => $tmpdir, sequence => \@sequence, ); return $tmpdir; } sub mock_http { my ($label, $mock_ref) = @_; return unless $mock_ref;
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
my $override = Sub::Override->new(); my $sub = sub { my ($method, $url, %args) = @_; my $expected = $mock_ref; if (ref($expected) eq 'ARRAY') { $expected = shift @{ $mock_ref }; } is( $method, $expected->{ method }, "[$label] http method" ) if $expected->{ method }; is( $url, $expected->{ url }, "[$label] http url" ) if $expected->{ url }; is_or_like( $args{ body }, $expected->{ body }, "[$label] http body" ) if $expected->{ body }; return ( $expected->{ result_body } || q{}, $expected->{ result_headers } || { Status => 500, Reason => 'no result specified in mock_http' } ); }; foreach my $to_mock ("${PACKAGE}::blocking_http_request", "QtQA::WWW::Util::blocking_http_request") { $override->replace( $to_mock, $sub ); } return $override; } # convenience function to create a gerrit stream-events event for test_state_machine sub gerrit_updated_soon { my ($uri, $project) = @_; $uri ||= $GERRIT_BASE; $project ||= 'prj/prjA'; return [ $SOON, $SOON, $uri, { type => 'ref-updated', refUpdate => { project => $project } } ]; } # Returns a mock Jenkins build object sub object_for_build { my (%args) = @_; my %toplevel; my %parameters; # each 'run' is itself a build my @runs = @{ delete($args{ runs }) || [] }; @runs = map { object_for_build( %{$_} ) } @runs; # permitted toplevel attributes; anything else is a 'parameter' foreach my $key (qw(number building result fullDisplayName)) { my $value = delete $args{ $key }; if (defined($value)) { $toplevel{ $key } = $value; } } %parameters = %args; my $object = \%toplevel; while (my ($name, $value) = each %parameters) { push @{ $object->{ actions }[0]{ parameters } }, { name => $name, value => $value }; } if (@runs) { $object->{ runs } = \@runs; }
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
return $object; } # Returns a mock Jenkins build object, JSON encoded sub json_for_build { my (%args) = @_; return encode_json object_for_build( %args ) } # Returns a mock Jenkins object containing multiple builds, JSON encoded sub json_for_builds { my (@args) = @_; my @builds = map { object_for_build( %{ $_ } ) } @args; return encode_json( {builds => \@builds} ); } # Returns a regex for matching the given $pattern as a query string portion sub qr_query_string { my ($pattern) = @_; return qr{ (?:&|\A) # beginning of string or of argument $pattern (?:&|\z) # end of string or of argument }xms; } sub query_string_patterns { my (@args) = @_; # for the entire string to match exactly this query, every individual pattern must match... my @part_patterns = map { qr_query_string($_) } @args; # ... and there must be no other query string components my $outer_pattern = '\A' . join( '&', map { '[^&]+' } @args ) . '\z'; return (@part_patterns, qr{$outer_pattern}); } sub http_responses_for_builds { my ($mock_http_base, @builds) = @_; return [ map { +{ # + helps perlcritic parse this as hashref, not block %{ $mock_http_base }, result_headers => {Status => 200}, result_body => json_for_build( %{$_} ), } } @builds ]; } # Tests a single state machine function. # # 'in' parameters refer to the state input, while 'out' parameters refer # to the expected state output. Omitted 'out' parameters aren't tested. # # The resulting stash is returned in case additional checks are desired. sub test_state_machine { my (%args) = @_; my $object = $args{ object } || croak 'missing object';
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
my $project_id = $args{ project_id } || croak 'missing project_id'; my $in_state = $args{ in_state } || croak 'missing in_state'; my $in_stash = $args{ in_stash } || {}; my $out_state = $args{ out_state }; my $out_stash = $args{ out_stash }; my $config = $args{ config } || $object->{ config }; my $label = $args{ label } || 'basic'; my $mock_git = $args{ mock_git }; my $mock_ssh = $args{ mock_ssh }; my $mock_http = $args{ mock_http }; my $mock_summarize_jenkins_build = $args{ mock_summarize_jenkins_build }; my $mock_sleep = $args{ mock_sleep }; my $logs = $args{ logs }; my $gerrit_events = $args{ gerrit_events }; my $throws_ok = $args{ throws_ok }; $label = "$in_state $label"; # make failures come from caller context local $Test::Builder::Level = $Test::Builder::Level + 1; local $ENV{ PATH } = $ENV{ PATH }; local $object->{ config } = $config; my @mockdirs; my @overrides; if ($mock_git) { push @mockdirs, mock_cmd( 'git', @{ $mock_git } ); } if ($mock_ssh) { push @mockdirs, mock_cmd( 'ssh', @{ $mock_ssh } ); } if ($mock_summarize_jenkins_build) { push @mockdirs, mock_cmd( 'fake-summarize-jenkins-build', @{ $mock_summarize_jenkins_build } ); push @overrides, Sub::Override->new( "${PACKAGE}::summarize_jenkins_build_cmd" => sub { 'fake-summarize-jenkins-build' } ); } if (@mockdirs) { Env::Path->PATH->Prepend( @mockdirs ); } if ($mock_sleep) { push @overrides, Sub::Override->new( 'Coro::AnyEvent::sleep' => sub {} ); } push @overrides, mock_http( $label, $mock_http ); my @gerrit_event_timers = map { my ($after, $interval, $uri, $gerrit_event) = @{ $_ }; if (!ref($uri)) { $uri = URI->new( $uri ); } AE::timer( $after, $interval, sub { $object->handle_gerrit_stream_event( $uri, $gerrit_event ); }); } @{ $gerrit_events || [] }; my (undef, undef, undef, $caller_name) = caller(1); $caller_name =~ s{^.*::}{}; @logs = (); my %stash; my $sub_name = "do_state_$in_state"; $sub_name =~ s{-}{_}g;
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
my $sub_ref = $object->can( $sub_name ); ok( $sub_ref, "[$label] $in_state is a known state" ) || return; %stash = %{ $in_stash }; my $next_state; my $run = sub { $next_state = $sub_ref->( $object, $project_id, \%stash ); }; if ($throws_ok) { &throws_ok( $run, $throws_ok, "[$label] throws OK" ); } else { &lives_ok( $run, "[$label] doesn't die" ); } if ($out_state) { is( $next_state, $out_state, "[$label] $in_state -> $out_state" ); } if ($out_stash) { is_deeply( \%stash, $out_stash, "[$label] stash" ); } if ($logs) { is_deeply( \@logs, $logs, "[$label] logs" ); } return \%stash; } ## no critic Subroutines::RequireArgUnpacking - allows for convenient syntax when overriding %test sub test_state_wait_until_staging_branch_exists { my (%test) = ( @_, in_state => 'wait-until-staging-branch-exists', out_stash => {}, out_state => 'wait-for-staging', logs => [], ); { # staging branch eventually exists, discovered by polling test_state_machine( %test, label => 'poll', mock_git => [ {}, {}, { stdout => '98921005a7df200cac9e488db4df4bf38ba85478 refs/staging/mybranch' }, ], ); } { # branch is discovered by gerrit event, not polling # make poll interval large so gerrit events arrive first local $CONFIG{ Global }{ StagingPollInterval } = 10; test_state_machine( %test, label => 'non-poll', gerrit_events => [ gerrit_updated_soon(), ], mock_git => [ {}, { stdout => '98921005a7df200cac9e488db4df4bf38ba85478 refs/staging/mybranch' }, ], logs => [ 'woke up by event from gerrit' ],
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
); } return; } sub test_state_start { my (%test) = ( @_, in_state => 'start', in_stash => {hi => 'there'}, out_stash => {}, # 'start' always empties the stash logs => [], ); { test_state_machine( %test, label => 'no staging branch', mock_git => [ # simulate staging branch doesn't exist (ls-remote has no output) {} ], out_state => 'wait-until-staging-branch-exists' ); } { test_state_machine( %test, label => 'staging branch', mock_git => [ # simulate staging branch exists { stdout => '98921005a7df200cac9e488db4df4bf38ba85478 refs/staging/mybranch' }, ], out_state => 'wait-for-staging', ); } return; } sub test_state_wait_for_staging { my (%test) = ( @_, in_state => 'wait-for-staging', in_stash => {}, logs => [], ); { # staged changes discovered by polling test_state_machine( %test, label => 'poll', mock_ssh => [ # simulate nothing staged for first couple of staging-ls; then eventually some activity appears {}, {}, {stdout => qq{some change\n}}, ], out_state => 'wait-for-staging-quiet', out_stash => { staged => 'some change', }, logs => [], ); }
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
{ # staged changes discovered by gerrit events; # put poll interval large so the events arrive first local $CONFIG{ Global }{ StagingPollInterval } = 10; test_state_machine( %test, label => 'non-poll', mock_ssh => [ {}, {stdout => qq{another change\n}}, ], gerrit_events => [ gerrit_updated_soon(), ], out_state => 'wait-for-staging-quiet', out_stash => { staged => 'another change', }, logs => [ 'woke up by event from gerrit' ], ); } return; } sub test_state_wait_for_staging_quiet { my (%test) = ( @_, in_state => 'wait-for-staging-quiet', ); { # polling determines staging branch is stable; start a build test_state_machine( %test, label => 'quiet, poll', mock_ssh => [ # stable staging branch ({stdout => 'c'}) x 2 ], in_stash => { staged => 'c' }, out_stash => { staged => 'c' }, out_state => 'staging-new-build', logs => ['done waiting for staging'], ); } { # polling, changes keep appearing and disappearing in staging branch; # eventually timeout and start a build test_state_machine( %test, label => 'timeout, poll', mock_ssh => [ # content oscillates as things are staged, unstaged ({stdout => 'a'}, {stdout => 'ab'}) x 10 ], in_stash => { staged => 'c' }, out_stash => { staged => 'c' }, out_state => 'staging-new-build', ); # we don't know exactly how many times 'staging activity occurred' should be logged, # it depends on timing; should be a couple at least is_deeply( [ @logs[0..2] ], [ ('staging activity occurred.') x 3 ] ); }
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
{ # non-polling, eventually all changes are unstaged, so return to waiting for staging local $CONFIG{ Global }{ StagingPollInterval } = 10; local $CONFIG{ Global }{ StagingQuietPeriod } = 20; local $CONFIG{ Global }{ StagingMaximumWait } = 60; test_state_machine( %test, label => 'unstaged, non-poll', mock_ssh => [ # content oscillates as things are staged, unstaged, then eventually everything # is unstaged ({stdout => 'a'}, {stdout => 'ab'}) x 2, {}, ], in_stash => { staged => 'c' }, out_stash => { }, out_state => 'wait-for-staging', gerrit_events => [ gerrit_updated_soon(), ], ); is_deeply( [ @logs[0..3] ], [ ('woke up by event from gerrit', 'staging activity occurred.') x 2 ] ); } return; } sub test_state_staging_new_build { my (%test) = ( @_, in_state => 'staging-new-build', ); { # succeeds (after an initial error) and moves to check-staged-changes my $stash = test_state_machine( %test, label => 'success', mock_ssh => [ # fake an error to ensure we can recover {stderr => q{some error}, exitcode => 2}, {} ], in_stash => {}, out_state => 'check-staged-changes', ); # build ref should be exported to stash my $build_ref = $stash->{ build_ref }; ok( $build_ref, 'build ref is set' ); like( $build_ref, qr{^refs/builds/mybranch_\d+$}, 'build ref looks OK' ); my $warning = shift @logs; like( $warning, qr{command \[ssh\].*\[staging-new-build\].*exited with status \d+ \[retry}, 'retried ssh OK' ); is_deeply( \@logs, ["created build ref $build_ref"], 'logs' ); } return; } sub test_state_check_staged_changes { my (%test) = ( @_,
631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
in_state => 'check-staged-changes', ); { # check staged changes always proceeds to trigger jenkins test_state_machine( %test, mock_ssh => [ {stdout => qq{some stuff\nmore stuff\n}}, ], in_stash => {build_ref => 'refs/builds/mybranch_1234'}, out_state => 'trigger-jenkins', stash => {staged => qq{some stuff\nmore stuff}}, logs => [], ); } return; } sub test_state_trigger_jenkins { my (%test) = ( @_, in_state => 'trigger-jenkins', in_stash => {build_ref => 'refs/builds/somebuild'}, ); # we expect the following HTTP request to be sent out # TODO: verify the postdata my %mock_http_base = ( method => 'POST', url => 'http://jenkins.example.com/job/prjA/buildWithParameters', body => [query_string_patterns( qr/qt_ci_request_id=[0-9a-f]{8}/, quotemeta(www_form_urlencoded(qt_ci_git_url => 'ssh://gerrit.example.com/prj/prjA')), www_form_urlencoded(qt_ci_git_ref => 'refs/builds/somebuild'), )] ); { test_state_machine( %test, label => 'error', mock_http => { %mock_http_base, result_headers => { Status => 404, Reason => 'frobnitz' } }, throws_ok => qr{new build for prjA failed: 404 frobnitz}, ); } { my $stash = test_state_machine( %test, label => 'success', mock_http => { %mock_http_base, result_headers => { Status => 200 } }, out_state => 'wait-for-jenkins-build-active', ); like( $stash->{ request_id }, qr{\A[0-9a-f]{8}\z}, 'request_id is set' ); } } sub test_state_wait_for_jenkins_build_active { my (%test) = ( @_, in_state => 'wait-for-jenkins-build-active', in_stash => { request_id => 'a1b2c3d4' }, ); # we expect the following HTTP request to be sent out my %mock_http_base = (
701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
method => 'GET', url => "http://jenkins.example.com/job/prjA/api/json?$QUEUE_JSON_QUERY_STRING" ); { test_state_machine( %test, label => 'http error', mock_http => { %mock_http_base, result_headers => { Status => 503, Reason => 'server down' } }, throws_ok => qr{server down}, ); } { test_state_machine( %test, label => 'not json error', mock_http => { %mock_http_base, result_headers => { Status => 200 }, result_body => "this ain't json", }, throws_ok => qr{.}, ); } { test_state_machine( %test, label => 'missing data error', mock_http => { %mock_http_base, result_headers => { Status => 200 }, result_body => '{"builds":{"incorrect":"data"}}', }, throws_ok => qr{JSON schema error}, ); } { test_state_machine( %test, label => 'timeout', mock_http => { %mock_http_base, result_headers => { Status => 200 }, result_body => json_for_builds( # some builds, but not the right ones {number => 41, qt_ci_request_id => 'aabbccdd'}, {number => 42, qt_ci_request_id => 'eeff0011'}, ), }, throws_ok => qr{Jenkins did not start a build with request ID a1b2c3d4}, ); } { test_state_machine( %test, label => 'success', mock_http => { %mock_http_base, result_headers => { Status => 200 }, result_body => json_for_builds( # a couple of unrelated builds, plus the real one {number => 41, qt_ci_request_id => 'aabbccdd'}, {number => 42, qt_ci_request_id => 'a1b2c3d4'}, {number => 43} ), },
771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
out_stash => {build_number => 42}, out_state => 'set-jenkins-build-description', ); } return; } sub test_state_set_jenkins_build_description { my (%test) = ( @_, in_state => 'set-jenkins-build-description', in_stash => { build_number => 1234, staged => qq{2b63d8d760c80ebf5fc939a35fd133a62bfb3fc2 123,45 do this\n} .qq{63d8d760c80ebf5fc939a35fd133a62bfb3fc22b 67,89 do that\n} }, ); # we expect the following HTTP request to be sent out # TODO: verify the postdata my %mock_http_base = ( method => 'POST', url => 'http://jenkins.example.com/job/prjA/1234/submitDescription', body => [query_string_patterns( quotemeta(www_form_urlencoded( description => qq{Tested changes:<ul>\n} .qq{<li><a href="http://gerrit.example.com/123">http://gerrit.example.com/123</a> [PS45] - do this</li>\n} .qq{<li><a href="http://gerrit.example.com/67">http://gerrit.example.com/67</a> [PS89] - do that</li>\n} .qq{</ul>} )) )] ); { test_state_machine( %test, label => 'http request fails', mock_http => { %mock_http_base, result_headers => {Status => 503, Reason => 'quux'}, }, throws_ok => qr{set description for prjA 1234 failed: 503 quux}, ); } { test_state_machine( %test, label => 'success', out_state => 'monitor-jenkins-build', mock_http => { %mock_http_base, result_headers => {Status => 200}, } ); } return; } sub test_state_monitor_jenkins_build { my (%test) = ( @_, in_state => 'monitor-jenkins-build', in_stash => {build_number => 1234}, );
841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
my %mock_http_base = ( method => 'GET', url => "http://jenkins.example.com/job/prjA/1234/api/json?$BUILD_JSON_QUERY_STRING" ); { test_state_machine( %test, label => 'http error', mock_http => { %mock_http_base, result_headers => {Status => 503, Reason => 'error37'}, }, throws_ok => qr{fetch.*: 503 error37}, ); } { test_state_machine( %test, label => 'json error', mock_http => { %mock_http_base, result_headers => {Status => 200}, result_body => q{not valid json}, }, throws_ok => qr{.*}, ); } { test_state_machine( %test, label => 'poll, eventually completed build', mock_http => http_responses_for_builds( \%mock_http_base, {number => 1234, building => 1}, {number => 1234, building => 1, runs => [ { number => 1234, building => 0, result => 'FAILED', fullDisplayName => 'some cfg' } ]}, {number => 1234, building => 0}, ), out_state => 'parse-jenkins-build', ); } { local $CONFIG{ Global }{ JenkinsCancelOnFailure } = 1; test_state_machine( %test, label => 'poll, cancel build', mock_http => http_responses_for_builds( \%mock_http_base, {number => 1234, building => 1}, {number => 1234, building => 1, runs => [ { number => 1234, building => 0, result => 'FAILED', fullDisplayName => 'some cfg' } ]}, ), out_state => 'cancel-jenkins-build',
911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
); } return; } sub test_state_cancel_jenkins_build { my (%test) = ( @_, in_state => 'cancel-jenkins-build', in_stash => { build => { number => 1234, url => 'http://jenkinsZ.example.com/job/prjAZ/1234Z', building => 1, } }, ); my %mock_http_base = ( method => 'POST', url => $test{ in_stash }{ build }{ url } . '/stop', ); { test_state_machine( %test, label => 'http error', mock_http => { %mock_http_base, result_headers => {Status => 503, Reason => 'error37'}, }, throws_ok => qr{cancel prjA build 1234 failed: 503 error37}, ); } foreach my $code (qw(200 302)) { test_state_machine( %test, label => "success, http $code", mock_http => { %mock_http_base, result_headers => {Status => $code}, }, out_state => 'parse-jenkins-build', out_stash => { build => { %{ $test{ in_stash }{ build } }, building => undef, result => 'ABORTED', aborted_by_integrator => 1 } } ); } return; } sub test_state_parse_jenkins_build { my (%test) = ( @_, in_state => 'parse-jenkins-build', in_stash => { build_ref => 'refs/builds/testbuild', staged => qq{2b63d8d760c80ebf5fc939a35fd133a62bfb3fc2 111,1 do this\n} .qq{63d8d760c80ebf5fc939a35fd133a62bfb3fc22b 22,3 do that\n}, build => {
981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
number => 1234, url => 'http://jenkinsZ.example.com/job/prjAZ/1234Z', result => 'some_result', } }, ); # "Tested changes" text block, for the above stash my $tested_changes = qq{ Tested changes (refs/builds/testbuild):\n} .qq{ http://gerrit.example.com/111 [PS1] - do this\n} .qq{ http://gerrit.example.com/22 [PS3] - do that}; # Portion of in_stash expected to appear unmodified in the out_stash my %common_stash = map { $_ => $test{ in_stash }{ $_ } } qw( build_ref staged ); { test_state_machine( %test, label => 'yaml error', mock_summarize_jenkins_build => [ {stdout => 'hi there'}, ], throws_ok => qr{YAML error:}i, ); } { test_state_machine( %test, label => 'success', mock_summarize_jenkins_build => [ {stdout => qq{formatted: build succeeded!\n}}, ], out_state => 'handle-jenkins-build-result', out_stash => { %common_stash, parsed_build => { result => $test{ in_stash }{ build }{ result }, formatted => qq{build succeeded!\n\n$tested_changes} } }, ); } { test_state_machine( %test, label => "don't retry on partial set of should_retry", mock_summarize_jenkins_build => [{ stdout => qq{formatted: build failed!\n} .qq{runs:\n - should_retry: 1\n - should_retry: 0\n} }], out_state => 'handle-jenkins-build-result', out_stash => { %common_stash, parsed_build => { result => $test{ in_stash }{ build }{ result }, formatted => qq{build failed!\n\n} .qq{ Tested changes (refs/builds/testbuild):\n} .qq{ http://gerrit.example.com/111 [PS1] - do this\n} .qq{ http://gerrit.example.com/22 [PS3] - do that}, runs => [{should_retry => 1}, {should_retry => 0}], }
1051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
}, ); } { test_state_machine( %test, label => 'retry when all runs should_retry', mock_summarize_jenkins_build => [{ stdout => qq{formatted: build failed!\n} .qq{runs:\n - should_retry: 1\n - should_retry: 1\n} }], out_state => 'handle-jenkins-build-result', out_stash => { %common_stash, parsed_build => { should_retry => 1, result => $test{ in_stash }{ build }{ result }, formatted => qq{build failed!\n\n$tested_changes}, runs => [{should_retry => 1}, {should_retry => 1}], }, build_attempt => 2 }, mock_sleep => 1, logs => [ 'build log indicates we should retry', 'will retry in 32 seconds' ], ); } { local $CONFIG{ Global }{ BuildAttempts } = 1; test_state_machine( %test, label => 'eventually give up retrying', mock_summarize_jenkins_build => [{ stdout => qq{formatted: build failed!\n} .qq{runs:\n - should_retry: 1\n - should_retry: 1\n} }], out_state => 'handle-jenkins-build-result', out_stash => { %common_stash, parsed_build => { result => $test{ in_stash }{ build }{ result }, formatted => qq{build failed!\n\n$tested_changes}, runs => [{should_retry => 1}, {should_retry => 1}], }, build_attempt => 1, }, logs => [ 'build log indicates we should retry', 'already tried 1 times, giving up' ], ); } return; } sub test_state_handle_jenkins_build_result { my (%test) = ( @_, in_state => 'handle-jenkins-build-result', in_stash => { build_ref => 'refs/builds/testbuild', parsed_build => {result => 'SUCCESS'}, }, ); {
1121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
my %out_stash = %{ $test{ in_stash } }; delete $out_stash{ parsed_build }; test_state_machine( %test, label => 'should_retry', in_stash => { %{ $test{ in_stash } }, parsed_build => { should_retry => 1 } }, out_state => 'trigger-jenkins', out_stash => \%out_stash, ); } { # TODO: verify the arguments and stdin of staging-approve # with a few different builds test_state_machine( %test, label => 'OK', mock_ssh => [ # simulate an error, then recover from it {stderr => 'some error!', exitcode => 2}, {exitcode => 0}, ], out_state => 'send-mail', ); } return; } sub test_state_send_mail { my (%test) = ( @_, in_state => 'send-mail', out_state => 'start', in_stash => { build_ref => 'refs/builds/testbuild', parsed_build => { result => 'SUCCESS', formatted => 'build succeeded!', }, }, ); my $mailmsg_count = 0; my $override = Sub::Override->new( 'Mail::Sender::MailMsg' => sub { # TODO: actually verify the content of the mails. ++$mailmsg_count; } ); { local $CONFIG{ Global }{ MailTo } = undef; test_state_machine( %test, label => 'do nothing if mail disabled', out_state => 'start', ); is( $mailmsg_count, 0, 'no mail sent' ); } { local $CONFIG{ Global }{ MailTo } = ['addr1@example.com','addr2@example.com'], test_state_machine( %test, label => 'send a mail if enabled',
1191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
out_state => 'start', ); is( $mailmsg_count, 1, 'one mail sent' ); } return; } sub test_state_error { my (%test) = ( @_, in_state => 'error', in_stash => { state => { name => 'some-state', stash => { foo => 'bar' }, } }, mock_sleep => 1, ); { test_state_machine( %test, label => 'retry', in_stash => { %{ $test{ in_stash } }, error => 'some error!', error_count => 3, }, out_state => 'some-state', out_stash => { %{ $test{ in_stash }{ state }{ stash }}, error_count => 3, }, logs => ['some error!, retry in 8 seconds', 'resuming from error into state some-state'], ); } { # This test will suspend the calling coro, so we need to run it from its own coro # (otherwise we'll deadlock). my $coro = async { local $Coro::current->{ desc } = 'test coro'; test_state_machine( %test, label => 'suspend', in_stash => { %{ $test{ in_stash } }, error => 'some error!', error_count => 1000, }, out_state => 'some-state', out_stash => { %{ $test{ in_stash }{ state }{ stash }}, error_count => 0, }, logs => [ 'some error!, occurred repeatedly.', "Suspending for investigation; to resume: kill -USR2 $$", 'resuming from error into state some-state', ], ); }; # keep sending the 'wake up' signal until it wakes up my $timer = AE::timer( $SOON, $SOON, sub { $test{ object }->resume_from_error_signal()->broadcast() } );
126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323
$coro->join(); } return; } sub test_states { my $object = $PACKAGE->new(); ok( $object ); # Set up a logger which injects all messages back to us local $object->{ logger } = Log::Dispatch->new( outputs => [ ['Null', min_level => 'debug'] ], callbacks => sub { my (%data) = @_; push @logs, $data{ message }; return $data{ message }; } ); # pass warnings through logger, as done in $PACKAGE::run() local $Coro::State::WARNHOOK = sub { $object->logger()->warning( @_ ); }; # base parameters for test_state_machine, to be overridden where appropriate. my %base_test = ( object => $object, config => \%CONFIG, project_id => 'prjA', ); test_state_start( %base_test ); test_state_wait_until_staging_branch_exists( %base_test ); test_state_wait_for_staging( %base_test ); test_state_wait_for_staging_quiet( %base_test ); test_state_staging_new_build( %base_test ); test_state_check_staged_changes( %base_test ); test_state_trigger_jenkins( %base_test ); test_state_wait_for_jenkins_build_active( %base_test ); test_state_set_jenkins_build_description( %base_test ); test_state_monitor_jenkins_build( %base_test ); test_state_cancel_jenkins_build( %base_test ); test_state_parse_jenkins_build( %base_test ); test_state_handle_jenkins_build_result( %base_test ); test_state_send_mail( %base_test ); test_state_error( %base_test ); return; } sub run { test_states(); return; } run(); done_testing();