blob: dc8fe58bcbc0bbde9b0de60a71a5d2000dc730e0 [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
Peter Pencheva1f004e2019-12-03 17:23:25 +0200223sub 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 Penchev3c91fe72019-12-03 16:01:26 +0200234sub setup_repo($)
235{
236 my ($cfg) = @_;
237
238 chdir $cfg->{repo} or die "Could not change into $cfg->{repo}: $!\n";
Peter Pencheva1f004e2019-12-03 17:23:25 +0200239 git_init $cfg;
Peter Penchev3c91fe72019-12-03 16:01:26 +0200240 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
267sub 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
293sub 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
318sub 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
340sub 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 Pencheve6138552019-12-03 17:11:16 +0200360sub 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 Pencheva1f004e2019-12-03 17:23:25 +0200386 git_init $cfg;
Peter Pencheve6138552019-12-03 17:11:16 +0200387 git_status_ok $cfg;
388 }
389 }
390}
391
392sub 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 Penchev3c91fe72019-12-03 16:01:26 +0200431MAIN:
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 Pencheve6138552019-12-03 17:11:16 +0200446 my $subrepodir = File::Temp->newdir(
447 TEMPLATE => 'gifn-test.XXXXXX',
448 TMPDIR => 1);
Peter Penchev3c91fe72019-12-03 16:01:26 +0200449
Peter Penchev27ca2bb2019-12-03 17:11:28 +0200450 my @gifn_cmd = @ARGV;
451 $gifn_cmd[0] = path($gifn_cmd[0])->absolute;
452
Peter Penchev3c91fe72019-12-03 16:01:26 +0200453 my $cfg = {
454 cwd => $cwd,
455 data => $cwd->child('tests')->child('data'),
Peter Penchev27ca2bb2019-12-03 17:11:28 +0200456 gifn => ['env', "LC_MESSAGES=$locale", @gifn_cmd],
Peter Penchev3c91fe72019-12-03 16:01:26 +0200457 git => ['env', "LC_MESSAGES=$locale", 'git', '--no-pager'],
458 repo => path($repodir),
Peter Pencheve6138552019-12-03 17:11:16 +0200459 subrepo => path($subrepodir),
Peter Penchev3c91fe72019-12-03 16:01:26 +0200460 };
461
Peter Penchev3c91fe72019-12-03 16:01:26 +0200462 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 Pencheve6138552019-12-03 17:11:16 +0200471
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 Penchev3c91fe72019-12-03 16:01:26 +0200478 };
479 my $err = $@;
480 chdir $cwd;
481 die $err if $err;
482
483 say 'OK';
484}