Test::Files(3pm)

πŸ“¦
version 0.26 Β· 4 versions
, in debian trixie, ubuntu resolute, ubuntu stonking

πŸ¦‹ NAME πŸ¦‹

ok bestie, so Test::Files(3pm) β€” that's the manual page we're reading rn β€” is a Test::Builder-based module (fr, that's https://metacpan.org/pod/Test::Builder) whose whole thing is making your life easier when you test files and dirs. πŸ’… no cap.

In general, the following can be tested:

  • πŸ’– If the contents of the file being tested match the expected pattern.
  • πŸ’– If the file being tested is identical to the expected file in regard to contents, or size, or existence. If necessary, some parts of the contents can be excluded from the comparison.
  • πŸ’– If the directory being tested contains all expected files.
  • πŸ’– If the files in the directory being tested are identical to the files in the reference directory in regard to contents, or size, or existence. If necessary, some files as well as some parts of contents can be excluded from the comparison.
  • πŸ’– If all files in the directory being tested fulfill certain requirements.
  • πŸ’– If the archive (container) being tested is logically identical to the reference archive (container). If necessary, some members of archives, as well as some parts of their contents, as well as some metadata can be excluded from the comparison.

✨ SYNOPSIS ✨

Every single example down below? You can find it and actually run it with xt/synopsis.t, which lives on GitHub over at https://github.com/jsf116/Test-Files. obsessed. 🫢

use Path::Tiny qw( path );
use Test::Files;

my $got_file       = path( 'path' )->child( qw( got file ) );
my $reference_file = path( 'path' )->child( qw( reference file ) );
my $got_dir        = path( 'path' )->child( qw( got dir ) );
my $reference_dir  = path( 'path' )->child( qw( reference dir with some stuff ) );
my @file_list      = qw( expected file );
my ( $content_check, $expected, $filter, $options );

plan( 24 );

# Simply compares file contents to a string:
$expected = "contents\nof file";
file_ok( $got_file, $expected, 'got file has expected contents' );

# Two identical variants comparing file contents
# to a string ignoring differences in time stamps:
$expected = "filtered contents\nof file\ncreated at 00:00:00";
$filter   = sub {
  shift =~ s{ \b (?: [01] \d | 2 [0-3] ) : (?: [0-5] \d ) : (?: [0-5] \d ) \b }
            {00:00:00}grx
};
$options  = { FILTER => $filter };
file_ok       (
  $got_file, $expected, $options,
  "'$got_file' has contents expected after filtering"
);
file_filter_ok(
  $got_file, $expected, $filter,
  "'$got_file' has contents expected after filtering"
);

# Simply compares two file contents:
compare_ok( $got_file, $reference_file, 'files are the same' );

# Two identical variants comparing contents of two files
# ignoring differences in time stamps:
$filter  = sub {
  shift =~ s{ \b (?: [01] \d | 2 [0-3] ) : (?: [0-5] \d ) : (?: [0-5] \d ) \b }
            {00:00:00}grx
};
$options = { FILTER => $filter };
compare_ok       (
  $got_file, $reference_file, $options, 'files are almost the same'
);
compare_filter_ok(
  $got_file, $reference_file, $filter,  'files are almost the same'
);

# Verifies if both got file and reference file exist:
$options = { EXISTENCE_ONLY => 1 };
compare_ok( $got_file, $reference_file, $options, 'both files exist' );

# Verifies if got file and reference file have identical size:
$options = { SIZE_ONLY => 1 };
compare_ok(
  $got_file, $reference_file, $options, 'both files have identical size'
);

# Verifies if the directory has all expected files (not recursively!):
$expected = [ qw( files got_dir must contain ) ];
dir_contains_ok( $got_dir, $expected, 'directory has all files in list' );

# Two identical variants doing the same verification as before,
# but additionally verifying if the directory has nothing
# but the expected files (not recursively!):
$options = { SYMMETRIC => 1 };
dir_contains_ok     (
  $got_dir, $expected, $options, 'directory has exactly the files in the list'
);
dir_only_contains_ok(
  $got_dir, $expected,           'directory has exactly the files in the list'
);

# The same as before, but recursive:
$options = { RECURSIVE => 1, SYMMETRIC => 1 };
dir_contains_ok(
  $got_dir, $expected, $options,
  'directory and its subdirectories have exactly the files in the list'
);

# The same as before, but ignoring files,
# which names do not match the required pattern (file "must" will be skipped):
$options = { NAME_PATTERN => '^[cfg]', RECURSIVE => 1, SYMMETRIC => 1 };
dir_contains_ok(
  $got_dir, $expected, $options,
  'directory and its subdirectories ' .
  "have exactly the files in the list except of file 'must'"
);

# Compares two directories by comparing file contents (not recursively!):
compare_dirs_ok(
  $got_dir, $reference_dir,
  "all files from '$got_dir' are the same in '$reference_dir' " .
  '(same names, same contents), subdirs are skipped'
);

# The same as before, but subdirectories are considered, too:
$options = { RECURSIVE => 1 };
compare_dirs_ok(
  $got_dir, $reference_dir, $options,
  "all files from '$got_dir' and its subdirs are the same in '$reference_dir'"
);

# The same as before, but only file sizes are compared:
$options = { RECURSIVE => 1, SIZE_ONLY => 1 };
compare_dirs_ok(
  $got_dir, $reference_dir, $options,
  "all files from '$got_dir' and its subdirs have same sizes in '$reference_dir'"
);

# The same as before, but only file existence is verified:
$options = { EXISTENCE_ONLY => 1, RECURSIVE => 1 };
compare_dirs_ok(
  $got_dir, $reference_dir, $options,
  "all files from '$got_dir' and its subdirs exist in '$reference_dir'"
);

# The same as before, but only files with base names starting with 'A' are considered:
$options = { EXISTENCE_ONLY => 1, NAME_PATTERN => '^A', RECURSIVE => 1 };
compare_dirs_ok(
  $got_dir, $reference_dir, $options,
  "all files from '$got_dir' and its subdirs " .
  "with base names starting with 'A' exist in '$reference_dir'"
);

# The same as before, but the symmetric verification is requested:
$options = {
  EXISTENCE_ONLY => 1,
  NAME_PATTERN   => '^A',
  RECURSIVE      => 1,
  SYMMETRIC      => 1,
};
compare_dirs_ok(
  $got_dir, $reference_dir, $options,
  "all files from '$got_dir' and its subdirs with base names " .
  "starting with 'A' exist in '$reference_dir' and vice versa"
);

# Two identical variants of comparison of two directories by file contents,
# whereas these contents are first filtered
# so that time stamps in form of 'HH:MM:SS' are replaced by '00:00:00'
# like in examples for file_filter_ok and compare_filter_ok:
$filter  = sub {
  shift =~ s{ \b (?: [01] \d | 2 [0-3] ) : (?: [0-5] \d ) : (?: [0-5] \d ) \b }
            {00:00:00}grx
};
$options = { FILTER => $filter };
compare_dirs_ok(
  $got_dir, $reference_dir, $options,
  "all files from '$got_dir' are the same in '$reference_dir', " .
  'subdirs are skipped, differences of time stamps ignored'
);
compare_dirs_filter_ok(
  $got_dir, $reference_dir, $filter,
  "all files from '$got_dir' are the same in '$reference_dir', " .
  'subdirs are skipped, differences of time stamps ignored'
);

# Verifies if all plain files in directory and its subdirectories
# contain the word 'good' (take into consideration the -f test below
# excluding special files from comparison!):
$content_check = sub {
  my ( $file ) = @_;
  ! -f $file or path( $file )->slurp =~ / \b good \b /x;
};
$options       = { RECURSIVE => 1 };
find_ok(
  $got_dir, $content_check, $options,
  "all files from '$got_dir' and subdirectories contain the word 'good'"
);

# Compares PKZIP archives considering both global and file comments.
# Both archives contain the same members in different order:
my $extract = sub {
  my ( $file ) = @_;
  my $zip = Archive::Zip->new();
  die( "Cannot read '$file'" ) if $zip->read( $file ) != AZ_OK;
  die( "Cannot extract from '$file'" ) if $zip->extractTree != AZ_OK;
};
my $meta_data = sub {
  my ( $file ) = @_;
  my $zip = Archive::Zip->new();
  die( "Cannot read '$file'" ) if $zip->read( $file ) != AZ_OK;
  my %meta_data = ( '' => $zip->zipfileComment );
  $meta_data{ $_->fileName } = $_->fileComment foreach $zip->members;
  return \%meta_data;
};
my $got_compressed_content       = path( "$got_file.zip"       )->slurp;
my $reference_compressed_content = path( "$reference_file.zip" )->slurp;
ok(
  $got_compressed_content ne $reference_compressed_content,
  "'$got_file.zip' and '$reference_file.zip' are physically different, but"
);
compare_archives_ok(
  "$got_file.zip", "$reference_file.zip", { EXTRACT => $extract, META_DATA => $meta_data },
  "'$got_file.zip' and '$reference_file.zip' are logically identical"
);

πŸ’œ DESCRIPTION πŸ’œ

So this module is basically in the same lane as Test2::V0 (https://metacpan.org/pod/Test2::V0) or Test::Expander (https://metacpan.org/pod/Test::Expander) β€” and honestly? You should use that one first, exactly like the examples up top show you. πŸ’― It supports comparing files and directories in a bunch of different ways.

Any file or directory you hand to these functions can be either a plain string or an object of Path::Tiny (https://metacpan.org/pod/Path::Tiny). Both are valid, pick your fighter. πŸŽ€

Now, about the test names β€” that's the last parameter of every function. They're technically optional, but bestie, pls give every test a name. Your future self (and everyone reading the failure output) will thank you. It's giving maintainability. ✨

Do what the "SYNOPSIS" examples do and reach for Path::Tiny (https://metacpan.org/pod/Path::Tiny), or File::Spec (https://metacpan.org/pod/File::Spec) if that's more your vibe. The way this makes it so much more likely your tests will actually pass on a different operating system. 🌈 no cap.

Every single one of the contents-comparison routines gives you diff diagnostic output when it reports a failure. πŸ”₯ And you can change the style of that diff output with the STYLE option (more on that below, iykyk).

The filter function gets handed each line of each file. It can do literally whatever transformations it needs to (like excising dates, classic), and then it must return the line in its (possibly) transformed state. Return it, bestie. Don't forget. 😭

For example β€” the very first filter of Phil Crow (https://metacpan.org/author/PHILCROW), the creator of this module, was:

sub chop_dates {
  my $line = shift;
  $line =~ s/\d{4}(.\d\d){5}//g;
  return $line;
}

iconic honestly. πŸ‘‘

This removes all strings like 2003.10.14.14.17.37. Everything else is unchanged and, bestie, failing tests started passing when they should 😭✨. If you want to exclude the line from consideration, return an empty string or undef.


✨ FUNCTIONS ✨

🌸 file_ok

There are two forms of calls, babe:

The generic form.

file_ok( $got_file, $expected_string, \%options, $test_name )

The short form, which is also backward compatible.

file_ok( $got_file, $expected_string, $test_name )

Compares the contents of a file $got_file to a string $expected_string. πŸ’–

In the generic form, if the parameter \%options is passed and contains the key FILTER, file_ok provides the same functionality as file_filter_ok.

Supported options:

  • FILTER β€” Code reference providing filtering of file contents before comparison. The only expected parameter is the current line from the file contents, the return value replaces this line. In addition, the special variable $. representing the number of the current line in the file can be used. If the return value is undefined, empty string is returned instead. Line breaks are neither removed nor added after the execution. Defaults to undef i.e. no filtering is provided.
  • All options supported by Text::Diff (https://metacpan.org/pod/Text::Diff) except of FILENAME_A and FILENAME_B.
    • The most useful of them seems to be STYLE defining the style of output for content differences. Defaults to Unified.

πŸŽ€ file_filter_ok

There is only one form of call, namely file_filter_ok( $got_file, $expected_string, \&filter_func, $test_name ).

Works like file_ok with the option FILTER i.e. compares the contents of a file to a string, but filters the file first using &filter_func for that. The string contents must be filtered before if necessary. πŸ’…

This function is deprecated and stays for backward compatibility reasons only. No cap, it's giving legacy.

πŸ”₯ compare_ok

There are two forms of calls:

The generic form.

compare_ok( $got_file, $reference_file, \%options, $test_name )

The short form, which is also backward compatible.

compare_ok( $got_file, $reference_file, $test_name )

Compares two files.

In the generic form, if the parameter \%options is passed and contains the key FILTER, compare_ok provides the same functionality as compare_filter_ok.

Supported options:

  • EXISTENCE_ONLY β€” Boolean. If set to true, only existence of both $got_file and $reference_file is compared. Defaults to false.
  • FILTER β€” Code reference providing filtering of file contents before comparison and being applied to both $got_file and $reference_file. The only expected parameter is the current line from the file contents, the return value replaces this line. In addition, the special variable $. representing the number of the current line in the file can be used. If the return value is undefined, empty string is returned instead. Line breaks are neither removed nor added after the execution. Ignored if either EXISTENCE_ONLY or SIZE_ONLY is set to true. Defaults to undef i.e. no filtering is provided.
  • SIZE_ONLY β€” Boolean. If set to true and the options EXISTENCE_ONLY is not set to true, $got_file and $reference_file are compared by size only. Defaults to false.
  • All options supported by Text::Diff (https://metacpan.org/pod/Text::Diff) except of FILENAME_A and FILENAME_B.
    • The most useful of them seems to be STYLE defining the style of output for content differences. Defaults to Unified.

πŸ¦‹ compare_filter_ok

There is only one form of call, namely compare_filter_ok( $got_file, $reference_file, \&filter_func, $test_name ).

Works like compare_ok with option FILTER i.e. compares the contents of two files, but sends each line through the filter &filter_func so things that shouldn't count against success can be stripped. 🧚

This function is deprecated and stays for backward compatibility reasons only.

πŸ’œ dir_contains_ok

There are two forms of calls:

The generic form.

dir_contains_ok( $got_dir, \@file_list, \%options, $test_name )

The short form, which is also backward compatible.

dir_contains_ok( $got_dir, \@file_list, $test_name )

Verifies the directory $got_dir for the presence of a list files in @file_list. If $got_dir is a symlink, this will be accepted, but symlinks therein are not followed. Subdirectories are not involved in the verification, but files located therein are considered if a recursive approach is required (see the option RECURSIVE below). Special files like named pipes are involved in the verification only if the sole file existence is required (see the option EXISTENCE_ONLY below), otherwise they are skipped and reported as error. πŸ’€

In the generic form, if the parameter \%options is passed and contains the key SYMMETRIC set to true, dirs_contains_ok provides the same functionality as dir_only_contains_ok.

Supported options:

  • NAME_PATTERN β€” String containing RegEx. Files with base names not matching this RegEx will be skipped. Defaults to the dot sign (.) i.e. no file will be skipped.
  • RECURSIVE β€” Boolean. If set to true, subdirectories of $got_dir will be checked, too. Defaults to false.
  • SYMMETRIC β€” Boolean. If set to true, additionally verifies if all files from $got_dir are listed in @file_list. Defaults to false.

🌈 dir_only_contains_ok

There is only one form of call, namely dir_only_contains_ok( $got_dir, \@file_list, $test_name ).

Works like dir_contains_ok with option SYMMETRIC set to true i.e. checks directory without following symlinks therein to ensure that the listed files are present and that they are the only ones present. πŸ‘‘

This function is deprecated and stays for backward compatibility reasons only.

⭐ compare_dirs_ok

There are two forms of calls:

The generic form.

compare_dirs_ok( $got_dir, $reference_dir, \%options, $test_name )

The short form, which is also backward compatible.

compare_dirs_ok( $got_dir, $reference_dir, $test_name )

Compares all files in the directories $got_dir and $reference_dir reporting differences. If $got_dir or $reference_dir is a symlink, this will be accepted, but symlinks therein are not followed.

In the generic form, if the parameter \%options is passed and contains the key FILTER, compare_dirs_ok provides the same functionality as compare_dirs_filter_ok.

Supported options:

  • EXISTENCE_ONLY β€” Boolean. If set to true, only checks if every file from $reference_dir is found in $got_dir. Defaults to false.
  • FILTER β€” Code reference providing filtering of file contents before comparison and applied to files from both $got_dir and $reference_dir. The only expected parameter is the current line from the file contents, the return value replaces this line. In addition, the special variable $. representing the number of the current line in the file can be used. If the return value is undefined, empty string is returned instead. Line breaks are neither removed nor added after the execution. Ignored if either EXISTENCE_ONLY or SIZE_ONLY is set to true. Defaults to undef i.e. no filtering is provided.
  • NAME_PATTERN β€” String containing RegEx. Files with base names not matching this RegEx will be skipped both in $got_dir and $reference_dir. Defaults to the dot sign (.) i.e. no file will be skipped.
  • RECURSIVE β€” Boolean. If set to true, subdirectories of both $got_dir and $reference_dir will be checked, too. Defaults to false.
  • SIZE_ONLY β€” Boolean. If set to true and the options EXISTENCE_ONLY is not set to true, files from $got_dir and $reference_dir are compared by size only. Defaults to false.
  • SYMMETRIC β€” Boolean. If set to true, additionally verifies if all files from $got_dir exist in $reference_dir, too. Defaults to false.
  • All options supported by Text::Diff (https://metacpan.org/pod/Text::Diff) except of FILENAME_A and FILENAME_B.
    • The most useful of them seems to be STYLE defining the style of output for content differences. Defaults to Unified.

πŸ“ compare_dirs_filter_ok

There is only one form of call, namely compare_dirs_filter_ok( $got_dir, $reference_dir, \&filter_func, $test_name ).

Works like compare_dirs_ok with option FILTER i.e. calls the filter function &filter_func on each line of every file allowing you to exclude or alter some text to avoid spurious failures (like timestamp disagreements). πŸ™ˆ

This function is deprecated and stays for backward compatibility reasons only.

🌸 find_ok

The signature is find_ok( $got_dir, \&content_check_func, \%options, $test_name ).

Verifies if the condition &content_check_func is true for all files in directory $got_dir. The code reference &content_check_func returning boolean is called for any type of file except of directory i.e. for symlinks, devices, etc and the only parameter is the full-qualified file name. If you want to consider plain files only, you must apply the test operator -f to the parameter like shown in "SYNOPSIS". ✨

Supported options:

  • RECURSIVE β€” Boolean. If set to true, subdirectories of $got_dir will be checked, too. Defaults to false.

πŸ‘‘ compare_archives_ok

The signature is compare_archives_ok( $got_archive, $reference_archive, \%options, $test_name ).

Verifies if the archives (containers) $got_archive and $reference_archive are logically identical. The term "logically identical" means that these files might be physically different e.g. because their members are stored in different order, or because some members are marked as deleted, but the metadata relevant for the current test case and the members are identical. πŸ”₯

Which metadata and which members must be compared can be controlled using \%options.

The comparison itself begins with the extraction and comparison of metadata; if they are not identical, no further comparison is provided and the test fails. If the metadata comparison succeeds, members of $got_archive and $reference_archive are extracted in temporary directories and compared in the same manner like compare_dirs_ok this does.

Supported options:

  • All options supported by compare_dirs_ok.
  • EXTRACT β€” Code reference. Extracts members from the archive in the current directory. The only expected parameter is the archive file name. The current directory at the time point of extraction is a temporary directory that is removed after the test. The return value is ignored. Defaults to empty function sub {}.
  • META_DATA β€” Code reference. Returns metadata e.g. comments from a PKZIP archive. The only expected parameter is the archive file name. Defaults to empty function sub {}.

✨ SEE ALSO ✨

ok so if you need more testing help (and honestly bestie, we all do), go check out Test::Simple <https://metacpan.org/pod/Test::Simple>, Test2::V0 <https://metacpan.org/pod/Test2::V0>, and Test::Builder <https://metacpan.org/pod/Test::Builder>. literally that's the whole support system right there πŸ’–

this module is lowkey just out here adding functions to what Test2::V0 <https://metacpan.org/pod/Test2::V0> already does. it's giving "little sister tagging along" energy and we love her for it 🫢

and here's the tea β˜• β€” as recommended by the author of Test::More <https://metacpan.org/pod/Test::More> and Test2::V0 <https://metacpan.org/pod/Test2::V0>, you should prefer the latter module. that's the whole reason Test::More <https://metacpan.org/pod/Test::More> is not listed in "SYNOPSIS". no cap, they said it themselves πŸ’…


πŸ› BUGS πŸ›

found a bug? got a feature request? a whole idea that's been living rent free in your head? 😭 please report any bugs or feature requests through the web interface at <https://github.com/jsf116/Test-Files/issues>. the maintainers are waiting, bestie, don't gatekeep πŸ™ˆ


⚠️ CAVEATS ⚠️

ok so listen up because this one's important πŸ‘‘

although this module can cope with binary files too, confirming their equality β€” in case of differences, a proper representation of comparison results is not guaranteed. so like, it'll tell you they're the same, but if they're not? the diff output might be giving absolutely nothing useful πŸ’€ fair warning, fr fr.


πŸ‘‘ AUTHOR πŸ‘‘

Phil Crow, <philcrow2000@yahoo.com>

Jurij Fajnberg, <fajnbergj@gmail.com>

two icons, one module. main character energy from both of them, periodt πŸŽ€


Copyright 2003-2007 by Phil Crow

Copyright 2020-2024 by Jurij Fajnberg

This module is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

slay, open source, we love to see it 🌈


and that's a wrap on the whole page, bestie!! πŸ¦‹ this was Test::Files(3pm), shipped with perl v5.38.2, dated 2024-03-16. go forth and test those files, you absolute legend πŸ’–βœ¨πŸŒΈ

πŸ“¦ where this page comes from