diff --git a/.editorconfig b/.editorconfig index 6bf6372..b8a739d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ indent_size = 4 [*.md] indent_size = 2 + +[{Makefile,*.mak}] +indent_style = tab diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7d7fce5..e7d03e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,11 +14,11 @@ build-allinone: test-run: stage: test script: - - t/run.sh + - make test test-run-allinone: stage: test dependencies: - build-allinone script: - - SCRIPT=~+/simplify_static_dir.pl t/run.sh + - SCRIPT=./simplify_static_dir.pl make test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f56e4e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +PROVE = prove -rv + +TESTS_DIR = t + +test: + $(PROVE) $(TESTS_DIR) + +.PHONY: test diff --git a/t/TestFunctions.pm b/t/TestFunctions.pm new file mode 100644 index 0000000..94441ff --- /dev/null +++ b/t/TestFunctions.pm @@ -0,0 +1,130 @@ +package TestFunctions; +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +require Archive::Tar; +use Cwd qw/ + abs_path + chdir + getcwd +/; +use File::Basename qw/ + dirname +/; +require File::Temp; + +use Exporter; +our @ISA = qw/ Exporter /; +our @EXPORT = qw/ + are_hardlinked + file_exists + filemtime + has_mtime + mktempdir + prep_tar + run_script + run_script_capture +/; + +use constant SCRIPT => $ENV{SCRIPT} // abs_path dirname(__FILE__) . '/../simplify_static_dir-main.pl'; + +sub are_hardlinked { + my $starter = shift; + + my $gen_ident = sub { + my ($dev, $ino) = stat $_[0]; + return "$dev:$ino"; + }; + + my $starter_ident = &$gen_ident($starter); + foreach my $file (@_) { + if (&$gen_ident($file) ne $starter_ident) { + return 0; + } + } + return 1; +} + +sub file_exists { + foreach my $file (@_) { + unless (-e $file) { + return 0; + } + } + return 1; +} + +sub filemtime { + (stat shift)[9]; +} + +sub has_mtime { + my $mtime = shift; + foreach my $file (@_) { + if (&filemtime($file) != $mtime) { + return 0; + } + } + return 1; +} + +sub mktempdir { + return File::Temp->newdir( + TEMPLATE => 'tests.XXXXXX', + TMPDIR => 1, + CLEANUP => 1, + ); +} + +sub prep_tar { + my $tarball = shift // (dirname(__FILE__) . '/t.tar'); + + my $td = &mktempdir; + + # Note: Using chdir from Cwd automatically keeps $ENV{PWD} up-to-date (just + # in case) + my $oldpwd = &getcwd; + + chdir $td; + my $tar = Archive::Tar->new; + $tar->read($tarball); + $tar->extract(); + chdir $oldpwd; + + return $td; +} + +sub run_script_capture { + my @cmd =(SCRIPT, @_); + + use IPC::Open3 qw/ open3 /; + my $stderr = File::Temp->new( + TMPDIR => 1, + CLEANUP => 1, + ); + my $stdout = File::Temp->new( + TMPDIR => 1, + CLEANUP => 1, + ); + my $in = ''; + local *CATCHOUT = $stdout; + local *CATCHERR = $stderr; + print STDERR "+ @cmd\n"; + my $pid = open3 $in, '>&CATCHOUT', '>&CATCHERR', @cmd; + waitpid $pid, 0; + seek $_, 0, 0 for \*CATCHOUT, \*CATCHERR; + + return ( + $?, + (join "\n", ), + (join "\n", ) + ); +} + +sub run_script { + print STDERR '+ ' . SCRIPT . " @_\n"; + system SCRIPT, @_; +} + +1; diff --git a/t/freed-bytes-commas.t b/t/freed-bytes-commas.t new file mode 100644 index 0000000..1598500 --- /dev/null +++ b/t/freed-bytes-commas.t @@ -0,0 +1,31 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 1; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $test_dir = &mktempdir; +&put_file( + "$test_dir/1", + "$test_dir/2", +); + +my (undef, $stdout, $stderr) = &run_script_capture('-f', $test_dir); +ok "freed 1,048,576 bytes (1 MB)\n" eq $stderr, 'prints freed bytes with commas'; + +sub put_file { + my $bytes = 1048576; # 1 MB + foreach my $file (@_) { + open my $fh, '>', $file + or die "Failed to open file $file for writing: $!"; + for (my $bytes_written = 0; $bytes_written < $bytes; ++$bytes_written) { + print $fh 'A'; + } + } +} diff --git a/t/freed-bytes.t b/t/freed-bytes.t new file mode 100644 index 0000000..7d686c0 --- /dev/null +++ b/t/freed-bytes.t @@ -0,0 +1,26 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 3; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/freed-bytes"; +my @files = ( + "$test_dir/1", + "$test_dir/2", + "$test_dir/3", + "$test_dir/4", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +my (undef, $stdout, $stderr) = &run_script_capture('-f', $test_dir, $test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok "freed 24 bytes (24 B)\n" eq $stderr, 'prints correct number of freed bytes'; diff --git a/t/funcs.sh b/t/funcs.sh deleted file mode 100644 index d25804c..0000000 --- a/t/funcs.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -# vi: et sts=4 sw=4 ts=4 - -echo_success() { - printf '[\033[1;32m%s\033[0;39m] %s' \ - ' OK ' \ - "$(escape_nonprinting "$*")" -} - -echo_failure() { - printf '[\033[1;31m%s\033[0;39m] %s' \ - 'FAILED' \ - "$(escape_nonprinting "$*")" -} - -echo_warning() { - printf '[\033[1;33m%s\033[0;39m] %s' \ - 'WARNING' \ - "$(escape_nonprinting "$*")" -} - -echo_passed() { - printf '[\033[1;33m%s\033[0;39m] %s' \ - 'PASSED' \ - "$(escape_nonprinting "$*")" -} - -escape_nonprinting() { - echo "$*" |cat -v -} - -assert_equals() { - local \ - STARTER=$1 \ - NEXT - shift 1 - for NEXT; do - if [[ $STARTER != "$NEXT" ]]; then - echo_failure "$STARTER does not equal $NEXT" - echo - return 1 - fi - echo_success "$STARTER equals $NEXT" - echo - done -} - -assert_file_exists() { - local FN - for FN; do - if [[ ! -e $FN ]]; then - echo_failure "$FN does not exist" - echo - return 1 - fi - done -} - -assert_hardlinked() { - local \ - STARTER=$1 \ - NEXT - shift 1 - for NEXT; do - if [[ ! $STARTER -ef $NEXT ]]; then - echo_failure "$STARTER is not hard-linked to $NEXT" - echo - return 1 - fi - echo_success "$STARTER is hard-linked to $NEXT" - echo - done -} - -assert_nothardlinked() { - local \ - STARTER=$1 \ - NEXT - shift 1 - for NEXT; do - if [[ $STARTER -ef $NEXT ]]; then - echo_failure "$STARTER is hard-linked to $NEXT" - echo - return 1 - fi - echo_success "$STARTER is not hard-linked to $NEXT" - echo - done -} - -assert_older_than() { - if [[ $1 -ot $2 ]]; then - echo_failure "$1 is older than $2" - echo - return 1 - fi - echo_success "$1 is not older than $2" - echo -} diff --git a/t/link-counting.t b/t/link-counting.t new file mode 100644 index 0000000..e8d167c --- /dev/null +++ b/t/link-counting.t @@ -0,0 +1,24 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 3; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/link-counting"; +my @files = ( + "$test_dir/most-links", + "$test_dir/second-most-links", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +&run_script($test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok &are_hardlinked(@files), 'files with existing links got hardlinked'; diff --git a/t/normal-linkage.t b/t/normal-linkage.t new file mode 100644 index 0000000..328f1fa --- /dev/null +++ b/t/normal-linkage.t @@ -0,0 +1,24 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 3; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/normal"; +my @files = ( + "$test_dir/foo/same", + "$test_dir/same", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +&run_script($test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok &are_hardlinked(@files), 'files with the same contents got hardlinked'; diff --git a/t/normal-non-linkage.t b/t/normal-non-linkage.t new file mode 100644 index 0000000..40377fd --- /dev/null +++ b/t/normal-non-linkage.t @@ -0,0 +1,24 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 3; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/normal"; +my @files = ( + "$test_dir/foo/same", + "$test_dir/not-same", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +&run_script($test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok !&are_hardlinked(@files), 'files with different contents did not get hardlinked'; diff --git a/t/run.sh b/t/run.sh deleted file mode 100755 index f8dd97e..0000000 --- a/t/run.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash -# vi: et sts=4 sw=4 ts=4 - -WORKDIR=${0%/*} -. "$WORKDIR/funcs.sh" -SCRIPT=${SCRIPT:-$(realpath -- "$WORKDIR/../simplify_static_dir-main.pl")} -TAR=$(realpath -- "$WORKDIR/t.tar") -TEMPDIR=$(mktemp -d -t "${0##*/}.XXXXXX") - -_prep_tar() { - rm -rf t && - tar xf "$TAR" -} -cleanup() { - rm -rf -- "$TEMPDIR" -} -trap 'cleanup' EXIT - -test_normal_linkage() { - _prep_tar && - $SCRIPT t || return 2 - local -r \ - FILE1=t/normal/foo/same \ - FILE2=t/normal/same - assert_file_exists "$FILE1" "$FILE2" && - assert_hardlinked "$FILE1" "$FILE2" -} - -test_normal_nonlinkage() { - _prep_tar && - $SCRIPT t || return 2 - local -r \ - FILE1=t/normal/foo/same \ - FILE2=t/normal/not-same - assert_file_exists "$FILE1" "$FILE2" && - assert_nothardlinked "$FILE1" "$FILE2" -} - -test_sha1collision_nonlinkage() { - _prep_tar && - $SCRIPT t || return 2 - local -r \ - FILE1=t/sha1-collision/shattered-1.pdf \ - FILE2=t/sha1-collision/shattered-2.pdf - assert_file_exists "$FILE1" "$FILE2" && - assert_nothardlinked "$FILE1" "$FILE2" -} - -test_zero_size_nonlinkage() { - _prep_tar && - $SCRIPT t || return 2 - local -r \ - FILE1=t/zero-size/empty1 \ - FILE2=t/zero-size/empty2 - assert_file_exists "$FILE1" "$FILE2" && - assert_nothardlinked "$FILE1" "$FILE2" -} - -test_link_counting() { - _prep_tar && - $SCRIPT t || return 2 - local -r FILES=( - t/link-counting/{most-links,second-most-links} - ) - assert_file_exists "${FILES[@]}" && - assert_hardlinked "${FILES[0]}" "${FILES[1]}" -} - -test_timestamp_preservation() { - _prep_tar && - $SCRIPT t || return 2 - local -r \ - FILE1=t/timestamp-preservation/newer-more-linked \ - FILE2=t/timestamp-preservation/older-less-linked - assert_file_exists "$FILE1" "$FILE2" && - assert_hardlinked "$FILE1" "$FILE2" && - assert_older_than "$FILE2" "$FILE1" -} - -test_freed_bytes() { - _prep_tar && - local -r OUT=$($SCRIPT -f t/freed-bytes{,,} 2>&1 |tail -1) - local -r FILES=( - t/freed-bytes/{1,2,3,4} - ) - assert_file_exists "${FILES[@]}" && - assert_equals "$OUT" "freed 24 bytes (24 B)" -} - -test_freed_bytes_commas() { - _prep_tar && - local -r OUT=$($SCRIPT -f t/output-commas 2>&1 |tail -1) - assert_equals "$OUT" "freed 1,048,576 bytes (1 MB)" -} - -cd "$TEMPDIR" || exit -TEST_COUNT=0 -TESTS_PASSED=0 -for TESTNAME in \ - test_normal_linkage \ - test_normal_nonlinkage \ - test_sha1collision_nonlinkage \ - test_zero_size_nonlinkage \ - test_link_counting \ - test_timestamp_preservation \ - test_freed_bytes \ - test_freed_bytes_commas \ - ; do - - (( TEST_COUNT++ )) - if $TESTNAME; then - (( TESTS_PASSED++ )) - else - echo_failure "$TESTNAME failed" - echo - fi - -done - -cleanup - -printf '%d/%d tests passed\n' "$TESTS_PASSED" "$TEST_COUNT" -if [[ $TESTS_PASSED -ne $TEST_COUNT ]]; then - exit 1 -fi diff --git a/t/sha1collision-non-linkage.t b/t/sha1collision-non-linkage.t new file mode 100644 index 0000000..7fc53ec --- /dev/null +++ b/t/sha1collision-non-linkage.t @@ -0,0 +1,24 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 3; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/sha1-collision"; +my @files = ( + "$test_dir/shattered-1.pdf", + "$test_dir/shattered-2.pdf", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +&run_script($test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok !&are_hardlinked(@files), 'files with the same SHA-1 hash did not get hardlinked'; diff --git a/t/t.tar b/t/t.tar index a02062a..4273b4a 100644 Binary files a/t/t.tar and b/t/t.tar differ diff --git a/t/timestamp-preservation.t b/t/timestamp-preservation.t new file mode 100644 index 0000000..ca5929e --- /dev/null +++ b/t/timestamp-preservation.t @@ -0,0 +1,26 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 4; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/timestamp-preservation"; +my @files = ( + "$test_dir/newer-more-linked", + "$test_dir/older-less-linked", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +my $should_have_mtime = &filemtime($files[1]); +&run_script($test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok &are_hardlinked(@files); +ok &has_mtime($should_have_mtime, @files), 'timestamps updated to use oldest'; diff --git a/t/zero-size-non-linkage.t b/t/zero-size-non-linkage.t new file mode 100644 index 0000000..9093d35 --- /dev/null +++ b/t/zero-size-non-linkage.t @@ -0,0 +1,24 @@ +#!/usr/bin/perl +# vi: et sts=4 sw=4 ts=4 +use strict; +use warnings; + +use Test::Simple + tests => 3; + +use FindBin qw//; +use lib $FindBin::RealBin; +use TestFunctions; + +my $tarball_dir = &prep_tar; +my $test_dir = "$tarball_dir/t/zero-size"; +my @files = ( + "$test_dir/empty1", + "$test_dir/empty2", +); + +# Smoke test +ok !&are_hardlinked(@files), 'not hardlinked before we start'; +&run_script($test_dir); +ok &file_exists(@files), 'files were not accidentally deleted'; +ok !&are_hardlinked(@files), 'zero-sized files did not get hardlinked';