blob: 989cbfa4b4b783599ff91e34cbae9c678795e55a [file] [log] [blame]
Peter Penchev3c91fe72019-12-03 16:01:26 +02001#!/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
27use v5.10;
28use strict;
29use warnings;
30
31use File::Temp;
32use Getopt::Std;
33use List::Util qw(all);
34use Path::Tiny;
35
36use constant VERSION_STRING => '0.1.0';
37
38use constant RE_CHANGE_ID => qr{
39 ^ \s* Change-Id: \s* (?<id> I[0-9a-f]+ ) \s* $
40}xi;
41
42use 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
55my $debug = 0;
56
57sub usage($)
58{
59 my ($err) = @_;
60 my $s = <<EOUSAGE
61Usage: 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
69EOUSAGE
70 ;
71
72 if ($err) {
73 die $s;
74 } else {
75 print "$s";
76 }
77}
78
79sub version()
80{
81 say 'gifn-test '.VERSION_STRING;
82}
83
84sub features()
85{
86 say 'Features: gifn_test='.VERSION_STRING;
87}
88
89sub debug($)
90{
91 say STDERR "RDBG $_[0]" if $debug;
92}
93
94sub 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
108sub 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
127sub run_command(@)
128{
129 my (@cmd) = @_;
130 my @lines = run_command_unchomped @cmd;
131 chomp for @lines;
132 return @lines;
133}
134
135sub 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
150sub 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
168sub 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
181sub 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
189sub 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
199sub 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
207sub 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
216sub equal_lists($ $)
217{
218 my ($expected, $got) = @_;
219
220 return @{$expected} == @{$got} && all { $expected->[$_] eq $got->[$_] } 0..$#{$expected};
221}
222
223sub setup_repo($)
224{
225 my ($cfg) = @_;
226
227 chdir $cfg->{repo} or die "Could not change into $cfg->{repo}: $!\n";
228 run_command @{$cfg->{git}}, 'init';
229 git_status_ok $cfg;
230
231 while (my ($short, $fname) = each %{PATCHFILES->{all}}) {
232 my $patch = $cfg->{data}->child($fname);
233 my @lines = $patch->lines_utf8({ chomp => 1 });
234 my @id = map { $_ =~ RE_CHANGE_ID ? ($+{id}): () } @lines;
235 die "No Change-Id line in $fname\n" unless @id;
236 die "Duplicate Change-Id line in $fname\n" if @id > 1;
237 $cfg->{patches}->{$short} = {
238 short => $short,
239 fname => $fname,
240 patch => $patch,
241 id => $id[0],
242 };
243 }
244
245 run_gifn_am $cfg, $_ for @{PATCHFILES->{initial}};
246
247 my @lines = get_current_changes $cfg;
248 my @expected = map {
249 $cfg->{patches}->{$_}->{id}
250 } @{PATCHFILES->{initial}};
251 die "Could not apply the initial patches: ".
252 "got [@lines], expected [@expected]\n" unless
253 equal_lists \@expected, \@lines;
254}
255
256sub test_bad_cmdline($)
257{
258 my ($cfg) = @_;
259
260 say "\ntest-bad-cmdline\n";
261 my @before = get_current_changes $cfg;
262
263 run_failing_command @{$cfg->{gifn}}, '-X', '-Y', '-Z';
264 git_status_ok $cfg;
265
266 run_failing_command @{$cfg->{gifn}}, 'am';
267 git_status_ok $cfg;
268 run_failing_command @{$cfg->{gifn}}, 'am', 'a', 'b';
269 git_status_ok $cfg;
270 run_failing_command @{$cfg->{gifn}}, 'am', 'a', 'b', 'c';
271 git_status_ok $cfg;
272
273 run_failing_command @{$cfg->{gifn}}, 'am', '/nonexistent';
274 git_status_ok $cfg;
275
276 my @after = get_current_changes $cfg;
277 die "The bad command-line invocations caused a changes change: ".
278 "before: [@before], after: [@after]\n" unless
279 equal_lists \@before, \@after;
280}
281
282sub test_already_applied($ @)
283{
284 my ($cfg, @patches) = @_;
285
286 say "\ntest-already-applied @patches\n";
287 for my $short (@patches) {
288 my @before = get_current_changes $cfg;
289 my $id = $cfg->{patches}->{$short}->{id};
290 debug "Should not try to apply $id again over @before";
291
292 my @lines = run_gifn_am $cfg, $short;
293 my $seek = qr{^ [#] .* \Q$id\E .* already \s+ present }xi;
294 my @found = grep { $_ =~ $seek } @lines;
295 die join '', map "$_\n", (
296 "Tried to apply change $id again:",
297 @lines,
298 ) unless @found;
299
300 my @after = get_current_changes $cfg;
301 die "Not even applying $id caused a changes change: ".
302 "before: [@before], after: [@after]\n" unless
303 equal_lists \@before, \@after;
304 }
305}
306
307sub test_fail_to_apply($ @)
308{
309 my ($cfg, @patches) = @_;
310
311 say "\ntest-fail-to-apply @patches\n";
312 for my $short (@patches) {
313 my @before = get_current_changes $cfg;
314 my $id = $cfg->{patches}->{$short}->{id};
315 debug "Should not be able to apply $id right now over @before";
316
317 run_failing_gifn_am $cfg, $short;
318 debug "Should be able to recover after a failed 'git am'";
319 run_command @{$cfg->{git}}, 'am', '--abort';
320 git_status_ok $cfg;
321
322 my @after = get_current_changes $cfg;
323 die "Failing to apply $id caused a changes change: ".
324 "before: [@before], after: [@after]\n" unless
325 equal_lists \@before, \@after;
326 }
327}
328
329sub test_apply($ @)
330{
331 my ($cfg, @patches) = @_;
332
333 say "\ntest-apply @patches\n";
334 for my $short (@patches) {
335 my @before = get_current_changes $cfg;
336 my $id = $cfg->{patches}->{$short}->{id};
337 debug "Should be able to apply $id over @before";
338
339 run_gifn_am $cfg, $short;
340
341 my @after = get_current_changes $cfg;
342 my @expected = (@before, $id);
343 die "Did not get the expected changes after applying $id: ".
344 "expected [@expected], got [@after]\n" unless
345 equal_lists \@expected, \@after;
346 }
347}
348
Peter Pencheve6138552019-12-03 17:11:16 +0200349sub setup_subdir_repos($)
350{
351 my ($cfg) = @_;
352
353 $cfg->{sub}->{full}->{base} = $cfg->{subrepo}->child('full');
354 $cfg->{sub}->{full}->{cinder} =
355 $cfg->{sub}->{full}->{base}->
356 child('openstack')->child('cinder');
357 $cfg->{sub}->{full}->{nova} =
358 $cfg->{sub}->{full}->{base}->
359 child('openstack')->child('nova');
360 $cfg->{sub}->{full}->{cinder}->mkpath({ mode => 0755 });
361 $cfg->{sub}->{full}->{nova}->mkpath({ mode => 0755 });
362
363 $cfg->{sub}->{short}->{base} = $cfg->{subrepo}->child('short');
364 $cfg->{sub}->{short}->{cinder} =
365 $cfg->{sub}->{short}->{base}->child('cinder');
366 $cfg->{sub}->{short}->{nova} =
367 $cfg->{sub}->{short}->{base}->child('nova');
368 $cfg->{sub}->{short}->{cinder}->mkpath({ mode => 0755 });
369 $cfg->{sub}->{short}->{nova}->mkpath({ mode => 0755 });
370
371 for my $part (qw(full short)) {
372 for my $comp (qw(cinder nova)) {
373 my $dir = $cfg->{sub}->{$part}->{$comp};
374 chdir($dir) or die "Could not change into $dir: $!\n";
375 run_command @{$cfg->{git}}, 'init';
376 git_status_ok $cfg;
377 }
378 }
379}
380
381sub test_subdir($ $ $ $)
382{
383 my ($cfg, $part, $expected, $opt) = @_;
384 my $run = $expected ? 'second' : 'first';
385 my $sub = $cfg->{sub}->{$part};
386
387 say "\ntest-subdir $part $run\n";
388
389 my $any = sub {
390 $sub->{cinder}->child('README.txt')->exists ||
391 $sub->{nova}->child('README.txt')->exists
392 };
393 my $all = sub {
394 $sub->{cinder}->child('README.txt')->exists &&
395 $sub->{nova}->child('README.txt')->exists
396 };
397
398 chdir $sub->{base} or die "Could not change into $sub->{base}: $!\n";
399 if ($expected && !$all->()) {
400 die "No $part files before the second run in $sub->{base}\n";
401 } elsif (!$expected && $any->()) {
402 die "Unexpected $part files in $sub->{base}\n";
403 }
404
405 run_command @{$cfg->{gifn}}, '-s', $cfg->{data}->child('series'),
406 @{$opt}, 'am';
407
408 if (!$all->()) {
409 my $run = $expected ? 'second' : 'first';
410 die "No $part files after the $run run in $sub->{base}\n";
411 }
412
413 for my $comp (qw(cinder nova)) {
414 chdir $sub->{$comp} or
415 die "Could not change into $sub->{comp}: $!\n";
416 git_status_ok $cfg;
417 }
418}
419
Peter Penchev3c91fe72019-12-03 16:01:26 +0200420MAIN:
421{
422 my %opts;
423
424 getopts('hNVv-:', \%opts) or usage 1;
425 help_or_version \%opts;
426 $debug = $opts{v};
427
428 usage 1 unless @ARGV;
429
430 my $cwd = path('.')->absolute;
431 my $locale = detect_utf8_locale;
432 my $repodir = File::Temp->newdir(
433 TEMPLATE => 'gifn-test.XXXXXX',
434 TMPDIR => 1);
Peter Pencheve6138552019-12-03 17:11:16 +0200435 my $subrepodir = File::Temp->newdir(
436 TEMPLATE => 'gifn-test.XXXXXX',
437 TMPDIR => 1);
Peter Penchev3c91fe72019-12-03 16:01:26 +0200438
439 my $cfg = {
440 cwd => $cwd,
441 data => $cwd->child('tests')->child('data'),
442 gifn => [@ARGV],
443 git => ['env', "LC_MESSAGES=$locale", 'git', '--no-pager'],
444 repo => path($repodir),
Peter Pencheve6138552019-12-03 17:11:16 +0200445 subrepo => path($subrepodir),
Peter Penchev3c91fe72019-12-03 16:01:26 +0200446 };
447
448 $cfg->{gifn}[0] = path($cfg->{gifn}[0])->absolute;
449
450 eval {
451 setup_repo $cfg;
452
453 test_bad_cmdline $cfg;
454 test_already_applied $cfg, @{PATCHFILES->{initial}};
455 test_fail_to_apply $cfg, @{PATCHFILES->{conflict}};
456 test_apply $cfg, @{PATCHFILES->{more}};
457 test_already_applied $cfg, (@{PATCHFILES->{initial}}, @{PATCHFILES->{more}});
458 test_fail_to_apply $cfg, @{PATCHFILES->{conflict}};
Peter Pencheve6138552019-12-03 17:11:16 +0200459
460 setup_subdir_repos $cfg;
461
462 test_subdir $cfg, 'full', 0, [];
463 test_subdir $cfg, 'short', 0, ['-S'];
464 test_subdir $cfg, 'full', 1, [];
465 test_subdir $cfg, 'short', 1, ['-S'];
Peter Penchev3c91fe72019-12-03 16:01:26 +0200466 };
467 my $err = $@;
468 chdir $cwd;
469 die $err if $err;
470
471 say 'OK';
472}