blob: a78c262914e9c4aab15ef3a6e71577fbcf90d491 [file] [log] [blame]
Peter Penchev3c91fe72019-12-03 16:01:26 +02001#!/usr/bin/perl
2#
Peter Pentchevb91fc932023-01-19 15:27:13 +02003# SPDX-FileCopyrightText: 2019 Peter Pentchev
4# SPDX-License-Identifier: BSD-2-Clause
Peter Penchev3c91fe72019-12-03 16:01:26 +02005
6use v5.10;
7use strict;
8use warnings;
9
10use File::Temp;
11use Getopt::Std;
12use List::Util qw(all);
13use Path::Tiny;
14
15use constant VERSION_STRING => '0.1.0';
16
17use constant RE_CHANGE_ID => qr{
18 ^ \s* Change-Id: \s* (?<id> I[0-9a-f]+ ) \s* $
19}xi;
20
21use 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
34my $debug = 0;
35
36sub usage($)
37{
38 my ($err) = @_;
39 my $s = <<EOUSAGE
40Usage: 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
48EOUSAGE
49 ;
50
51 if ($err) {
52 die $s;
53 } else {
54 print "$s";
55 }
56}
57
58sub version()
59{
60 say 'gifn-test '.VERSION_STRING;
61}
62
63sub features()
64{
65 say 'Features: gifn_test='.VERSION_STRING;
66}
67
68sub debug($)
69{
70 say STDERR "RDBG $_[0]" if $debug;
71}
72
73sub 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
87sub 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
106sub run_command(@)
107{
108 my (@cmd) = @_;
109 my @lines = run_command_unchomped @cmd;
110 chomp for @lines;
111 return @lines;
112}
113
114sub 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
129sub 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
147sub 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
160sub 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
168sub 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
178sub 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
186sub 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
195sub equal_lists($ $)
196{
197 my ($expected, $got) = @_;
198
199 return @{$expected} == @{$got} && all { $expected->[$_] eq $got->[$_] } 0..$#{$expected};
200}
201
Peter Pencheva1f004e2019-12-03 17:23:25 +0200202sub 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 Penchev3c91fe72019-12-03 16:01:26 +0200213sub setup_repo($)
214{
215 my ($cfg) = @_;
216
217 chdir $cfg->{repo} or die "Could not change into $cfg->{repo}: $!\n";
Peter Pencheva1f004e2019-12-03 17:23:25 +0200218 git_init $cfg;
Peter Penchev3c91fe72019-12-03 16:01:26 +0200219 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
246sub 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
272sub 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
297sub 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
319sub 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 Pencheve6138552019-12-03 17:11:16 +0200339sub 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 Pencheva1f004e2019-12-03 17:23:25 +0200365 git_init $cfg;
Peter Pencheve6138552019-12-03 17:11:16 +0200366 git_status_ok $cfg;
367 }
368 }
369}
370
371sub 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 Penchev3c91fe72019-12-03 16:01:26 +0200410MAIN:
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 Pencheve6138552019-12-03 17:11:16 +0200425 my $subrepodir = File::Temp->newdir(
426 TEMPLATE => 'gifn-test.XXXXXX',
427 TMPDIR => 1);
Peter Penchev3c91fe72019-12-03 16:01:26 +0200428
Peter Penchev27ca2bb2019-12-03 17:11:28 +0200429 my @gifn_cmd = @ARGV;
430 $gifn_cmd[0] = path($gifn_cmd[0])->absolute;
431
Peter Penchev3c91fe72019-12-03 16:01:26 +0200432 my $cfg = {
433 cwd => $cwd,
434 data => $cwd->child('tests')->child('data'),
Peter Penchev27ca2bb2019-12-03 17:11:28 +0200435 gifn => ['env', "LC_MESSAGES=$locale", @gifn_cmd],
Peter Penchev3c91fe72019-12-03 16:01:26 +0200436 git => ['env', "LC_MESSAGES=$locale", 'git', '--no-pager'],
437 repo => path($repodir),
Peter Pencheve6138552019-12-03 17:11:16 +0200438 subrepo => path($subrepodir),
Peter Penchev3c91fe72019-12-03 16:01:26 +0200439 };
440
Peter Penchev3c91fe72019-12-03 16:01:26 +0200441 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 Pencheve6138552019-12-03 17:11:16 +0200450
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 Penchev3c91fe72019-12-03 16:01:26 +0200457 };
458 my $err = $@;
459 chdir $cwd;
460 die $err if $err;
461
462 say 'OK';
463}