Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Test::MockFile

Perl module for mocking file system operations in unit tests. Intercepts `stat`, `lstat`, `-X` operators, `open`, `sysopen`, `opendir`, and related calls so tests run without touching disk.

## Architecture

- **Core**: `lib/Test/MockFile.pm` (~2530 lines) — main module, CORE::GLOBAL overrides, strict mode
- **FileHandle**: `lib/Test/MockFile/FileHandle.pm` — tied file handle for mocked files
- **DirHandle**: `lib/Test/MockFile/DirHandle.pm` — directory handle for mocked dirs
- **Plugin system**: `lib/Test/MockFile/Plugin.pm`, `Plugins.pm`, `Plugin/FileTemp.pm`
- **Key dependency**: `Overload::FileCheck` (XS) — enables `-X` operator interception

## How It Works

1. `Overload::FileCheck` hooks `-X` operators and `stat`/`lstat` via XS
2. `CORE::GLOBAL::*` overrides intercept `open`, `sysopen`, `opendir`, `readdir`, etc.
3. Mocked files stored in `%files_being_mocked` hash (path → blessed object, weakref)
4. Strict mode (default ON) dies on unmocked file access — configurable via rules

## Build & Test

```bash
perl Makefile.PL && make && make test
```

Dependencies installed via `cpanfile` (for CI: `cpm install -g`).

## Conventions

- **Perl style**: `.perltidyrc` in repo root — run `perltidy` before committing
- **POD**: `.podtidy-opts` — documentation inline in `.pm` files
- **Minimum Perl**: 5.14 (code uses `goto` on CORE functions, available 5.16+; workaround for 5.14)
- **Branch naming**: feature branches off `master`

## CI

- GitHub Actions: `.github/workflows/linux.yml`
- Matrix: Perl 5.14–5.40 on `perldocker/perl-tester`
- Env: `PERL_USE_UNSAFE_INC=0`, `AUTHOR_TESTING=1`

## Key Internals

- `_upgrade_barewords()` — converts bareword filehandles to typeglobs
- `_find_file_or_fh()` — resolves paths/handles, follows symlinks (max depth 10)
- `_abs_path_to_file()` — normalizes paths (strips `//`, `./`, trailing `/`)
- `_strict_mode_violation()` — enforces strict mode with stack inspection
- `_goto_is_available()` — detects Perl versions where `goto \&CORE::func` works
- Strict rules: `@STRICT_RULES` array, evaluated in order (first match wins)

## Open Issues (25)

Notable: #158 (glob corruption), #112 (flock), #77 (seek/truncate/tell), #44 (autodie compat), #27 (two handles same file). Full list: https://github.com/cpanel/Test-MockFile/issues
22 changes: 11 additions & 11 deletions lib/Test/MockFile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ add rules work for you.
=item C<$command_rule> a string or regexp or list of any to indicate
which command to match

=itemC<$file_rule> a string or regexp or undef or list of any to indicate
=item C<$file_rule> a string or regexp or undef or list of any to indicate
which files your rules apply to.

=item C<$action> a CODE ref or scalar to handle the exception.
Expand Down Expand Up @@ -644,7 +644,7 @@ sub file {

if ( @stats > 1 ) {
confess(
sprintf 'Unkownn arguments (%s) passed to file() as stats',
sprintf 'Unknown arguments (%s) passed to file() as stats',
join ', ', @stats
);
}
Expand Down Expand Up @@ -846,7 +846,7 @@ sub dir {
# TODO: Add stat information

# FIXME: Quick and dirty: provide a helper method?
my $has_content = grep m{^\Q$path/\E}xms, %files_being_mocked;
my $has_content = grep m{^\Q$path/\E}xms, keys %files_being_mocked;
return $class->new(
{
'path' => $path,
Expand Down Expand Up @@ -1974,7 +1974,7 @@ sub __sysopen (*$$;$) {
sub __opendir (*$) {

# Upgrade but ignore bareword indicator
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[9];
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it really what we want there? 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ve the feeling that this wrong and we intentionally check defined on 0 and ref on 9


my $mock_dir = _get_file_object( $_[1] );

Expand Down Expand Up @@ -2023,7 +2023,7 @@ sub __opendir (*$) {
sub __readdir (*) {

# Upgrade but ignore bareword indicator
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[9];
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[0];

my $mocked_dir = _get_file_object( $_[0] );

Expand Down Expand Up @@ -2064,7 +2064,7 @@ sub __readdir (*) {
sub __telldir (*) {

# Upgrade but ignore bareword indicator
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[9];
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[0];

my ($fh) = @_;
my $mocked_dir = _get_file_object($fh);
Expand All @@ -2091,7 +2091,7 @@ sub __telldir (*) {
sub __rewinddir (*) {

# Upgrade but ignore bareword indicator
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[9];
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[0];

my ($fh) = @_;
my $mocked_dir = _get_file_object($fh);
Expand Down Expand Up @@ -2119,7 +2119,7 @@ sub __rewinddir (*) {
sub __seekdir (*$) {

# Upgrade but ignore bareword indicator
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[9];
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[0];

my ( $fh, $goto ) = @_;
my $mocked_dir = _get_file_object($fh);
Expand All @@ -2146,7 +2146,7 @@ sub __seekdir (*$) {
sub __closedir (*) {

# Upgrade but ignore bareword indicator
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[9];
( undef, @_ ) = _upgrade_barewords(@_) if defined $_[0] && !ref $_[0];

my ($fh) = @_;
my $mocked_dir = _get_file_object($fh);
Expand Down Expand Up @@ -2476,10 +2476,10 @@ Filehandles can provide the file descriptor (in number) using the
C<fileno> keyword but this is purposefully unsupported in
L<Test::MockFile>.

The reaosn is that by mocking a file, we're creating an alternative
The reason is that by mocking a file, we're creating an alternative
file system. Returning a C<fileno> (file descriptor number) would
require creating file descriptor numbers that would possibly conflict
with the file desciptors you receive from the real filesystem.
with the file descriptors you receive from the real filesystem.

In short, this is a recipe for buggy tests or worse - truly destructive
behavior. If you have a need for a real file, we suggest L<File::Temp>.
Expand Down