This talk describes refactoring FindBin::libs from Perl5 to Raku: breaking the module up into functional pieces, writing the tests using Raku, testing and releasing the module with mi6.
2. FindBin::libs is a programatic “-I”
FindBin
Scan up the tree.
Use non-root ./lib dirs.
3. Why?
Test or execute project without installing.
In-house modules without CPAN or zef.
Tests use ./t/lib or ./t/etc
Avoid destructive tests in production.
Resolve path.
Use modules relative to ‘real’ path.
4. Describing Raku logic
Not Perl.
Coding & testing utilities.
Command line tools.
zef Installer.
Releasing with zef.
5. First Step: FindBin
You are here ---> *
Interactive vs. program.
Absolute path.
As-is vs. resolved.
Native filesystem.
&Bin &Script
7. “unit”
Raku uses “compilation units”.
This unit is a single module.
use v6.d;
unit module FindBin:ver<0.4.0>:auth<CPAN:lembark>;
8. “unit”
Raku uses “compilation units”.
Raku supports multiple versions & authors.
The version & author are part of the definition.
use v6.d;
unit module FindBin:ver<0.4.0>:auth<CPAN:lembark>;
10. Constant’s don’t change
Neither does your interactive status:
constant IS‑INTERACTIVE
= $*PROGRAM‑NAME eq '-e'| '-' | 'interactive'
;
11. Twigil
`tween a sigil and a name.
‘*’ is for dynamic.
AUTOGENERATED
constant IS‑INTERACTIVE
= $*PROGRAM‑NAME eq '-e'| '-' | 'interactive'
;
12. Module args
‘:’ prefix is part of the language:
use FindBin; # :DEFAULT
use FindBin :resolve;
use FindBin :verbose;
use FindBin :Bin;
use FindBin :Script :resolve;
13. Module args
‘:’ prefix is part of the language:
use FindBin; # :DEFAULT
use FindBin :resolve;
use FindBin :verbose;
use FindBin :Bin;
use FindBin :Script :resolve;
constant _FindBin_RESOLVE-DEF is export( :resolve ) = True;
constant _FindBin_VERBOSE-DEF is export( :verbose ) = True;
14. Slip
|( ) converts a list into a Slip.
No context: Raku lists don't flatten automatically.
Slips do.
Good examples at doc.raku.org (see references).
constant OPTION-TAGS = |( :resolve , :verbose );
16. Signature
An object.
Defines input, output.
sub Bin
(
Bool() :$resolve = CALLER::LEXICAL::_FindBin_RESOLVE-DEF,
Bool() :$verbose = CALLER::LEXICAL::_FindBin_VERBOSE-DEF
--> IO
)
17. Signature
:$resolve stores “:resolve” or “:!resolve” argument.
Defaults to caller’s lexical.
sub Bin
(
Bool() :$resolve = CALLER::LEXICAL::_FindBin_RESOLVE-DEF,
Bool() :$verbose = CALLER::LEXICAL::_FindBin_VERBOSE-DEF
--> IO
)
18. Signature
Bool() coerces the values to type Bool.
Avoids undef.
sub Bin
(
Bool() :$resolve = CALLER::LEXICAL::_FindBin_RESOLVE-DEF,
Bool() :$verbose = CALLER::LEXICAL::_FindBin_VERBOSE-DEF
--> IO
)
19. export
Explicit “:Bin”, no args.
Either of “:verbose” or “:resolve” via flattening.
sub Bin
(
Bool() :$resolve = CALLER::LEXICAL::_FindBin_RESOLVE-DEF,
Bool() :$verbose = CALLER::LEXICAL::_FindBin_VERBOSE-DEF
--> IO
)
is export( :Bin, :DEFAULT, OPTION-TAGS )
20. Using a constant
No sigil.
“?? !!” Ternary.
my $bin_from
= IS-INTERACTIVE
?? $*CWD
!! $*PROGRAM.IO
;
21. Using a constant
$*CWD is an IO. my $bin_from
= IS-INTERACTIVE
?? $*CWD
!! $*PROGRAM.IO
;
22. Using a constant
$*PROGRAM is Str.
.IO calls constructor:
Converts the string to
an IO object.
my $bin_from
= IS-INTERACTIVE
?? $*CWD
!! $*PROGRAM.IO
;
23. Using an IO
.resolve --> IO
.absolute --> Str.
my $path
= $resolve
?? $bin_from.resolve
!! $bin_from.absolute.IO
;
24. Returning a directory
dirname works on
*NIX, fails on
MS, VMS.
Parent includes
the volume.
IS-INTERACTIVE
?? $path
!! $path.parent
25. Bin
sub Bin
(
Bool() :$resolve = CALLER::LEXICAL::_FindBin_RESOLVE-DEF,
Bool() :$verbose = CALLER::LEXICAL::_FindBin_VERBOSE-DEF
--> IO
)
is export( :Bin, :DEFAULT, OPTION-TAGS )
{
my $bin_from
= IS-INTERACTIVE
?? $*CWD
!! $*PROGRAM.IO
;
my $path
= $resolve
?? $bin_from.resolve
!! $bin_from.absolute.IO
;
if $verbose { ... }
IS-INTERACTIVE
?? $path
!! $path.parent
}
Viola!
31. Generic sanity test
use v6.d;
use Test;
use lib $*PROGRAM.IO.parent(2).add( 'lib' );
( $*PROGRAM-NAME.IO.basename ~~ m{^ (d+ '-')? (.+) '.'t $} )
or bail-out 'Unable to parse test name: ' ~ $*PROGRAM-NAME;
my $madness = $1.subst( '-', '::', :g );
use-ok $madness;
my $found = ::( $madness ).^name;
is $found, $madness, "Package '$found' ($madness).";
done-testing;
32. Checking exports
Pseudo-class MY:: with hardwired name.
use v6.d;
use Test;
ok ! MY::<&Bin> , 'Bin not installed before use.';
ok ! MY::<&Script> , 'Script not installed before use.';
ok ! MY::<_FindBin_RESOLVE-DEF>, '! _FindBin_RESOLVE-DEF’;
ok ! MY::<_FindBin_VERBOSE-DEF>, '! _FindBin_VERBOSE-DEF’;
33. Checking exports
do
{
use FindBin;
ok MY::<&Bin> , 'Bin installed by default.';
ok MY::<&Script> , 'Script installed by default.';
ok ! MY::<_FindBin_RESOLVE-DEF>, '! _FindBin_RESOLVE-DEF';
ok ! MY::<_FindBin_VERBOSE-DEF>, '! _FindBin_VERBOSE-DEF';
note '# Bin is: ' ~ Bin;
note '# Script is: ' ~ Script;
};
34. Checking exports
do
{
use FindBin;
...
};
ok ! MY::<&Bin> , 'Bin is lexicaly scoped.';
ok ! MY::<&Script> , 'Script is lexicaly scoped.';
ok ! MY::<_FindBin_RESOLVE-DEF>, '! _FindBin_RESOLVE-DEF';
ok ! MY::<_FindBin_VERBOSE-DEF>, '! _FindBin_RESOLVE-DEF';
35. Script test program is easy
use v6.d;
use Test;
use lib $*PROGRAM.IO.parent(2).add( 'lib' );
use FindBin;
my $found = Script;
my $expect = $*PROGRAM.basename;
ok $found, 'Script returns ' ~ $found;
ok $expect, '$*PROGRAM.basename is ' ~ $expect;
is $found, $expect, 'Script matches basename.';
done-testing;
36. Testing -e takes more syntax
use v6.d;
use Test;
my $expect = '-e';
my ( $found )
= qx{ raku -I./lib -M'FindBin' -e 'say Script' }.chomp;
is $found, $expect, "'raku -e' returns '$found' ($expect)";
done-testing;
37. Now you got all of the tests...
How do you run them?
prove6; what you are used to
mi6 test; command line assistants
assixt test;
38. Releasing a module
“mi6 release;”
Changes entry
META6.json
Content pushed.
Assign a version tag.
Upload to CPAN.
39. Getting mi6
$ zef install App::Mi6
===> Searching for: App::Mi6
===> Testing: App::Mi6:ver<0.2.6>:auth<cpan:SKAJI>
===> Testing [OK] for App::Mi6:ver<0.2.6>:auth<cpan:SKAJI>
===> Installing: App::Mi6:ver<0.2.6>:auth<cpan:SKAJI>
1 bin/ script [mi6] installed to:
/opt/raku/share/raku/site/bin
Note the install dir.
Symlinks are your friends...
40. Changes
Revision history for FindBin
{{$NEXT}}
- Return IO for simplicity
0.3.4 2019-05-25T23:02:06-05:00
- use generic 01 test for use/require.
- POD, whitespace, comments.
0.3.3 2019-05-20T11:42:45-05:00
...
42. POD is still largely POD
=begin pod
=head1 SYNOPSIS
# export Bin() and Script() by default.
use FindBin;
my $dir_from = Bin(); # $dir_from is an IO object
my $script_base = Script(); # $script_base is a string.
# explicitly export only Bin() or Script().
use FindBin :Bin;
43. Releasing a module
$ mi6 release
==> Release distribution to CPAN
There are 12 steps: # Rakuholics Anonymous???
<snip>
==> Step 1. CheckChanges
==> Step 2. CheckOrigin
==> Step 3. CheckUntrackedFiles
==> Step 4. BumpVersion
Next release version? [0.3.5]: 0.4.0
==> Step 5. RegenerateFiles
==> Step 6. DistTest
<snip>
==> Step 8. UploadToCPAN
Are you sure you want to upload FindBin-0.4.0.tar.gz to CPAN? (y/N) y
44. Releasing a module
Housekeeping includes version tag.
==> Step 9. RewriteChanges
==> Step10. GitCommit
[master 6b2a3e2] 0.4.0
4 files changed, 5 insertions(+), 3 deletions(-)
<snip
To gitlab.com:lembark/Raku-FindBin.git
607929e..6b2a3e2 master -> master
==> Step11. CreateGitTag
Total 0 (delta 0), reused 0 (delta 0)
To gitlab.com:lembark/Raku-FindBin.git
* [new tag] 0.4.0 -> 0.4.0
==> Step12. CleanDist
45. Instant gratification
zef can install from a directory.
$ zef install . ;
===> Testing: FindBin:ver<0.4.0>:auth<CPAN:lembark>
# Madness: 'FindBin' (01-)
# FindBin
# Bin is: /sandbox/lembark/Modules/Raku/FindBin/t
<snip>
===> Testing [OK] for FindBin:ver<0.4.0>:auth<CPAN:lembark>
===> Installing: FindBin:ver<0.4.0>:auth<CPAN:lembark>
46. Bin is the root of all evil...
Next Step: Root out the rest of it.
Originally part of FindBin::libs:
Scan up directory tree.
Append relative paths.
Filter them.
48. Even more than more than one way to do it
multi scan-up
(
Stringy :$filter = 'all', *%argz
--> Seq
)
{
state %filterz =
(
all => True ,
dir => :d ,
file => :f ,
exist => :e ,
);
my $value = %filterz{ $filter }
or fail "Unknown: '$filter'. not "~ %filterz.keys.join(' ');
samewith filter => $value, |%argz
}
49. Single named arg “:filter”
multi scan-up
(
Stringy :$filter = 'all', *%argz
--> Seq
)
{
state %filterz =
(
all => True ,
dir => :d ,
file => :f ,
exist => :e ,
);
my $value = %filterz{ $filter }
or fail "Unknown: '$filter'. not "~ %filterz.keys.join(' ');
samewith filter => $value, |%argz
}
50. *%argz slurps remaining args
multi scan-up
(
Stringy :$filter = 'all', *%argz
--> Seq
)
{
stage %filterz =
(
all => True ,
dir => :d ,
file => :f ,
exist => :e ,
);
my $value = %filterz{ $filter }
or fail "Unknown: '$filter'. not "~ %filterz.keys.join(' ');
samewith filter => $value, |%argz
}
51. |%argz flattens them back out
multi scan-up
(
Stringy :$filter = 'all', *%argz
--> Seq
)
{
state %filterz =
(
all => True ,
dir => :d ,
file => :f ,
exist => :e ,
);
my $value = %filterz{ $filter }
or fail "Unknown: '$filter'. not "~ %filterz.keys.join(' ');
samewith filter => $value, |%argz
}
52. same name, new filter
multi scan-up
(
Stringy :$filter = 'all', *%argz
--> Seq
)
{
state %filterz =
(
all => True ,
dir => :d ,
file => :f ,
exist => :e ,
);
my $value = %filterz{ $filter }
or fail "Unknown: '$filter'. not "~ %filterz.keys.join(' ');
samewith filter => $value, |%argz
}
53. The other way to do it
multi scan-up
(
Any :$filter,
Str :$append = '',
Bool() :$resolve = False,
Bool() :$verbose = False,
Bool() :$skip-root = False,
IO() :$from = Bin( :$resolve, :$verbose )
--> Seq
)
is export( :DEFAULT )
$filter here is anything.
54. Args are defined in the parameters.
multi scan-up
(
Any :$filter,
Str :$append = '',
Bool() :$resolve = False,
Bool() :$verbose = False,
Bool() :$skip-root = False,
IO() :$from = Bin( :$resolve, :$verbose )
--> Seq
)
is export( :DEFAULT )
Re-cycled from Stringy multi via %args.
55. Telling them apart
multi Foo ( Stringy $filter = ‘all’, … )
multi Foo ( Any $filter, … )
Foo() Pick default no viable alternative.
Foo(‘X’) Pick Stringy derived from Any.
Foo( blah ) Pick Any.
56. Pick a filter, Any filter...
Perl5 used a code block, passed as a closure.
$filter->( $path ) or next...
57. Pick a filter, Any filter...
Smart matches are better way.
Pass in a file test, regex, block...
$path ~~ $filter or next;
58. Pick a filter, Any filter...
Smart matches are better way.
Pass in a file test, regex, block...
$path ~~ $filter or next;
Anything but a string.
Which was used up in the other multi.
59. Finding your path
absolute, resolve, parent should be familiar by now...
my $start =
(
$resolve
?? $from.resolve( completely => True )
!! $from
).absolute.IO;
my $path
= $start ~~ :d
?? $start
!! $start.parent
;
60. Finding your path
Fail if the path is not resolvable.
my $start =
(
$resolve
?? $from.resolve( completely => True )
!! $from
).absolute.IO;
my $path
= $start ~~ :d
?? $start
!! $start.parent
;
61. Gathering for a loop
return gather loop
{
…
take … if … ;
…
last if … ;
}
gather returns a Seq from take.
loop runs until a break.
Replaces for(;;){ ... push @result... }
62. Gathering for a loop
return gather loop
{
…
take … if … ;
…
last if … ;
}
gather returns a Seq from take.
loop runs until a break.
Replaces for(;;){ ... push @result... }
63. Gathering for a loop
return gather loop
{
…
take … if … ;
…
last if … ;
}
gather returns a Seq from take.
loop runs until a break.
Replaces for(;;){ ... push @result... }
64. Find your roots
return gather loop
{
my $next = $path.parent;
my $at-root = $path ~~ $next;
...
$path ~~ $filter
and take $path;
...
last if $at-root;
$path = $next;
}
65. What to take
if( $skip-root && $at-root )
{
say “Root: ‘$path’ (skip)” if $verbose;
}
else
{
my $take
= $append
?? $path.add( $append ) # add a relative path.
!! $path
;
$take ~~ $filter # $filter controls the match.
and take $take;
}
66. return gather loop
{
my $next = $path.parent;
my $at-root = $path ~~ $next;
if $at-root && $skip-root
{
note ‘Skip root’ if $verbose;
}
else
{
$take = $append ?? ... ;
$take ~~ $filter
and take $take;
}
$at-root
and last;
$path = $next;
}
Skip root before append.
Supply path to $filter.
Exit at root.
Working logic
74. Testing lazyness
One test: Compare it to an array.
my @found = scan-up.eager;
for ( 0 ..^ +@found ) -> $i { … }
Another is smart-match it with a seq:
my $found = scan-up;
ok $found ~~ $expect, ‘Found it.’;
75. Compare Bin dir with a file
Path and Bin results should match.
my $pass0 = scan-up;
ok $pass0, "scan-up = $pass0";
my $pass1 = scan-up :from( $*PROGRAM-NAME );
ok $pass1, "scan-up :from( $*PROGRAM-NAME ) = $pass1";
ok $pass0 ~~ $pass1, 'Bin matches $*Program-Name';
76. File and dir path work
$ Raku t/03-no-subdir.rakutest
ok 1 - scan-up = /sandbox/lembark/Modules/Raku/FileSystem-
Parent/t /sandbox/lembark/Modules/Raku/FileSystem-Parent
/sandbox/lembark/Modules/Raku /sandbox/lembark/Modules
/sandbox/lembark /sandbox /
ok 2 - scan-up :from( t/03-no-subdir.t ) =
/sandbox/lembark/Modules/Raku/FileSystem-Parent/t
/sandbox/lembark/Modules/Raku/FileSystem-Parent
/sandbox/lembark/Modules/Raku /sandbox/lembark/Modules
/sandbox/lembark /sandbox /
ok 3 - Bin and $*Program-Name return same list.
1..3
77. Adding a lib
:$append with variable name.
my $append = 'lib';
my $pass0 = scan-up :$append;
ok $pass0, 'scan-up $append';
my $pass1 = scan-up :$append, from => $*PROGRAM-NAME;
ok $pass1, 'scan-up $append :from => $*PROGRAM-NAME';
ok $pass0.eager ~~ $pass1.eager, ‘They match’;
78. Testing :$append
ok 1 - scan-up $append
ok 2 - scan-up $append :from => $*PROGRAM-NAME
# Pass0:/sandbox/lembark/Modules/Raku/FileSystem-Parent/t/lib
/sandbox/lembark/Modules/Raku/FileSystem-Parent/lib
/sandbox/lembark/Modules/Raku/lib
/sandbox/lembark/Modules/lib /sandbox/lembark/lib /sandbox/lib
/lib
# Pass1:/sandbox/lembark/Modules/Raku/FileSystem-Parent/t/lib
/sandbox/lembark/Modules/Raku/FileSystem-Parent/lib
/sandbox/lembark/Modules/Raku/lib
/sandbox/lembark/Modules/lib /sandbox/lembark/lib /sandbox/lib
/lib
ok 3 - Bin and $*Program-Name return same list.
79. Test verbose overrides
There is, of course, more than one way.
Shouldn’t affect Bin’s return value.
use FindBin :verbose;
my $false_1 = Bin( verbose => False ).Str;
my $false_2 = Bin( :verbose( False ) ).Str;
my $false_3 = Bin( :!verbose ).Str;
is $false_1, $false_2, "Match with false.";
is $false_2, $false_3, "Match with false.";
82. “EXPORT” does the deed
Outside of the package.
Handles positionals passed via use.
use v6.d;
sub EXPORT
(
*@args –-> Map
)
{
my $resolve = ? @args.first( 'resolve' );
my $verbose = ? @args.first( 'verbose' );
# do things...
}
unit module FindBin::libs:ver<0.0.1>:auth<CPAN:lembark>
# ain’t nobody here but us chickens
83. “EXPORT” does the deed
Outside of the package.
Handles positionals passed via use.
use v6.d;
sub EXPORT
(
*@args –-> Map
)
{
my $resolve = ? @args.first( 'resolve' );
my $verbose = ? @args.first( 'verbose' );
# do things...
}
unit module FindBin::libs:ver<0.0.1>:auth<CPAN:lembark>
# ain’t nobody here at all
84. Raku Colon Cleanser
Ever loose track of parens in an expression?
Raku method calls can replace
$foo.some_method( $bar );
with
$foo.some_method: $bar;
85. Raku Colon Cleanser
Ever loose track of parens in an expression?
Convert:
$foo.frob($bar.nicate($thing.new($blort)));
into:
$foo.frob: $bar.nicate: $thing.new: $blort
86. Raku Colon Cleanser
Ever loose track of parens in an expression?
$foo.frob:
$bar.nicate:
$thing.new: $blort
;
93. Named Parameters & Variables
Egads! Using “:$prefix” ties your code to an API!
94. Named Parameters & Variables
Egads! Using “:$prefix” ties your code to my API!
Signatures allow arbitrary variable names.
:$prefix is a shortcut for
sub foo( :prefix( $prefix ) )
You can also have:
sub foo ( :prefix( $dir_pref ) )
Change internal variable names.
95. Named Parameters & Variables
Egads! Using “:$prefix” ties your code to my API!
Signatures allow arbitrary variable names.
:$prefix is a shortcut for
sub foo( :prefix( $prefix ) )
You can also have:
sub foo ( :add( $prefix ) )
Change external parameter name.
96. --> Map
sub EXPORT
(
*@args --> Map
)
{
# return a Map
%( '@FindBin-libs-dirs' => @found )
}
Map of ( Name => Value )
97. --> Map
Map of ( Name => Value )
‘@’ is part of the name.
sub EXPORT
(
*@args --> Map
)
{
# return a Map
%( '@FindBin-libs-dirs' => @found )
}
98. sub EXPORT
(
*@args --> Map
)
{
use FileSystem::Parent;
my $resolve = ? @_.first( 'resolve' );
my $verbose = ? @_.first( 'verbose' );
my %fixed = ( ... );
my @found
= scan-up( :$verbose, :$resolve, |%fixed );
for @found.reverse -> $prefix
{
CompUnit::RepositoryRegistry.use-repository:
CompUnit::Repository::FileSystem.new:
:$prefix
}
%( '@FindBin-libs-dirs' => @found )
}
Exporter
99. “unit” is a placeholder.
Export does all the work.
unit module FindBin::libs has no subs.
100. Pass1: Check for installed dirs.
EXPORT installs the variable.
Flatten it for printing.
use FindBin::libs;
ok @FindBin-libs-dirs, 'Exported FindBin-libs-dirs';
note join( "n#t", '# libs:', |@FindBin-libs-dirs );
done-testing;
101. Pass2: Validate directory order.
Frobnicate.rakumod in ./t/lib/FindBin & ./lib/FindBin.
“FrobPath” will be ‘t/lib/...’ or ‘lib/...’.
unit module FindBin::Frobnicate:ver<0.1.1>:auth<CPAN:lembark>;
constant FrobPath is export( :DEFAULT )
= 't/lib/FindBin/Frobnicate.pm6';
constant FrobPath is export( :DEFAULT )
= 'lib/FindBin/Frobnicate.pm6';
102. Pass2: Validate directory order.
Test in ./t should prefer t/lib.
use FindBin::libs;
use FindBin::Frobnicate;
ok MY::<FrobPath>, 'FrobPath exported.';
my $expect = 't/lib/FindBin/Frobnicate.pm6';
my $found := MY::FrobPath;
is $found, $expect, "Found: ‘$found’”;
103. prove6
Looks familiar.
$ prove6 -v t/03-use-frobnicate.t;
ok 1 - FrobPath exported.
ok 2 - Found 't/lib/FindBin/Frobnicate.pm6'
1..2
t/03-use-frobnicate.t .. ok
All tests successful.
Files=1, Tests=2, 0 wallclock secs
Result: PASS
104. Summary
Raku is quite doable.
mi6 & assixt make it manageable.
Raku has gotten much better.
Raku is quite doable.
mi6 & assixt make it more manageable.
The doc’s website has gotten much better:
Raku
Try it... you’ll like it.