# NAME

Test::Which - Skip tests if external programs are missing from PATH (with version checks)

# VERSION

Version 0.06

# SYNOPSIS

    use Test::Which 'ffmpeg' => '>=6.0', 'convert' => '>=7.1';

    # At runtime in a subtest or test body
    use Test::Which qw(which_ok);

    subtest 'needs ffmpeg' => sub {
            which_ok 'ffmpeg' => '>=6.0' or return;
            ... # tests that use ffmpeg
    };

# DESCRIPTION

`Test::Which` mirrors [Test::Needs](https://metacpan.org/pod/Test%3A%3ANeeds) but checks for executables in PATH.
It can also check version constraints using a built-in heuristic that tries
common version flags (--version, -version, -v, -V) and extracts version numbers
from the output.

If a version is requested but cannot be determined, the requirement fails.

Key features:

- Compile-time and runtime checking of program availability
- Version comparison with standard operators (>=, >, <, <=, ==, !=)
- Regular expression matching for version strings
- Custom version flag support for non-standard programs
- Custom version extraction for unusual output formats
- Caching to avoid repeated program execution
- Cross-platform support (Unix, Linux, macOS, Windows)

# EXAMPLES

## Basic Usage

Check for program availability without version constraints:

    use Test::Which qw(which_ok);

    which_ok 'perl', 'ffmpeg', 'convert';

## Version Constraints

Check programs with minimum version requirements:

    # String constraints with comparison operators
    which_ok 'perl' => '>=5.10';
    which_ok 'ffmpeg' => '>=4.0', 'convert' => '>=7.1';

    # Exact version match
    which_ok 'node' => '==18.0.0';

    # Version range
    which_ok 'python' => '>=3.8', 'python' => '<4.0';

## Hashref Syntax

Use hashrefs for more complex constraints:

    # String version in hashref
    which_ok 'perl', { version => '>=5.10' };

    # Regex matching
    which_ok 'perl', { version => qr/5\.\d+/ };
    which_ok 'ffmpeg', { version => qr/^[4-6]\./ };

## Custom Version Flags

Some programs use non-standard flags to display version information:

    # Java uses -version (single dash)
    which_ok 'java', {
        version => '>=11',
        version_flag => '-version'
    };

    # Try multiple flags in order
    which_ok 'myprogram', {
        version => '>=2.0',
        version_flag => ['--show-version', '-version', '--ver']
    };

    # Program prints version without any flag
    which_ok 'sometool', {
        version => '>=1.0',
        version_flag => '',
        timeout => 10,    # seconds - the default is 5
    };

    # Windows-specific flag
    which_ok 'cmd', {
        version => qr/\d+/,
        version_flag => '/?'
    } if $^O eq 'MSWin32';

If `version_flag` is not specified, the module tries these flags in order:
`--version`, `-version`, `-v`, `-V` (and `/?`, `-?` on Windows)

## Custom Version Extraction

For programs with unusual version output formats:

    which_ok 'myprogram', {
        version => '>=1.0',
        extractor => sub {
            my $output = shift;
            return $1 if $output =~ /Build (\d+\.\d+)/;
            return undef;
        }
    };

The extractor receives the program's output and should return the version
string or undef if no version could be found.

## Mixed Usage

Combine different constraint types:

    which_ok
        'perl' => '>=5.10',           # String constraint
        'ffmpeg',                      # No constraint
        'convert', { version => qr/^7\./ };  # Regex constraint

## Compile-Time Checking

Skip entire test files if requirements aren't met:

    use Test::Which 'ffmpeg' => '>=6.0', 'convert' => '>=7.1';

    # Test file is skipped if either program is missing or the version is too old
    # No tests below this line will run if requirements aren't met

## Runtime Checking in Subtests

Check requirements for individual subtests:

    use Test::Which qw(which_ok);

    subtest 'video conversion' => sub {
        which_ok 'ffmpeg' => '>=4.0' or return;
        # ... tests using ffmpeg
    };

    subtest 'image processing' => sub {
        which_ok 'convert' => '>=7.0' or return;
        # ... tests using ImageMagick
    };

## Absolute Paths

You can specify absolute paths instead of searching PATH:

    which_ok '/usr/local/bin/myprogram' => '>=1.0';

The program must be executable.

# VERSION DETECTION

The module attempts to detect version numbers using these strategies in order:

- 1. Look for version near the word "version" (case-insensitive)

    Matches patterns like: `ffmpeg version 4.2.7`, `Version: 2.1.0`

- 2. Extract dotted version from first line of output

    Common for programs that print version info prominently

- 3. Find any dotted version number in output

    Fallback for less standard formats

- 4. Look for single number near "version"

    For programs that use simple integer versioning

- 5. Use any standalone number found

    Last resort - least reliable

# VERSION COMPARISON

Version comparison uses Perl's [version](https://metacpan.org/pod/version) module. Versions are normalized
to have the same number of components before comparison to avoid
`version.pm`'s parsing quirks.

For example:
  - `2020.10` becomes `2020.10.0`
  - `2020.10.15` stays `2020.10.15`
  - Then they're compared correctly

Supported operators: `>=`, `>`, `<=`, `<`, `=`, `!=`

# CACHING

Version detection results are cached to avoid repeated program execution.
Each unique combination of program path and version flags creates a separate
cache entry.

Cache benefits:
\- Faster repeated checks in test suites
\- Reduced system load
\- Works across multiple test files in the same process

The cache persists for the lifetime of the Perl process.

# VERBOSE OUTPUT

Set environment variables to see detected versions:

    TEST_WHICH_VERBOSE=1 prove -v t/mytest.t
    TEST_VERBOSE=1 perl t/mytest.t
    prove -v t/mytest.t  # HARNESS_IS_VERBOSE is set automatically

Output includes the detected version for each checked program:

    # perl: version 5.38.0
    # ffmpeg: version 6.1.1

# PLATFORM SUPPORT

- **Unix/Linux/macOS**: Full support for all features
- **Windows**: Basic functionality supported. Complex shell features
(STDERR redirection, empty flags) may have limitations.

# DIAGNOSTICS

Common error messages:

- `Missing required program 'foo'`

    The program 'foo' could not be found in PATH.

- `Version issue for foo: no version detected`

    The program exists but the module couldn't extract a version number from
    its output. Try specifying a custom `version_flag` or `extractor`.

- `Version issue for foo: found 1.0 but need `=2.0>

    The program's version doesn't meet the constraint.

- `Version issue for foo: found version 1.0 but doesn't match pattern`

    For regex constraints, the detected version didn't match the pattern.

- `hashref constraint must contain 'version' key`

    When using hashref syntax, you must include a `version` key.

- `invalid constraint 'foo'`

    The version constraint string couldn't be parsed. Use formats like
    `'`=1.2.3'>, `'`2.0'>, or `'1.5'`.

# FUNCTIONS/METHODS

## which\_ok @programs\_or\_pairs

Checks the named programs (with optional version constraints).
If any requirement is not met, the current test or subtest is skipped
via [Test::Builder](https://metacpan.org/pod/Test%3A%3ABuilder).

Returns true if all requirements are met, false otherwise.

# SUPPORT

This module is provided as-is without any warranty.

# SEE ALSO

[Test::Needs](https://metacpan.org/pod/Test%3A%3ANeeds) - Similar module for checking Perl module availability

[File::Which](https://metacpan.org/pod/File%3A%3AWhich) - Used internally to locate programs

[version](https://metacpan.org/pod/version) - Used for version comparison

[Test::Builder](https://metacpan.org/pod/Test%3A%3ABuilder) - Used for test integration

# LIMITATIONS

- Version detection is heuristic-based and may fail for programs with
unusual output formats.
Use custom `version_flag` or `extractor` for such cases,
though even those may break.
- No built-in timeout for program execution. Hanging programs will hang tests.
- Cache persists for process lifetime - updated programs won't be re-detected
without restarting the test process.
- Requires programs to be in PATH or specified with absolute paths.

# AUTHOR

Nigel Horne, `<njh at nigelhorne.com>`

# LICENCE AND COPYRIGHT

Copyright 2025 Nigel Horne.

Usage is subject to licence terms.

The licence terms of this software are as follows:

- Personal single user, single computer use: GPL2
- All other users (including Commercial, Charity, Educational, Government)
  must apply in writing for a licence for use from Nigel Horne at the
  above e-mail.
