std.t: create_file(): Use `>` as separate parameter in open()
[sunny256-utils.git] / tests / filesynced.t
blob408b311d4398d5c837e8c8326c17e04a25e3e243
1 #!/usr/bin/env perl
3 #=======================================================================
4 # filesynced.t
5 # File ID: 8f5fa76e-a802-11e5-bb87-fefdb24f8e10
7 # Test suite for filesynced(1).
9 # Character set: UTF-8
10 # ©opyleft 2015– Øyvind A. Holm <sunny@sunbase.org>
11 # License: GNU General Public License version 2 or later, see end of
12 # file for legal stuff.
13 #=======================================================================
15 use strict;
16 use warnings;
18 BEGIN {
19 use Test::More qw{no_plan};
20 # use_ok() goes here
23 use Getopt::Long;
24 use IPC::Open3;
26 local $| = 1;
28 our $CMD_BASENAME = "filesynced";
29 our $CMD = "../$CMD_BASENAME";
30 my $SQLITE = "sqlite3";
32 our %Opt = (
34 'all' => 0,
35 'help' => 0,
36 'quiet' => 0,
37 'todo' => 0,
38 'verbose' => 0,
39 'version' => 0,
43 our $progname = $0;
44 $progname =~ s/^.*\/(.*?)$/$1/;
45 our $VERSION = '0.0.0';
47 my %descriptions = ();
49 Getopt::Long::Configure('bundling');
50 GetOptions(
52 'all|a' => \$Opt{'all'},
53 'help|h' => \$Opt{'help'},
54 'quiet|q+' => \$Opt{'quiet'},
55 'todo|t' => \$Opt{'todo'},
56 'verbose|v+' => \$Opt{'verbose'},
57 'version' => \$Opt{'version'},
59 ) || die("$progname: Option error. Use -h for help.\n");
61 $Opt{'verbose'} -= $Opt{'quiet'};
62 $Opt{'help'} && usage(0);
63 if ($Opt{'version'}) {
64 print_version();
65 exit(0);
68 my $sql_error = 0;
70 exit(main());
72 sub main {
73 # {{{
74 my $Retval = 0;
76 diag(sprintf('========== Executing %s v%s ==========',
77 $progname, $VERSION));
79 if ($Opt{'todo'} && !$Opt{'all'}) {
80 goto todo_section;
83 =pod
85 testcmd("$CMD command", # {{{
86 <<'END',
87 [expected stdout]
88 END
89 '',
91 'description',
94 # }}}
96 =cut
98 diag('Testing -h (--help) option...');
99 likecmd("$CMD -h", # {{{
100 '/ Show this help/i',
101 '/^$/',
103 'Option -h prints help screen',
106 # }}}
107 diag('Testing -v (--verbose) option...');
108 likecmd("$CMD -h -v", # {{{
109 '/^\n\S+ \d+\.\d+\.\d+/s',
110 '/^$/',
112 'Option -v with -h returns version number and help screen',
115 # }}}
116 diag('Testing --version option...');
117 likecmd("$CMD --version", # {{{
118 '/^\S+ \d+\.\d+\.\d+/',
119 '/^$/',
121 'Option --version returns version number',
124 # }}}
126 my $syncfile = "$ENV{'HOME'}/bin/synced.sql";
127 if (!-r $syncfile) {
128 diag("$syncfile not found, skipping tests");
129 return 0;
132 my $Tmptop = "tmp-filesynced-t-$$-" . substr(rand, 2, 8);
133 my $GIT = "git";
135 ok(mkdir($Tmptop), "mkdir [Tmptop]");
136 ok(chdir($Tmptop), "chdir [Tmptop]") || BAIL_OUT();
137 likecmd("$GIT init repo-fs-t", # {{{
138 '/.*/',
139 '/.*/',
141 "git init repo-fs-t",
144 # }}}
145 ok(-d "repo-fs-t/.git", "repo-fs-t/.git exists") || BAIL_OUT();
146 ok(-d "../$Tmptop", "We're in [Tmptop]") || BAIL_OUT();
147 ok(chdir("repo-fs-t"), "chdir repo-fs-t");
148 $CMD = "../../../$CMD_BASENAME";
149 ok(-f $CMD, "Executable is in place") || BAIL_OUT();
150 testcmd("$CMD -v", # No options, no database {{{
152 "filesynced: synced.sqlite: Sync database not found\n",
154 "No options, no database",
157 # }}}
158 testcmd("$CMD -l -v", # No database and -l {{{
160 "filesynced: synced.sqlite: Sync database not found\n",
162 "No database and -l",
165 # }}}
166 diag("--init");
167 chomp(my $sql_create_synced = <<END); # {{{
168 CREATE TABLE synced (
169 file TEXT
170 CONSTRAINT synced_file_length
171 CHECK (length(file) > 0)
172 UNIQUE
173 NOT NULL
175 orig TEXT
177 rev TEXT
178 CONSTRAINT synced_rev_length
179 CHECK (length(rev) = 40 OR rev = '')
181 date TEXT
182 CONSTRAINT synced_date_length
183 CHECK (date IS NULL OR length(date) = 19)
184 CONSTRAINT synced_date_valid
185 CHECK (date IS NULL OR datetime(date) IS NOT NULL)
188 # }}}
189 chomp(my $sql_create_todo = <<END); # {{{
190 CREATE TABLE todo (
191 file TEXT
192 CONSTRAINT todo_file_length
193 CHECK(length(file) > 0)
194 UNIQUE
195 NOT NULL
197 pri INTEGER
198 CONSTRAINT todo_pri_range
199 CHECK(pri BETWEEN 1 AND 5)
201 comment TEXT
204 # }}}
205 testcmd("$CMD --init", # {{{
209 "--init without options",
212 # }}}
213 is(sql_dump("synced.sql"), <<END, "synced.sql is ok"); # {{{
214 $sql_create_synced
215 $sql_create_todo
218 # }}}
219 likecmd("$CMD --init", # {{{
220 '/^$/',
221 '/\/repo-fs-t\/synced.sql already exists\n$/s',
223 "Refuse to --init when synced.sql exists",
226 # }}}
227 testcmd("sqlite3 synced.sqlite <synced.sql", '', '', 0, # {{{
228 "Create synced.sqlite from synced.sql");
230 # }}}
231 ok(unlink("synced.sql"), "Delete synced.sql");
232 likecmd("$CMD --init", # {{{
233 '/^$/',
234 '/\/repo-fs-t\/synced.sqlite: File already exists\n' .
235 'filesynced: No token received from filesynced --lock\n' .
236 '$/s',
238 "It also reacts negatively to the presence of synced.sqlite",
241 # }}}
242 ok(unlink("synced.sqlite"), "Delete synced.sqlite");
243 diag("--lock");
244 testcmd("$CMD --init", # {{{
248 "Create synced.sql again with --init",
251 # }}}
252 testcmd("$CMD --lock >key.txt", "", "", 0, # {{{
253 "Use --lock, store key in key.txt",
256 # }}}
257 my $realtoken = file_data("key.txt");
258 likecmd("$CMD --lock --timeout 0", # {{{
259 '/^$/',
260 '/^' .
261 'filesynced --lock: .+\/repo-fs-t\/synced.sql\.lock: ' .
262 'Waiting for lockdir\.\.\.\n' .
263 'filesynced: Lock not aquired after 0 seconds, aborting\n' .
264 '$/s',
266 "Try to lock again, wimp gives up after 0 seconds",
269 # }}}
270 like($realtoken, # {{{
272 '/^' .
273 'token_' .
274 '20\d\d' . '[01]\d' . '\d\d' .
275 'T' .
276 '[0-2]\d' . '[0-5]\d' . '[0-6]\d' .
277 'Z' .
278 '\.' .
279 '\d+' .
280 '\n' .
281 '$/s'
283 "key.txt looks ok",
286 # }}}
287 diag("--unlock");
288 testcmd("$CMD --unlock", # {{{
290 "filesynced --unlock: Token mismatch\n",
292 "No argument to --unlock",
295 # }}}
296 testcmd("$CMD --unlock ''", # {{{
298 "filesynced --unlock: Token mismatch\n",
300 "--unlock receives empty string",
303 # }}}
304 testcmd("$CMD --unlock token_20141212T123456Z.1234." . ("2" x 40), # {{{
306 "filesynced --unlock: Token mismatch\n",
308 "--unlock token is wrong",
311 # }}}
312 testcmd("$CMD --unlock $realtoken", # {{{
316 "--unlock token is valid",
319 # }}}
320 likecmd("$CMD --lock", # {{{
321 '/^token_\S+\n$/',
322 '/^$/',
324 "--lock, throw away the token",
327 # }}}
328 testcmd("$CMD --unlock -f", # {{{
332 "--unlock with -f (force)",
335 # }}}
336 likecmd("$CMD --lock", # {{{
337 '/^token_\S+\n$/',
338 '/^$/',
340 "--lock again, throw away the token",
343 # }}}
344 testcmd("$CMD --force --unlock", # {{{
348 "--unlock with --force",
351 # }}}
352 diag("--add");
353 testcmd("$CMD --add nonexisting.txt", # {{{
355 "filesynced: nonexisting.txt: File not found, no entries updated\n",
357 "Try to --add non-existing file",
360 # }}}
361 ok(create_file("tmpfile.txt", "This is tmpfile.txt"),
362 "Create tmpfile.txt");
363 testcmd("$CMD --add tmpfile.txt nonexisting.txt", # {{{
365 "filesynced: nonexisting.txt: File not found, no entries updated\n",
367 "Try to --add existing and non-existing file",
370 # }}}
371 is(sql_dump("synced.sql"), # {{{
372 <<END,
373 $sql_create_synced
374 $sql_create_todo
376 "tmpfile.txt is not added to synced.sql yet",
379 # }}}
380 testcmd("$CMD --add tmpfile.txt", # {{{
384 "Add tmpfile.txt with --add",
387 # }}}
388 is(sql_dump("synced.sql"), # {{{
389 <<END,
390 $sql_create_synced
391 $sql_create_todo
392 synced|tmpfile.txt|NULL|NULL|NULL
394 "tmpfile.txt is added to synced.sql",
397 # }}}
398 likecmd("$CMD --add tmpfile.txt", # {{{
399 '/^$/s',
400 '/filesynced: Cannot add "tmpfile\.txt" to the database, ' .
401 'no entries updated\n/s',
403 "Fail to add it again",
406 # }}}
407 is(sql_dump("synced.sql"), # {{{
408 <<END,
409 $sql_create_synced
410 $sql_create_todo
411 synced|tmpfile.txt|NULL|NULL|NULL
413 "There's only one tmpfile.txt in synced.sql",
416 # }}}
417 ok(!-d "synced.sql.lock", "synced.sql.lock/ is gone");
418 diag("--delete");
419 testcmd("$CMD --delete nonexisting.txt", # {{{
423 "--delete nonexisting.txt",
426 # }}}
427 testcmd("$CMD --delete tmpfile.txt", # {{{
429 "filesynced: Deleted tmpfile.txt from synced\n",
431 "--delete tmpfile.txt",
434 # }}}
435 is(sql_dump("synced.sql"), # {{{
436 <<END,
437 $sql_create_synced
438 $sql_create_todo
440 "tmpfile.txt is gone from synced.sql",
443 # }}}
444 testcmd("$CMD --add -t bash tmpfile.txt", # {{{
448 "Add tmpfile.txt again, now with -t bash",
451 # }}}
452 is(sql_dump("synced.sql"), # {{{
453 <<END,
454 $sql_create_synced
455 $sql_create_todo
456 synced|tmpfile.txt|Lib/std/bash|NULL|NULL
458 "tmpfile.txt is added to synced.sql with orig value",
461 # }}}
462 diag("--unsynced");
463 create_file("file1", "This is file1.\n");
464 testcmd("git add file1", # {{{
468 "git add file1",
471 # }}}
472 likecmd("git commit -m 'Add file1'", # {{{
473 '/^.+Add file1.+$/s',
474 '/^$/',
476 "git commit file1",
479 # }}}
480 testcmd("$CMD --unsynced", # {{{
484 "file1 is not listed by --unsynced because " .
485 "it's not in synced.sql",
488 # }}}
489 testcmd("$CMD --add -t Lib/std/bash file1", # {{{
493 "--add file1",
496 # }}}
497 testcmd("$CMD --unsynced", # {{{
498 "file1\n",
501 "file1 is listed by --unsynced",
504 # }}}
505 testcmd("$CMD HEAD file1", # {{{
509 "Mark file1 as updated",
512 # }}}
513 testcmd("$CMD --unsynced", # {{{
517 "file1 should be gone from --unsynced now",
520 # }}}
521 # diag("--valid-sha");
522 # FIXME: Create tests for --valid-sha. Have to set up some sync
523 # rules first.
524 testcmd("$CMD --delete file1", # {{{
526 "filesynced: Deleted file1 from synced\n",
528 "--delete file1",
531 # }}}
532 diag("--create-index");
533 testcmd("$CMD --create-index", # {{{
537 "Use with --create-index",
540 # }}}
541 is(sql_dump("synced.sql"), # {{{
542 <<END,
543 $sql_create_synced
544 $sql_create_todo
545 CREATE INDEX idx_synced_file ON synced (file);
546 CREATE INDEX idx_synced_orig ON synced (orig);
547 CREATE INDEX idx_synced_rev ON synced (rev);
548 synced|tmpfile.txt|Lib/std/bash|NULL|NULL
550 "synced.sql contains indexes",
553 # }}}
555 diag("Clean up");
556 ok(chdir(".."), "chdir ..");
557 testcmd("rm -rf repo-fs-t", '', '', 0, "Delete repo-fs-t/");
558 ok(chdir(".."), "chdir ..");
559 ok(rmdir($Tmptop), "rmdir [Tmptop]");
561 todo_section:
564 if ($Opt{'all'} || $Opt{'todo'}) {
565 diag('Running TODO tests...'); # {{{
567 TODO: {
569 local $TODO = '';
570 # Insert TODO tests here.
573 # TODO tests }}}
576 diag('Testing finished.');
577 return $Retval;
578 # }}}
579 } # main()
581 sub sql {
582 # {{{
583 my ($db, $sql) = @_;
584 my @retval = ();
586 msg(5, "sql(): db = '$db'");
587 local(*CHLD_IN, *CHLD_OUT, *CHLD_ERR);
589 my $pid = open3(*CHLD_IN, *CHLD_OUT, *CHLD_ERR, $SQLITE, $db) or (
590 $sql_error = 1,
591 msg(0, "sql(): open3() error: $!"),
592 return("sql() error"),
594 msg(5, "sql(): sql = '$sql'");
595 print(CHLD_IN "$sql\n") or msg(0, "sql(): print CHLD_IN error: $!");
596 close(CHLD_IN);
597 @retval = <CHLD_OUT>;
598 msg(5, "sql(): retval = '" . join('|', @retval) . "'");
599 my @child_stderr = <CHLD_ERR>;
600 if (scalar(@child_stderr)) {
601 msg(1, "$SQLITE error: " . join('', @child_stderr));
602 $sql_error = 1;
604 return(join('', @retval));
605 # }}}
606 } # sql()
608 sub sqlite_dump {
609 # Return contents of database file {{{
610 my $File = shift;
612 return sql($File, <<END);
613 .nullvalue NULL
614 .schema
615 SELECT 'synced', * FROM synced;
617 # }}}
618 } # sqlite_dump()
620 sub sql_dump {
621 # {{{
622 my $File = shift;
623 my $db = "$File.sqlite";
624 my $Txt;
626 is(system("$SQLITE $db <$File"), 0, "Create $db from $File");
627 ok(-f $db, "$db exists");
628 $Txt = sqlite_dump("$db");
629 ok(unlink($db), "Delete $db");
631 return $Txt;
632 # }}}
633 } # sql_dump()
635 sub testcmd {
636 # {{{
637 my ($Cmd, $Exp_stdout, $Exp_stderr, $Exp_retval, $Desc) = @_;
638 defined($descriptions{$Desc}) &&
639 BAIL_OUT("testcmd(): '$Desc' description is used twice");
640 $descriptions{$Desc} = 1;
641 my $stderr_cmd = '';
642 my $cmd_outp_str = $Opt{'verbose'} >= 1 ? "\"$Cmd\" - " : '';
643 my $Txt = join('', $cmd_outp_str, defined($Desc) ? $Desc : '');
644 my $TMP_STDERR = "$CMD_BASENAME-stderr.tmp";
645 my $retval = 1;
647 if (defined($Exp_stderr)) {
648 $stderr_cmd = " 2>$TMP_STDERR";
650 $retval &= is(`$Cmd$stderr_cmd`, $Exp_stdout, "$Txt (stdout)");
651 my $ret_val = $?;
652 if (defined($Exp_stderr)) {
653 $retval &= is(file_data($TMP_STDERR), $Exp_stderr, "$Txt (stderr)");
654 unlink($TMP_STDERR);
655 } else {
656 diag("Warning: stderr not defined for '$Txt'");
658 $retval &= is($ret_val >> 8, $Exp_retval, "$Txt (retval)");
660 return $retval;
661 # }}}
662 } # testcmd()
664 sub likecmd {
665 # {{{
666 my ($Cmd, $Exp_stdout, $Exp_stderr, $Exp_retval, $Desc) = @_;
667 defined($descriptions{$Desc}) &&
668 BAIL_OUT("likecmd(): '$Desc' description is used twice");
669 $descriptions{$Desc} = 1;
670 my $stderr_cmd = '';
671 my $cmd_outp_str = $Opt{'verbose'} >= 1 ? "\"$Cmd\" - " : '';
672 my $Txt = join('', $cmd_outp_str, defined($Desc) ? $Desc : '');
673 my $TMP_STDERR = "$CMD_BASENAME-stderr.tmp";
674 my $retval = 1;
676 if (defined($Exp_stderr)) {
677 $stderr_cmd = " 2>$TMP_STDERR";
679 $retval &= like(`$Cmd$stderr_cmd`, $Exp_stdout, "$Txt (stdout)");
680 my $ret_val = $?;
681 if (defined($Exp_stderr)) {
682 $retval &= like(file_data($TMP_STDERR), $Exp_stderr, "$Txt (stderr)");
683 unlink($TMP_STDERR);
684 } else {
685 diag("Warning: stderr not defined for '$Txt'");
687 $retval &= is($ret_val >> 8, $Exp_retval, "$Txt (retval)");
689 return $retval;
690 # }}}
691 } # likecmd()
693 sub file_data {
694 # Return file content as a string {{{
695 my $File = shift;
696 my $Txt;
698 open(my $fp, '<', $File) or return undef;
699 local $/ = undef;
700 $Txt = <$fp>;
701 close($fp);
702 return $Txt;
703 # }}}
704 } # file_data()
706 sub create_file {
707 # Create new file and fill it with data {{{
708 my ($file, $text) = @_;
709 my $retval = 0;
710 if (open(my $fp, ">$file")) {
711 print($fp $text);
712 close($fp);
713 $retval = is(
714 file_data($file),
715 $text,
716 "$file was successfully created",
719 return($retval); # 0 if error, 1 if ok
720 # }}}
721 } # create_file()
723 sub print_version {
724 # Print program version {{{
725 print("$progname $VERSION\n");
726 return;
727 # }}}
728 } # print_version()
730 sub usage {
731 # Send the help message to stdout {{{
732 my $Retval = shift;
734 if ($Opt{'verbose'}) {
735 print("\n");
736 print_version();
738 print(<<"END");
740 Usage: $progname [options]
742 Contains tests for the $CMD_BASENAME(1) program.
744 Options:
746 -a, --all
747 Run all tests, also TODOs.
748 -h, --help
749 Show this help.
750 -q, --quiet
751 Be more quiet. Can be repeated to increase silence.
752 -t, --todo
753 Run only the TODO tests.
754 -v, --verbose
755 Increase level of verbosity. Can be repeated.
756 --version
757 Print version information.
760 exit($Retval);
761 # }}}
762 } # usage()
764 sub msg {
765 # Print a status message to stderr based on verbosity level {{{
766 my ($verbose_level, $Txt) = @_;
768 $verbose_level > $Opt{'verbose'} && return;
769 print(STDERR "$progname: $Txt\n");
770 return;
771 # }}}
772 } # msg()
774 __END__
776 # This program is free software; you can redistribute it and/or modify
777 # it under the terms of the GNU General Public License as published by
778 # the Free Software Foundation; either version 2 of the License, or (at
779 # your option) any later version.
781 # This program is distributed in the hope that it will be useful, but
782 # WITHOUT ANY WARRANTY; without even the implied warranty of
783 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
784 # See the GNU General Public License for more details.
786 # You should have received a copy of the GNU General Public License
787 # along with this program.
788 # If not, see L<http://www.gnu.org/licenses/>.
790 # vim: set fenc=UTF-8 ft=perl fdm=marker ts=4 sw=4 sts=4 et fo+=w :