Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 1 | #!/usr/bin/perl |
| 2 | # |
| 3 | # Copyright (c) 2019 Peter Pentchev |
| 4 | # All rights reserved. |
| 5 | # |
| 6 | # Redistribution and use in source and binary forms, with or without |
| 7 | # modification, are permitted provided that the following conditions |
| 8 | # are met: |
| 9 | # 1. Redistributions of source code must retain the above copyright |
| 10 | # notice, this list of conditions and the following disclaimer. |
| 11 | # 2. Redistributions in binary form must reproduce the above copyright |
| 12 | # notice, this list of conditions and the following disclaimer in the |
| 13 | # documentation and/or other materials provided with the distribution. |
| 14 | # |
| 15 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND |
| 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| 18 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE |
| 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
| 21 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
| 22 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| 23 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| 24 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| 25 | # SUCH DAMAGE. |
| 26 | |
| 27 | use v5.10; |
| 28 | use strict; |
| 29 | use warnings; |
| 30 | |
| 31 | use File::Temp; |
| 32 | use Getopt::Std; |
| 33 | use List::Util qw(all); |
| 34 | use Path::Tiny; |
| 35 | |
| 36 | use constant VERSION_STRING => '0.1.0'; |
| 37 | |
| 38 | use constant RE_CHANGE_ID => qr{ |
| 39 | ^ \s* Change-Id: \s* (?<id> I[0-9a-f]+ ) \s* $ |
| 40 | }xi; |
| 41 | |
| 42 | use constant PATCHFILES => { |
| 43 | initial => [qw(add modify)], |
| 44 | conflict => [qw(conflict)], |
| 45 | more => [qw(another)], |
| 46 | |
| 47 | all => { |
| 48 | add => 'add-readme.patch', |
| 49 | modify => 'modify-readme.patch', |
| 50 | conflict => 'conflict-in-readme.patch', |
| 51 | another => 'another-file.patch', |
| 52 | }, |
| 53 | }; |
| 54 | |
| 55 | my $debug = 0; |
| 56 | |
| 57 | sub usage($) |
| 58 | { |
| 59 | my ($err) = @_; |
| 60 | my $s = <<EOUSAGE |
| 61 | Usage: gifn-test [-Nv] cmd [arg...] |
| 62 | gifn-test -V | -h | --version | --help |
| 63 | gifn-test --features |
| 64 | |
| 65 | -h display program usage information and exit |
| 66 | -N no-operation mode |
| 67 | -V display program version information and exit |
| 68 | -v verbose operation; display diagnostic output |
| 69 | EOUSAGE |
| 70 | ; |
| 71 | |
| 72 | if ($err) { |
| 73 | die $s; |
| 74 | } else { |
| 75 | print "$s"; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | sub version() |
| 80 | { |
| 81 | say 'gifn-test '.VERSION_STRING; |
| 82 | } |
| 83 | |
| 84 | sub features() |
| 85 | { |
| 86 | say 'Features: gifn_test='.VERSION_STRING; |
| 87 | } |
| 88 | |
| 89 | sub debug($) |
| 90 | { |
| 91 | say STDERR "RDBG $_[0]" if $debug; |
| 92 | } |
| 93 | |
| 94 | sub check_wait_result($ $ $) |
| 95 | { |
| 96 | my ($stat, $pid, $name) = @_; |
| 97 | my $sig = $stat & 127; |
| 98 | if ($sig != 0) { |
| 99 | die "Program '$name' (pid $pid) was killed by signal $sig\n"; |
| 100 | } else { |
| 101 | my $code = $stat >> 8; |
| 102 | if ($code != 0) { |
| 103 | die "Program '$name' (pid $pid) exited with non-zero status $code\n"; |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | sub run_command_unchomped(@) |
| 109 | { |
| 110 | my (@cmd) = @_; |
| 111 | my $name = $cmd[0]; |
| 112 | |
| 113 | my $pid = open my $f, '-|'; |
| 114 | if (!defined $pid) { |
| 115 | die "Could not fork for $name: $!\n"; |
| 116 | } elsif ($pid == 0) { |
| 117 | debug "About to run '@cmd'"; |
| 118 | exec { $name } @cmd; |
| 119 | die "Could not execute '$name': $!\n"; |
| 120 | } |
| 121 | my @res = <$f>; |
| 122 | close $f; |
| 123 | check_wait_result $?, $pid, $name; |
| 124 | return @res; |
| 125 | } |
| 126 | |
| 127 | sub run_command(@) |
| 128 | { |
| 129 | my (@cmd) = @_; |
| 130 | my @lines = run_command_unchomped @cmd; |
| 131 | chomp for @lines; |
| 132 | return @lines; |
| 133 | } |
| 134 | |
| 135 | sub run_failing_command(@) |
| 136 | { |
| 137 | my (@cmd) = @_; |
| 138 | |
| 139 | my @lines = eval { |
| 140 | run_command @cmd; |
| 141 | }; |
| 142 | my $err = $@; |
| 143 | if (!defined $err) { |
| 144 | die "The '@cmd' command did not fail and output ". |
| 145 | scalar(@lines)." lines of text\n"; |
| 146 | } |
| 147 | return $err; |
| 148 | } |
| 149 | |
| 150 | sub help_or_version($) |
| 151 | { |
| 152 | my ($opts) = @_; |
| 153 | my $has_dash = defined $opts->{'-'}; |
| 154 | my $dash_help = $has_dash && $opts->{'-'} eq 'help'; |
| 155 | my $dash_version = $has_dash && $opts->{'-'} eq 'version'; |
| 156 | my $dash_features = $has_dash && $opts->{'-'} eq 'features'; |
| 157 | |
| 158 | if ($has_dash && !$dash_help && !$dash_version && !$dash_features) { |
| 159 | warn "Invalid long option '".$opts->{'-'}."' specified\n"; |
| 160 | usage 1; |
| 161 | } |
| 162 | version if $opts->{V} || $dash_version; |
| 163 | usage 0 if $opts->{h} || $dash_help; |
| 164 | features if $dash_features; |
| 165 | exit 0 if $opts->{V} || $opts->{h} || $has_dash; |
| 166 | } |
| 167 | |
| 168 | sub detect_utf8_locale() |
| 169 | { |
| 170 | my @lines = run_command 'locale', '-a'; |
| 171 | my %avail = map { $_ => 1 } @lines; |
| 172 | for my $pref (qw(POSIX C en_US en_CA en_GB en_AU en)) { |
| 173 | for my $ext (qw(UTF-8 utf8)) { |
| 174 | my $value = "$pref.$ext"; |
| 175 | return $value if $avail{$value}; |
| 176 | } |
| 177 | } |
| 178 | die "Could not find a suitable UTF-8 output locale\n"; |
| 179 | } |
| 180 | |
| 181 | sub git_status_ok($) |
| 182 | { |
| 183 | my ($cfg) = @_; |
| 184 | |
| 185 | my @lines = run_command @{$cfg->{git}}, 'status', '--short'; |
| 186 | die "git status --short returned @lines\n" if @lines; |
| 187 | } |
| 188 | |
| 189 | sub run_gifn_am($ $) |
| 190 | { |
| 191 | my ($cfg, $short) = @_; |
| 192 | |
| 193 | my @lines = run_command @{$cfg->{gifn}}, 'am', |
| 194 | $cfg->{patches}->{$short}->{patch}; |
| 195 | git_status_ok $cfg; |
| 196 | return @lines; |
| 197 | } |
| 198 | |
| 199 | sub run_failing_gifn_am($ $) |
| 200 | { |
| 201 | my ($cfg, $short) = @_; |
| 202 | |
| 203 | return run_failing_command @{$cfg->{gifn}}, 'am', |
| 204 | $cfg->{patches}->{$short}->{patch}; |
| 205 | } |
| 206 | |
| 207 | sub get_current_changes($) |
| 208 | { |
| 209 | my ($cfg) = @_; |
| 210 | |
| 211 | return map { |
| 212 | $_ =~ RE_CHANGE_ID ? ($+{id}) : () |
| 213 | } run_command @{$cfg->{git}}, 'log', '--reverse'; |
| 214 | } |
| 215 | |
| 216 | sub equal_lists($ $) |
| 217 | { |
| 218 | my ($expected, $got) = @_; |
| 219 | |
| 220 | return @{$expected} == @{$got} && all { $expected->[$_] eq $got->[$_] } 0..$#{$expected}; |
| 221 | } |
| 222 | |
Peter Penchev | a1f004e | 2019-12-03 17:23:25 +0200 | [diff] [blame] | 223 | sub git_init($) |
| 224 | { |
| 225 | my ($cfg) = @_; |
| 226 | |
| 227 | run_command @{$cfg->{git}}, 'init'; |
| 228 | run_command @{$cfg->{git}}, 'config', '--local', |
| 229 | 'user.name', 'Somebody'; |
| 230 | run_command @{$cfg->{git}}, 'config', '--local', |
| 231 | 'user.email', 'someone@example.com'; |
| 232 | } |
| 233 | |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 234 | sub setup_repo($) |
| 235 | { |
| 236 | my ($cfg) = @_; |
| 237 | |
| 238 | chdir $cfg->{repo} or die "Could not change into $cfg->{repo}: $!\n"; |
Peter Penchev | a1f004e | 2019-12-03 17:23:25 +0200 | [diff] [blame] | 239 | git_init $cfg; |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 240 | git_status_ok $cfg; |
| 241 | |
| 242 | while (my ($short, $fname) = each %{PATCHFILES->{all}}) { |
| 243 | my $patch = $cfg->{data}->child($fname); |
| 244 | my @lines = $patch->lines_utf8({ chomp => 1 }); |
| 245 | my @id = map { $_ =~ RE_CHANGE_ID ? ($+{id}): () } @lines; |
| 246 | die "No Change-Id line in $fname\n" unless @id; |
| 247 | die "Duplicate Change-Id line in $fname\n" if @id > 1; |
| 248 | $cfg->{patches}->{$short} = { |
| 249 | short => $short, |
| 250 | fname => $fname, |
| 251 | patch => $patch, |
| 252 | id => $id[0], |
| 253 | }; |
| 254 | } |
| 255 | |
| 256 | run_gifn_am $cfg, $_ for @{PATCHFILES->{initial}}; |
| 257 | |
| 258 | my @lines = get_current_changes $cfg; |
| 259 | my @expected = map { |
| 260 | $cfg->{patches}->{$_}->{id} |
| 261 | } @{PATCHFILES->{initial}}; |
| 262 | die "Could not apply the initial patches: ". |
| 263 | "got [@lines], expected [@expected]\n" unless |
| 264 | equal_lists \@expected, \@lines; |
| 265 | } |
| 266 | |
| 267 | sub test_bad_cmdline($) |
| 268 | { |
| 269 | my ($cfg) = @_; |
| 270 | |
| 271 | say "\ntest-bad-cmdline\n"; |
| 272 | my @before = get_current_changes $cfg; |
| 273 | |
| 274 | run_failing_command @{$cfg->{gifn}}, '-X', '-Y', '-Z'; |
| 275 | git_status_ok $cfg; |
| 276 | |
| 277 | run_failing_command @{$cfg->{gifn}}, 'am'; |
| 278 | git_status_ok $cfg; |
| 279 | run_failing_command @{$cfg->{gifn}}, 'am', 'a', 'b'; |
| 280 | git_status_ok $cfg; |
| 281 | run_failing_command @{$cfg->{gifn}}, 'am', 'a', 'b', 'c'; |
| 282 | git_status_ok $cfg; |
| 283 | |
| 284 | run_failing_command @{$cfg->{gifn}}, 'am', '/nonexistent'; |
| 285 | git_status_ok $cfg; |
| 286 | |
| 287 | my @after = get_current_changes $cfg; |
| 288 | die "The bad command-line invocations caused a changes change: ". |
| 289 | "before: [@before], after: [@after]\n" unless |
| 290 | equal_lists \@before, \@after; |
| 291 | } |
| 292 | |
| 293 | sub test_already_applied($ @) |
| 294 | { |
| 295 | my ($cfg, @patches) = @_; |
| 296 | |
| 297 | say "\ntest-already-applied @patches\n"; |
| 298 | for my $short (@patches) { |
| 299 | my @before = get_current_changes $cfg; |
| 300 | my $id = $cfg->{patches}->{$short}->{id}; |
| 301 | debug "Should not try to apply $id again over @before"; |
| 302 | |
| 303 | my @lines = run_gifn_am $cfg, $short; |
| 304 | my $seek = qr{^ [#] .* \Q$id\E .* already \s+ present }xi; |
| 305 | my @found = grep { $_ =~ $seek } @lines; |
| 306 | die join '', map "$_\n", ( |
| 307 | "Tried to apply change $id again:", |
| 308 | @lines, |
| 309 | ) unless @found; |
| 310 | |
| 311 | my @after = get_current_changes $cfg; |
| 312 | die "Not even applying $id caused a changes change: ". |
| 313 | "before: [@before], after: [@after]\n" unless |
| 314 | equal_lists \@before, \@after; |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | sub test_fail_to_apply($ @) |
| 319 | { |
| 320 | my ($cfg, @patches) = @_; |
| 321 | |
| 322 | say "\ntest-fail-to-apply @patches\n"; |
| 323 | for my $short (@patches) { |
| 324 | my @before = get_current_changes $cfg; |
| 325 | my $id = $cfg->{patches}->{$short}->{id}; |
| 326 | debug "Should not be able to apply $id right now over @before"; |
| 327 | |
| 328 | run_failing_gifn_am $cfg, $short; |
| 329 | debug "Should be able to recover after a failed 'git am'"; |
| 330 | run_command @{$cfg->{git}}, 'am', '--abort'; |
| 331 | git_status_ok $cfg; |
| 332 | |
| 333 | my @after = get_current_changes $cfg; |
| 334 | die "Failing to apply $id caused a changes change: ". |
| 335 | "before: [@before], after: [@after]\n" unless |
| 336 | equal_lists \@before, \@after; |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | sub test_apply($ @) |
| 341 | { |
| 342 | my ($cfg, @patches) = @_; |
| 343 | |
| 344 | say "\ntest-apply @patches\n"; |
| 345 | for my $short (@patches) { |
| 346 | my @before = get_current_changes $cfg; |
| 347 | my $id = $cfg->{patches}->{$short}->{id}; |
| 348 | debug "Should be able to apply $id over @before"; |
| 349 | |
| 350 | run_gifn_am $cfg, $short; |
| 351 | |
| 352 | my @after = get_current_changes $cfg; |
| 353 | my @expected = (@before, $id); |
| 354 | die "Did not get the expected changes after applying $id: ". |
| 355 | "expected [@expected], got [@after]\n" unless |
| 356 | equal_lists \@expected, \@after; |
| 357 | } |
| 358 | } |
| 359 | |
Peter Penchev | e613855 | 2019-12-03 17:11:16 +0200 | [diff] [blame] | 360 | sub setup_subdir_repos($) |
| 361 | { |
| 362 | my ($cfg) = @_; |
| 363 | |
| 364 | $cfg->{sub}->{full}->{base} = $cfg->{subrepo}->child('full'); |
| 365 | $cfg->{sub}->{full}->{cinder} = |
| 366 | $cfg->{sub}->{full}->{base}-> |
| 367 | child('openstack')->child('cinder'); |
| 368 | $cfg->{sub}->{full}->{nova} = |
| 369 | $cfg->{sub}->{full}->{base}-> |
| 370 | child('openstack')->child('nova'); |
| 371 | $cfg->{sub}->{full}->{cinder}->mkpath({ mode => 0755 }); |
| 372 | $cfg->{sub}->{full}->{nova}->mkpath({ mode => 0755 }); |
| 373 | |
| 374 | $cfg->{sub}->{short}->{base} = $cfg->{subrepo}->child('short'); |
| 375 | $cfg->{sub}->{short}->{cinder} = |
| 376 | $cfg->{sub}->{short}->{base}->child('cinder'); |
| 377 | $cfg->{sub}->{short}->{nova} = |
| 378 | $cfg->{sub}->{short}->{base}->child('nova'); |
| 379 | $cfg->{sub}->{short}->{cinder}->mkpath({ mode => 0755 }); |
| 380 | $cfg->{sub}->{short}->{nova}->mkpath({ mode => 0755 }); |
| 381 | |
| 382 | for my $part (qw(full short)) { |
| 383 | for my $comp (qw(cinder nova)) { |
| 384 | my $dir = $cfg->{sub}->{$part}->{$comp}; |
| 385 | chdir($dir) or die "Could not change into $dir: $!\n"; |
Peter Penchev | a1f004e | 2019-12-03 17:23:25 +0200 | [diff] [blame] | 386 | git_init $cfg; |
Peter Penchev | e613855 | 2019-12-03 17:11:16 +0200 | [diff] [blame] | 387 | git_status_ok $cfg; |
| 388 | } |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | sub test_subdir($ $ $ $) |
| 393 | { |
| 394 | my ($cfg, $part, $expected, $opt) = @_; |
| 395 | my $run = $expected ? 'second' : 'first'; |
| 396 | my $sub = $cfg->{sub}->{$part}; |
| 397 | |
| 398 | say "\ntest-subdir $part $run\n"; |
| 399 | |
| 400 | my $any = sub { |
| 401 | $sub->{cinder}->child('README.txt')->exists || |
| 402 | $sub->{nova}->child('README.txt')->exists |
| 403 | }; |
| 404 | my $all = sub { |
| 405 | $sub->{cinder}->child('README.txt')->exists && |
| 406 | $sub->{nova}->child('README.txt')->exists |
| 407 | }; |
| 408 | |
| 409 | chdir $sub->{base} or die "Could not change into $sub->{base}: $!\n"; |
| 410 | if ($expected && !$all->()) { |
| 411 | die "No $part files before the second run in $sub->{base}\n"; |
| 412 | } elsif (!$expected && $any->()) { |
| 413 | die "Unexpected $part files in $sub->{base}\n"; |
| 414 | } |
| 415 | |
| 416 | run_command @{$cfg->{gifn}}, '-s', $cfg->{data}->child('series'), |
| 417 | @{$opt}, 'am'; |
| 418 | |
| 419 | if (!$all->()) { |
| 420 | my $run = $expected ? 'second' : 'first'; |
| 421 | die "No $part files after the $run run in $sub->{base}\n"; |
| 422 | } |
| 423 | |
| 424 | for my $comp (qw(cinder nova)) { |
| 425 | chdir $sub->{$comp} or |
| 426 | die "Could not change into $sub->{comp}: $!\n"; |
| 427 | git_status_ok $cfg; |
| 428 | } |
| 429 | } |
| 430 | |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 431 | MAIN: |
| 432 | { |
| 433 | my %opts; |
| 434 | |
| 435 | getopts('hNVv-:', \%opts) or usage 1; |
| 436 | help_or_version \%opts; |
| 437 | $debug = $opts{v}; |
| 438 | |
| 439 | usage 1 unless @ARGV; |
| 440 | |
| 441 | my $cwd = path('.')->absolute; |
| 442 | my $locale = detect_utf8_locale; |
| 443 | my $repodir = File::Temp->newdir( |
| 444 | TEMPLATE => 'gifn-test.XXXXXX', |
| 445 | TMPDIR => 1); |
Peter Penchev | e613855 | 2019-12-03 17:11:16 +0200 | [diff] [blame] | 446 | my $subrepodir = File::Temp->newdir( |
| 447 | TEMPLATE => 'gifn-test.XXXXXX', |
| 448 | TMPDIR => 1); |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 449 | |
Peter Penchev | 27ca2bb | 2019-12-03 17:11:28 +0200 | [diff] [blame] | 450 | my @gifn_cmd = @ARGV; |
| 451 | $gifn_cmd[0] = path($gifn_cmd[0])->absolute; |
| 452 | |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 453 | my $cfg = { |
| 454 | cwd => $cwd, |
| 455 | data => $cwd->child('tests')->child('data'), |
Peter Penchev | 27ca2bb | 2019-12-03 17:11:28 +0200 | [diff] [blame] | 456 | gifn => ['env', "LC_MESSAGES=$locale", @gifn_cmd], |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 457 | git => ['env', "LC_MESSAGES=$locale", 'git', '--no-pager'], |
| 458 | repo => path($repodir), |
Peter Penchev | e613855 | 2019-12-03 17:11:16 +0200 | [diff] [blame] | 459 | subrepo => path($subrepodir), |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 460 | }; |
| 461 | |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 462 | eval { |
| 463 | setup_repo $cfg; |
| 464 | |
| 465 | test_bad_cmdline $cfg; |
| 466 | test_already_applied $cfg, @{PATCHFILES->{initial}}; |
| 467 | test_fail_to_apply $cfg, @{PATCHFILES->{conflict}}; |
| 468 | test_apply $cfg, @{PATCHFILES->{more}}; |
| 469 | test_already_applied $cfg, (@{PATCHFILES->{initial}}, @{PATCHFILES->{more}}); |
| 470 | test_fail_to_apply $cfg, @{PATCHFILES->{conflict}}; |
Peter Penchev | e613855 | 2019-12-03 17:11:16 +0200 | [diff] [blame] | 471 | |
| 472 | setup_subdir_repos $cfg; |
| 473 | |
| 474 | test_subdir $cfg, 'full', 0, []; |
| 475 | test_subdir $cfg, 'short', 0, ['-S']; |
| 476 | test_subdir $cfg, 'full', 1, []; |
| 477 | test_subdir $cfg, 'short', 1, ['-S']; |
Peter Penchev | 3c91fe7 | 2019-12-03 16:01:26 +0200 | [diff] [blame] | 478 | }; |
| 479 | my $err = $@; |
| 480 | chdir $cwd; |
| 481 | die $err if $err; |
| 482 | |
| 483 | say 'OK'; |
| 484 | } |