### ----------------------------------------------------
### -- Test::Selector
### -- Licenses: Artistic-2.0
### -- Authors: Luc St-Louis <lucs@pobox.com>
### -- File: README.md
### ----------------------------------------------------
[![Actions Status](https://github.com/lucs/Test-Selector/actions/workflows/test.yml/badge.svg)](https://github.com/lucs/Test-Selector/actions)

NAME
====

Test::Selector - Mark and selectively run only parts of test files

DESCRIPTION
-----------

During development of a module, you may want to run only one or more of the tests in its test files to see how your code is coming along; you may not yet care whether the other tests pass or not or you may just not want to have to wait for them to finish or to clutter up the screen.

The traditional way to run tests separately is to have them in different files, but this can lead to having many of them and it might be hard to find which ones are relevant at any point during development.

With this module, you will be able to wrap into labeled blocks parts of your test files and use the supplied `tsel` program to select which of these blocks to run. It also lets you specify where to find the test files and in which ones to look for the blocks. You can also specify where to find other modules that may be required, but not yet installed.

Labeled blocks
--------------

Suppose you have the following (uninteresting) code in a test file:

        my $str = 'abc';
        ok(
            $str.chars == $str.flip.chars,
            "A reversed string is the same length as the original.",
        );

For the purpose of this module, you would wrap it like this with some arbitrary label, here 's1':

        t s1 => {
            my $str = 'abc';
            ok(
                $str.chars == $str.flip.chars,
                "A reversed string is the same length as the original.",
            );
        }

Then to run specifically that block, from the command line you would invoke `▸ tsel s1`.

`t` is a subroutine exported by the module. It can be invoked in one of the following example ways:

        t some-label => { ⋯ }

        t $some-var => { ⋯ }

The latter form is useful when generating many similar tests; an example of this will be shown later.

The `tsel` program
------------------

By default, `tsel` expects you to run it from the root directory of your module in development; that is, it expects to find there a `./t` subdirectory with the test files, and a `./lib` subdirectory to which it will set RAKULIB before running the tests.

Here are a few example invocations:

        cd ⟨Module directory⟩
        tsel s1
        tsel -f=04 s\*
        tsel -q ⋯
        tsel -l s\*
        tsel -ri=⋯/SomeModule/lib,/⋯/OtherModule/lib ⋯

        cd ⟨Arbitrary directory⟩
        tsel -t=⋯/MyModule/t -r=⋯/MyModule/lib,⋯/OtherModule/lib ⋯

`tsel` has the following arguments:

        ⟨Blocks label pattern⟩ :

            Only blocks whose label matches the specified pattern will be
            run. Some characters in the pattern are special; here is what
            they match:

                *    : any string, zero or more characters
                ?    : exactly one character
                […]  : characters from a set, for example ‹[abc]›

            Default: ‹*›

            Here are some examples, escaping the special characters to
            prevent them from being expanded by the shell, and labels they
            could match or not:

                n\*      Yes: n, n1, nything     No: an
                \[ab]\*c Yes: axc, a23c, bc      No: bcd, ebc
                a1\?     Yes: a11, a1x           No: a1, a1bc

        -f=⟨Files prefix⟩ :

            Only files whose extension is ‹.rakutest›, ‹.t›, or ‹.t6› and
            whose name starts with the specified prefix characters will be
            used.

            Default: all files with the proper extension will be used.

            Example:

                -f=04   Yes: 04-baz.rakutest, 04.t6  No: 04-foo.raku

        -t=⟨Use test files found in these comma separated directories⟩
        -ti=⟨Use test files found in ./t and in these comma separated directories⟩

            At most one of these options may be specified. If none is
            specified, test files will be searched for in ./t, the idea being
            that you will usually be running the program from the root of
            the module in development.

        -r=⟨Prepend to RAKULIB these comma separated directories⟩
        -ri=⟨Prepend to RAKULIB ./lib and these comma separated directories⟩

            At most one of these options may be specified. If none is
            specified, ./lib will be prepended to RAKULIB, the idea being
            that you will usually be running the program from the root of
            the module in development.

        -l :

            This will only alphabetically list all matching block labels.

        -q :

            Run blocks more quietly, preventing ‹ok:› and ‹# Subtest:›
            lines from being output.

        -qq :

            Run blocks even more quietly, preventing also any line whose first
            non-blank char is ‹#› from being output.

        -v :

            This will print the module version, something like
            "Test::Selector 0.2.0 on rakudo (2023.02)", and immediately
            exit the program.

sub `label()`
-------------

Used in a labeled block, returns the block's label. For example:

            # If this block runs, it will print «My label is a42␤».
        t a42 => {
            say "My label is ", label;
        }

How do I skip running some blocks?
----------------------------------

Prepend one or two underscores to the block label:

        _   : The block will not be run, even if it matches the label
              glob, but a 'skipped' message will be output.

        __  : The block will be completely ignored.

For example, given:

        t   s1 => { say "I'm block '{label}'." }
        t __s2 => { say "I'm block '{label}'." }
        t  _s3 => { say "I'm block '{label}'." }

running `▸ tsel s\*` will output something like this:

        # Testing ⟨the file⟩:
           # s1
        I'm block 's1'.
           # _s3 : skipped

Note that even if a block is completely ignored by `tsel`, it must nevertheless compile correctly; if it doesn't, you have no choice but to fix, comment out, or remove the offending code.

Note also that skipped or ignored blocks will have their label, with their underscore prefix, displayed by the `-l` option if the label without the underscores matches the requested block label pattern. For example, given the same as above, running `▸ tsel -l s\*` will show something like this:

        # Labels in ⟨the file⟩:
        s1
        __s2
        _s3

Can I use different names for `t` and `label`?
----------------------------------------------

You may want to do that if for some reason `t` or `label` would cause a name collision in your code (or maybe you just don't like those names, eh). You can set the names you want instead at `use` time, by passing the wanted names as arguments. To use a different name for `t`, pass a single argument, the name you want. For example:

        use Test::Selector 'my-blocksub';

And you'd use it just like `t`:

        my-blocksub ⟨some-label⟩ => { … }

To use a different name for `label`, you need to pass two arguments: the first one is the desired new (or same) name for `t`, and the second, the new name for `label`. For example:

        use Test::Selector 't', 'my-labelsub';

Similarly, you'd use it just like `label`:

        t a42 => {
            say "My label is ", my-labelsub;
        }

Full Example
------------

Suppose you have the following (again, uninteresting) code in the `./t/baz.rakutest` file:

        use Test;

        plan 3;

        my $str = 'abc';
        ok(
            $str.chars == $str.flip.chars,
            "A reversed string is the same length as the original.",
        );

        my $baz = 'xYz';
        ok(
            $baz !~~ / \d /,
            "The string '$baz' contains no digits.",
        );
        ok(
            $baz.chars == 3,
            "The string '$baz' has three characters.",
        );

        say "Hi there!";

Running `▸ raku ⋯/t/baz.rakutest` would produce:

        1..3
        ok 1 - A reversed string is the same length as the original.
        ok 2 - The string 'xYz' contains no digits.
        ok 3 - The string 'xYz' has three characters.
        Hi there!

Now suppose you would like to sometimes run separately either the first test or the second and third together -- the latter share the `$baz` variable, so it makes sense. You could wrap those parts in labeled blocks and have `done-testing` instead of `plan 3`, since not all tests will always be run. For example:

        use Test;
        use Test::Selector;

        t s1 => {
            my $str = 'abc';
            ok(
                $str.chars == $str.flip.chars,
                "A reversed string is the same length as the original.",
            );
        }

        t s2 => {
            my $baz = 'xYz';
            ok(
                $baz !~~ / \d /,
                "The string '$baz' contains no digits.",
            );
            ok(
                $baz.chars == 3,
                "The string '$baz' has three characters.",
            );
        }

        say "Hi there!";

        done-testing;

To run all the blocks, and thus all the tests, run it as before, or invoke `tsel` without arguments from the module's root directory. In what follows, we will suppose that that the `baz.rakutest` file is the only file under `./t`:

        cd ⟨Module directory⟩
        tsel

That will print:

        # Testing baz.rakutest:
           # s1
        ok 1 - A reversed string is the same length as the original.
           # s2
        ok 2 - The string 'xYz' contains no digits.
        ok 3 - The string 'xYz' has three characters.
        Hi there!
        1..3

Notice a few things:

        . The name of the file being tested is output.
        . Output from a labeled block is preceded by a comment
          displaying that label.

Now let's ask `tsel` to run only the block labeled 's2':

        tsel s2

That prints:

        # Testing baz.rakutest:
           # s2
        ok 1 - The string 'xYz' contains no digits.
        ok 2 - The string 'xYz' has three characters.
        Hi there!
        1..2

Nice, but maybe we don't care about that "Hi there!" line or, for that matter, any other code that may appear in the file. To prevent such code from being run all the time, you could comment it out (it would never run at all then, eh) or place all such code in one or more labeled blocks, prefixed or not with underscores to make them ignored, it's up to you. For example:

        t dont-care => {
            say "Hi there!";
        }

With that, running `▸ tsel s1` would output:

        # Testing baz.rakutest:
           # s1
        ok 1 - A reversed string is the same length as the original.
        1..1

Two last examples. Running `▸ tsel -q` would then output:

        # Testing baz.rakutest:
           # s1
           # s2
        1..3

And `▸ tsel -qq`:

        1..3

Using a variable as a block label
---------------------------------

Suppose you're developing a function named `jub()`:

        sub jub ($c, Int $i) {
            return $c ~~ Int ?? $c * $i !! $c x $i;
        }

You could have the following test blocks:

        t a1 => {
            is( (my $result = jub('x', 4)), 'xxxx' );
            is( $result.chars, 4 );
        }
        t a2 => {
            is( (my $result = jub('ZZZ', 2)), 'ZZZZZZ' );
            is( $result.chars, 6 );
        }
        t a3 => {
            is( (my $result = jub(5, 3)), 15 );
            is( $result.chars, 2 );
        }

The (only) interesting thing here is that all the test blocks have the exact same structure, so we could refactor to this:

        use Test;
        use Test::Selector;

        sub jub ($c, Int $i) {
            return $c ~~ Int ?? $c * $i !! $c x $i;
        }

        sub check-jub (
            $block-id,
            $arg1,
            $arg2,
            $expected-result,
            $expected-length,
        ) {
            t "a$block-id" => {
                is( (my $result = jub($arg1, $arg2)), $expected-result );
                is( $result.chars, $expected-length );
            }
        }

        check-jub |< 1 x   4 xxxx   4 >;
        check-jub |< 2 ZZZ 2 ZZZZZZ 6 >;
        check-jub |< 3 5   3 15     2 >;

        done-testing;

So to run only the last one, you'd invoke `▸ tsel a3`.

Now the test data is much easier to prepare and if the structure of the test itself needs to be modified, it's going to be easy too.

AUTHOR
------

Luc St-Louis <lucs@pobox.com>

COPYRIGHT AND LICENSE
---------------------

Copyright © 2023 Luc St-Louis <lucs@pobox.com>

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.



