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