http://qs321.pair.com?node_id=11124108

kcott has asked for the wisdom of the Perl Monks concerning the following question:

I'm extending an application for $work and am seeking some advice on testing unexpected I/O failures.

The application receives a lot of parameters and I perform sanity checks on these. There are three parameters of interest with respect to this SoPW, in brief:

The application works fine if good parameters are supplied. There's a total of nine (file-related) sanity checks; I've successfully tested all of these with bad parameters.

The actual I/O is very straightforward; e.g.

open my $in_fh, '<', $input_file_path ... open my $out_fh, '>', $output_file_path ...

The error messages on failure are similarly standard:

Can't open $input_file_path for reading: $! Can't open $output_file_path for writing: $!

Given the sanity checks, I/O failure would be unexpected but still possible; for instance, between the sanity checks and an open call, a file could be deleted or renamed, it's permissions changed, a hardware failure could occur, and so on.

I managed to test the write failure by running normally; manually removing the write permissions of the output file; and then running again with the same parameters.
Yes, I remembered to put the write permissions back after this test. :-)

So that just leaves me with testing the read failure; unfortunately, I can't think of a way to do that. Any ideas would be greatly appreciated.

While I do like to test everything, if this last test can't be done it's not a huge problem. The code is very straightforward (I've probably written similar code thousands of times in the past); the syntax is fine (perl -c); and, I know it works with good parameters.

— Ken

Replies are listed 'Best First'.
Re: Testing unexpected I/O failures
by Fletch (Bishop) on Nov 24, 2020 at 05:26 UTC

    Not exactly sure what failure mode for reading you're trying to emulate. Are you checking with -r then if that succeeds doing the open? Because if so then that's introducing a possible race condition. In general it's better to do whatever open operation and deal with the failure than do a test and then take action (the former will be atomic; the latter opens a window something else can change the state of the filesystem under you).

    That being said if that is what you're trying to emulate I want to say that I've seen weird access and permissions behavior when running as root on a filesystem with the squash root option on (then again I may be recalling something in Tcl where it was using the access(3) library routine which short circuited and always returned true for root).

    The cake is a lie.
    The cake is a lie.
    The cake is a lie.

      G'day Fletch,

      Overall, there are a lot of input parameters; currently, there are about 50 possible messages that can arise when the sanity checks are run. Most of these are not related to any file checks: "A can't be zero-length"; "B must be an integer"; "C can't be greater than D"; and so on. There are nine file-related checks as outlined in the OP.

      I wanted to get all sanity checks performed prior to starting the processing proper; which begins with opening the input file. I considered it to be more useful to advise the user upfront that there was a specific problem with the directory or file parameter supplied, rather than offering a generic $! message from open, such as "File not found", "Permission denied", or similar.

      Your suggestion of doing the open first with supplied values (i.e. dir_param/file_param) and then dealing with failure might be a better way to go. Something like:

      open ... or do { ... };

      With the do determining more specific messages: "Directory was not supplied"; "File specified does not exist"; and so on.

      If users get a specific message, it may indicate something they can fix themselves, especially if it's a simple typo; e.g. file parameter supplied as file.tx but should be file.txt. When presented with a generic message, users may not be able to investigate themselves and may, unfortunately, provide the oft seen error report: "It didn't work".

      — Ken

        The idiom I've normally used is more along the lines of something like this; call open then use constants from Errno to figure out from !$ what specifically went wrong and give more detailed advice. You can also use a variation on this to (e.g.) try and open a file for reading and fall back to another default location if !$ == ENOENT because it didn't exist; (EDIT) or you don't care if it didn't exist, but other errors you do want to bring to the user's attention.

        use Errno qw( :POSIX ); use Log::Log4perl; ## ... if( open( my $fh, q{<}, $filename ) ) { my $frobulated_data = {}; ## process with $fh close( $fh ); return $frobulated_data; } else { if( $! == ENOENT ) { ERROR( qq{Couldn't find file '$filename': $!} ); } elsif( $! == EACCES ) { ERROR( qq{Problems with permissions for '$filename': $!} ); } elsif( ## whatever else specific you want to handle ... ) { #... } else { ERROR( qq{Problem opening '$filename': $!} ); } return; }

        The cake is a lie.
        The cake is a lie.
        The cake is a lie.

Re: Testing unexpected I/O failures
by GrandFather (Saint) on Nov 24, 2020 at 03:25 UTC

    Does Test::MockFile help?

    Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond

      G'day GrandFather,

      Thanks for the suggestion. I had a look at the documentation for that module. From what I read, its main purpose is to allow testing of code/logic without touching the filesystem, which isn't really my problem.

      Whether a real or a mocked file, there would only be a few CPU cycles between the file checks (-e, -r, etc.) and the open call: that only provides a very tiny window for something to change such that, for instance, -r is successful but open subsequently finds that the file does not have read permission.

      Anyway, thanks for replying and offering a suggestion.

      — Ken

        So, do you want to be able to test what happens (or detect it) when within these few CPU cycles some other process modifies the file while your Perl process does IO? Or do you want to ignore this risk?

        As a side remark, on top of race conditions you also have buffered IO routines. Do these keep IOing even if the file is gone / file permissions changed until the buffer is exhausted? If that's the case you will always get a delayed picture.

Re: Testing unexpected I/O failures
by jszinger (Scribe) on Nov 24, 2020 at 17:49 UTC
    Run two processes. The first changes the input file as fast as possible. The second one runs the test until an error occurs. If you get the right error, then the test passes; otherwise the test fails. Running more copies of the test at once should increase the chances of hitting the race condition. But why have the race condition at all? Opening the input file provides the needed feedback about the parameter. Are you sure you also don’t have a race condition the directory and output checks?

      G'day jszinger,

      I had considered something along the lines you suggest (with multiple processes) but decided it was too hit-and-miss for my liking. Looking at the situation where one process changes read permissions, it is more likely this will happen before -r fails or after open succeeds, than in the very tiny window between -r and open.

      Please see my response to ++Fletch's post regarding my reasons for wanting specific parameter checks and reporting. The solution he suggests is along the same lines as yours: at the moment, that's the most likely course of action that I'll follow.

      I'm doing file checks on both the directory and file parameters so, yes, race conditions are possible there.

      — Ken

Re: Testing unexpected I/O failures
by Don Coyote (Hermit) on Nov 24, 2020 at 11:54 UTC

    Just a wandering high level thought, would it be feasible to fork a process that evaluates not select, prior to sanity checks and ending after an otherwise successful open? The idea being this may provide some information about an unexpected event occuring.

    Whether this is something already considered or worth further investigating I would be interested to understand.

      G'day Don Coyote,

      Thanks for the reply and your suggestion.

      At present, I would imagine any unexpected events of this nature to be extremely rare. If that situation changes, something along the lines of what you have here may be useful; however, at this stage, I'm not considering implementing anything like this.

      — Ken