Categories
Posts in this category
- Current State of Exceptions in Rakudo and Perl 6
- Meet DBIish, a Perl 6 Database Interface
- doc.perl6.org and p6doc
- Exceptions Grant Report for May 2012
- Exceptions Grant Report -- Final update
- Perl 6 Hackathon in Oslo: Be Prepared!
- Localization for Exception Messages
- News in the Rakudo 2012.05 release
- News in the Rakudo 2012.06 release
- Perl 6 Hackathon in Oslo: Report From The First Day
- Perl 6 Hackathon in Oslo: Report From The Second Day
- Quo Vadis Perl?
- Rakudo Hack: Dynamic Export Lists
- SQLite support for DBIish
- Stop The Rewrites!
- Upcoming Perl 6 Hackathon in Oslo, Norway
- A small regex optimization for NQP and Rakudo
- Pattern Matching and Unpacking
- Rakudo's Abstract Syntax Tree
- The REPL trick
- First day at YAPC::Europe 2013 in Kiev
- YAPC Europe 2013 Day 2
- YAPC Europe 2013 Day 3
- A new Perl 6 community server - call for funding
- New Perl 6 community server now live, accepting signups
- A new Perl 6 community server - update
- All Perl 6 modules in a box
- doc.perl6.org: some stats, future directions
- Profiling Perl 6 code on IRC
- Why is it hard to write a compiler for Perl 6?
- Writing docs helps you take the user's perspective
- Perl 6 Advent Calendar 2016 -- Call for Authors
- Perl 6 By Example: Running Rakudo
- Perl 6 By Example: Formatting a Sudoku Puzzle
- Perl 6 By Example: Testing the Say Function
- Perl 6 By Example: Testing the Timestamp Converter
- Perl 6 By Example: Datetime Conversion for the Command Line
- What is Perl 6?
- Perl 6 By Example, Another Perl 6 Book
- Perl 6 By Example: Silent Cron, a Cron Wrapper
- Perl 6 By Example: Testing Silent Cron
- Perl 6 By Example: Stateful Silent Cron
- Perl 6 By Example: Perl 6 Review
- Perl 6 By Example: Parsing INI files
- Perl 6 By Example: Improved INI Parsing with Grammars
- Perl 6 By Example: Generating Good Parse Errors from a Parser
- Perl 6 By Example: A File and Directory Usage Graph
- Perl 6 By Example: Functional Refactorings for Directory Visualization Code
- Perl 6 By Example: A Unicode Search Tool
- What's a Variable, Exactly?
- Perl 6 By Example: Plotting using Matplotlib and Inline::Python
- Perl 6 By Example: Stacked Plots with Matplotlib
- Perl 6 By Example: Idiomatic Use of Inline::Python
- Perl 6 By Example: Now "Perl 6 Fundamentals"
- Perl 6 Books Landscape in June 2017
- Living on the (b)leading edge
- The Loss of Name and Orientation
- Perl 6 Fundamentals Now Available for Purchase
- My Ten Years of Perl 6
- Perl 6 Coding Contest 2019: Seeking Task Makers
- A shiny perl6.org site
- Creating an entry point for newcomers
- An offer for software developers: free IRC logging
- Sprixel, a 6 compiler powered by JavaScript
- Announcing try.rakudo.org, an interactive Perl 6 shell in your browser
- Another perl6.org iteration
- Blackjack and Perl 6
- Why I commit Crud to the Perl 6 Test Suite
- This Week's Contribution to Perl 6 Week 5: Implement Str.trans
- This Week's Contribution to Perl 6
- This Week's Contribution to Perl 6 Week 8: Implement $*ARGFILES for Rakudo
- This Week's Contribution to Perl 6 Week 6: Improve Book markup
- This Week's Contribution to Perl 6 Week 2: Fix up a test
- This Week's Contribution to Perl 6 Week 9: Implement Hash.pick for Rakudo
- This Week's Contribution to Perl 6 Week 11: Improve an error message for Hyper Operators
- This Week's Contribution to Perl 6 - Lottery Intermission
- This Week's Contribution to Perl 6 Week 3: Write supporting code for the MAIN sub
- This Week's Contribution to Perl 6 Week 1: A website for proto
- This Week's Contribution to Perl 6 Week 4: Implement :samecase for .subst
- This Week's Contribution to Perl 6 Week 10: Implement samespace for Rakudo
- This Week's Contribution to Perl 6 Week 7: Implement try.rakudo.org
- What is the "Cool" class in Perl 6?
- Report from the Perl 6 Hackathon in Copenhagen
- Custom operators in Rakudo
- A Perl 6 Date Module
- Defined Behaviour with Undefined Values
- Dissecting the "Starry obfu"
- The case for distributed version control systems
- Perl 6: Failing Softly with Unthrown Exceptions
- Perl 6 Compiler Feature Matrix
- The first Perl 6 module on CPAN
- A Foray into Perl 5 land
- Gabor: Keep going
- First Grant Report: Structured Error Messages
- Second Grant Report: Structured Error Messages
- Third Grant Report: Structured Error Messages
- Fourth Grant Report: Structured Error Messages
- Google Summer of Code Mentor Recap
- How core is core?
- How fast is Rakudo's "nom" branch?
- Building a Huffman Tree With Rakudo
- Immutable Sigils and Context
- Is Perl 6 really Perl?
- Mini-Challenge: Write Your Prisoner's Dilemma Strategy
- List.classify
- Longest Palindrome by Regex
- Perl 6: Lost in Wonderland
- Lots of momentum in the Perl 6 community
- Monetize Perl 6?
- Musings on Rakudo's spectest chart
- My first executable from Perl 6
- My first YAPC - YAPC::EU 2010 in Pisa
- Trying to implement new operators - failed
- Programming Languages Are Not Zero Sum
- Perl 6 notes from February 2011
- Notes from the YAPC::EU 2010 Rakudo hackathon
- Let's build an object
- Perl 6 is optimized for fun
- How to get a parse tree for a Perl 6 Program
- Pascal's Triangle in Perl 6
- Perl 6 in 2009
- Perl 6 in 2010
- Perl 6 in 2011 - A Retrospection
- Perl 6 ticket life cycle
- The Perl Survey and Perl 6
- The Perl 6 Advent Calendar
- Perl 6 Questions on Perlmonks
- Physical modeling with Math::Model and Perl 6
- How to Plot a Segment of a Circle with SVG
- Results from the Prisoner's Dilemma Challenge
- Protected Attributes Make No Sense
- Publicity for Perl 6
- PVC - Perl 6 Vocabulary Coach
- Fixing Rakudo Memory Leaks
- Rakudo architectural overview
- Rakudo Rocks
- Rakudo "star" announced
- My personal "I want a PONIE" wish list for Rakudo Star
- Rakudo's rough edges
- Rats and other pets
- The Real World Strikes Back - or why you shouldn't forbid stuff just because you think it's wrong
- Releasing Rakudo made easy
- Set Phasers to Stun!
- Starry Perl 6 obfu
- Recent Perl 6 Developments August 2008
- The State of Regex Modifiers in Rakudo
- Strings and Buffers
- Subroutines vs. Methods - Differences and Commonalities
- A SVG plotting adventure
- A Syntax Highlighter for Perl 6
- Test Suite Reorganization: How to move tests
- The Happiness of Design Convergence
- Thoughts on masak's Perl 6 Coding Contest
- The Three-Fold Function of the Smart Match Operator
- Perl 6 Tidings from September and October 2008
- Perl 6 Tidings for November 2008
- Perl 6 Tidings from December 2008
- Perl 6 Tidings from January 2009
- Perl 6 Tidings from February 2009
- Perl 6 Tidings from March 2009
- Perl 6 Tidings from April 2009
- Perl 6 Tidings from May 2009
- Perl 6 Tidings from May 2009 (second iteration)
- Perl 6 Tidings from June 2009
- Perl 6 Tidings from August 2009
- Perl 6 Tidings from October 2009
- Timeline for a syntax change in Perl 6
- Visualizing match trees
- Want to write shiny SVG graphics with Perl 6? Port Scruffy!
- We write a Perl 6 book for you
- When we reach 100% we did something wrong
- Where Rakudo Lives Now
- Why Rakudo needs NQP
- Why was the Perl 6 Advent Calendar such a Success?
- What you can write in Perl 6 today
- Why you don't need the Y combinator in Perl 6
- You are good enough!
Sun, 08 Jan 2017
Perl 6 By Example: Testing Silent Cron
Permanent link
This blog post is part of my ongoing project to write a book about Perl 6.
If you're interested, either in this book project or any other Perl 6 book news, please sign up for the mailing list at the bottom of the article, or here. It will be low volume (less than an email per month, on average).
The previous blog post left us with a bare-bones silent-cron implementation, but without tests. I probably sound like a broken record for bringing this up time and again, but I really want some tests when I start refactoring or extending my programs. And this time, getting the tests in is a bit harder, so I think it's worth discussing how to do it.
Refactoring
As a short reminder, this is what the program looks like:
#!/usr/bin/env perl6
sub MAIN(*@cmd, :$timeout) {
my $proc = Proc::Async.new(|@cmd);
my $collector = Channel.new;
for $proc.stdout, $proc.stderr -> $supply {
$supply.tap: { $collector.send($_) }
}
my $promise = $proc.start;
my $waitfor = $promise;
$waitfor = Promise.anyof(Promise.in($timeout), $promise)
if $timeout;
$ = await $waitfor;
$collector.close;
my $output = $collector.list.join;
if !$timeout || $promise.status ~~ Kept {
my $exitcode = $promise.result.exitcode;
if $exitcode != 0 {
say "Program @cmd[] exited with code $exitcode";
print "Output:\n", $output if $output;
}
exit $exitcode;
}
else {
$proc.kill;
say "Program @cmd[] did not finish after $timeout seconds";
sleep 1 if $promise.status ~~ Planned;
$proc.kill(9);
$ = await $promise;
exit 2;
}
}
There's logic in there for executing external programs with a timeout, and then there's logic for dealing with two possible outcomes. In terms of both testability and for future extensions it makes sense to factor out the execution of external programs into a subroutine. The result of this code is not a single value, we're potentially interested in the output it produced, the exit code, and whether it ran into a timeout. We could write a subroutine that returns a list or a hash of these values, but here I chose to write a small class instead:
class ExecutionResult {
has Int $.exitcode = -1;
has Str $.output is required;
has Bool $.timed-out = False;
method is-success {
!$.timed-out && $.exitcode == 0;
}
}
We've seen classes before, but this one has a few new features. Attributes
declared with the .
twigil automatically get an accessor method, so
has Int $.exitcode;
is roughly the same as
has Int $!exitcode;
method exitcode() { $!exitcode }
So it allows a user of the class to access the value in the attribute from the
outside. As a bonus, you can also initialize it from the standard constructor
as a named argument, ExecutionResult.new( exitcode => 42 )
. The exit code is
not a required attribute, because we can't know the exit code of a program
that has timed out. So with has Int $.exitcode = -1
we give it a default
value that applies if the attribute hasn't been initialized.
The output is a required attribute, so we mark it as such with is
required
. That's a trait. Traits are pieces of code that modify the behavior
of other things, here of an attribute. They crop up in several places, for
example in subroutine signatures (is copy
on a parameter), variable
declarations and classes. If you try to call ExecutionResult.new()
without
specifying an output
, you get such an error:
The attribute '$!output' is required, but you did not provide a value for it.
Mocking and Testing
Now that we have a convenient way to return more than one value from a hypothetical subroutine, let's look at what this subroutine might look like:
sub run-with-timeout(@cmd, :$timeout) {
my $proc = Proc::Async.new(|@cmd);
my $collector = Channel.new;
for $proc.stdout, $proc.stderr -> $supply {
$supply.tap: { $collector.send($_) }
}
my $promise = $proc.start;
my $waitfor = $promise;
$waitfor = Promise.anyof(Promise.in($timeout), $promise)
if $timeout;
$ = await $waitfor;
$collector.close;
my $output = $collector.list.join;
if !$timeout || $promise.status ~~ Kept {
say "No timeout";
return ExecutionResult.new(
:$output,
:exitcode($promise.result.exitcode),
);
}
else {
$proc.kill;
sleep 1 if $promise.status ~~ Planned;
$proc.kill(9);
$ = await $promise;
return ExecutionResult.new(
:$output,
:timed-out,
);
}
}
The usage of Proc::Async
has remained the same, but instead of printing this when an error occurs, the
routine now returns ExecutionResult
objects.
This simplifies the MAIN
sub quite a bit:
multi sub MAIN(*@cmd, :$timeout) {
my $result = run-with-timeout(@cmd, :$timeout);
unless $result.is-success {
say "Program @cmd[] ",
$result.timed-out ?? "ran into a timeout"
!! "exited with code $result.exitcode()";
print "Output:\n", $result.output if $result.output;
}
exit $result.exitcode // 2;
}
A new syntactic feature here is the ternary operator, CONDITION ??
TRUE-BRANCH !! FALSE-BRANCH
, which you might know from other programming
languages such as C or Perl 5 as CONDITION ? TRUE-BRANCH : FALSE-BRANCH
.
Finally, the logical defined-or operator LEFT // RIGHT
returns the LEFT
side if it's defined, and if not, runs the RIGHT
side and returns its
value. It works like the ||
and or
infix operators, except that those
check for the boolean value of the left, not whether they are defined.
In Perl 6, we distinguish between defined and true values. By
default, all instances are true and defined, and all type objects
are false and undefined.
Several built-in types override what they consider to be true. Numbers
that equal 0 evaluate to False
in a boolean context, as do
empty strings and empty containers such as arrays, hashes and sets.
On the other hand, only the built-in type Failure overrides definedness.
You can override the truth value of a custom type by implementing
a method Bool
(which should return True
or False
), and the
definedness with a method defined
.
Now we could start testing the sub run-with-timeout
by writing custom external
commands with defined characteristics (output, run time, exit code), but
that's rather fiddly to do so in a reliable, cross-platform way. So instead I
want to replace Proc::Async
with a mock implementation, and give the sub a
way to inject that:
sub run-with-timeout(@cmd, :$timeout, :$executer = Proc::Async) {
my $proc = $executer.defined ?? $executer !! $executer.new(|@cmd);
# rest as before
Looking through sub run-with-timeout
, we can make a quick list of methods
that the stub Proc::Async
implementation needs: stdout
, stderr
, start
and kill
. Both stdout
and stderr
need to return a
Supply. The simplest thing that could
possibly work is to return a Supply that will emit just a single value:
my class Mock::Proc::Async {
has $.out = '';
has $.err = '';
method stdout {
Supply.from-list($.out);
}
method stderr {
Supply.from-list($.err);
}
Supply.from-list returns a Supply that will emit all the arguments passed to it; in this case just a single string.
The simplest possible implementation of kill
just does nothing:
method kill($?) {}
$?
in a signature is an optional argument ($foo?
) without a name.
Only one method remains that needs to be stubbed: start
. It's supposed to
return a Promise that, after a defined number of seconds, returns a Proc
object or a mock thereof. Since the code only calls the exitcode
method on
it, writing a stub for it is easy:
has $.exitcode = 0;
has $.execution-time = 1;
method start {
Promise.in($.execution-time).then({
(class {
has $.exitcode;
}).new(:$.exitcode);
});
}
Since we don't need the class for the mock Proc
anywhere else, we don't even
need to give it a name. class { ... }
creates an anonymous class, and the
.new
call on it creates a new object from it.
As mentioned before, a Proc
with a non-zero exit code throws an exception
when evaluated in void context, or sink context as we call it in Perl 6. We
can emulate this behavior by extending the anonymous class a bit:
class {
has $.exitcode;
method sink() {
die "mock Proc used in sink context";
}
}
With all this preparation in place, we can finally write some tests:
multi sub MAIN('test') {
use Test;
my class Mock::Proc::Async {
has $.exitcode = 0;
has $.execution-time = 0;
has $.out = '';
has $.err = '';
method kill($?) {}
method stdout {
Supply.from-list($.out);
}
method stderr {
Supply.from-list($.err);
}
method start {
Promise.in($.execution-time).then({
(class {
has $.exitcode;
method sink() {
die "mock Proc used in sink context";
}
}).new(:$.exitcode);
});
}
}
# no timeout, success
my $result = run-with-timeout([],
timeout => 2,
executer => Mock::Proc::Async.new(
out => 'mocked output',
),
);
isa-ok $result, ExecutionResult;
is $result.exitcode, 0, 'exit code';
is $result.output, 'mocked output', 'output';
ok $result.is-success, 'success';
# timeout
$result = run-with-timeout([],
timeout => 0.1,
executer => Mock::Proc::Async.new(
execution-time => 1,
out => 'mocked output',
),
);
isa-ok $result, ExecutionResult;
is $result.output, 'mocked output', 'output';
ok $result.timed-out, 'timeout reported';
nok $result.is-success, 'success';
}
This runs through two scenarios, one where a timeout is configured but not used (because the mocked external program exits first), and one where the timeout takes effect.
Improving Reliability and Timing
Relying on timing in tests is always unattractive. If the times are too short (or too slow together), you risk sporadic test failures on slow or heavily loaded machines. If you use more conservative temporal spacing of tests, the tests can become very slow.
There's a module (not distributed with Rakudo) to alleviate this pain: Test::Scheduler provides a thread scheduler with virtualized time, allowing you to write the tests like this:
use Test::Scheduler;
my $*SCHEDULER = Test::Scheduler.new;
my $result = start run-with-timeout([],
timeout => 5,
executer => Mock::Proc::Async.new(
execution-time => 2,
out => 'mocked output',
),
);
$*SCHEDULER.advance-by(5);
$result = $result.result;
isa-ok $result, ExecutionResult;
# more tests here
This installs the custom scheduler, and $*SCHEDULER.advance-by(5)
instructs
it to advance the virtual time by 5 seconds, without having to wait five
actual seconds. At the time of writing (December 2016), Test::Scheduler
is
rather new module, and has a bug that prevents the second test case from
working this way.
Installing a Module
If you want to try out Test::Scheduler
, you need to install it first. If you
run Rakudo Star, it has already provided you with the panda
module installer. You can
use that to download and install the module for you:
$ panda install Test::Scheduler
If you don't have panda available, you can instead bootstrap zef
, an
alternative module installer:
$ git clone https://github.com/ugexe/zef.git
$ cd zef
$ perl6 -Ilib bin/zef install .
and then use zef
to install the module:
$ zef install Test::Scheduler
Summary
In this installment, we've seen attributes with accessors, the ternary
operator and anonymous classes. Testing of threaded code has been discussed,
and how a third-party module can help. Finally we had a very small glimpse
at the two module installers, panda
and zef
.