NAME
Test::Log::Abstraction - Capture log output in tests and assert on it
VERSION
0.002.0
SYNOPSIS
Check what your code logged
use Test::Most;
use Test::Log::Abstraction;
# Give the test logger to the code that you are testing
my $logger = Test::Log::Abstraction->new();
my $obj = Some::Class->new(logger => $logger);
$obj->do_something();
# Each of these is one TAP test
$logger->like(qr/updated/, 'do_something() logs that it updated');
$logger->has_level('error', 'an error was logged');
$logger->unlike(qr/fatal/, 'nothing fatal was logged');
is($logger->count(), 3, 'three messages were logged');
done_testing();
Check that nothing was logged
my $logger = Test::Log::Abstraction->new();
Some::Class->new(logger => $logger)->run();
$logger->empty('a normal run logs nothing');
Test several steps with one logger
my $logger = Test::Log::Abstraction->new();
my $obj = Some::Class->new(logger => $logger);
$obj->load('good.csv');
$logger->empty('good file: no messages');
$logger->clear(); # forget the messages from the first step
$obj->load('bad.csv');
$logger->has_level('warn', 'bad file: a warning');
Look at the messages yourself
foreach my $entry (@{ $logger->messages() }) {
print "$entry->{level}: $entry->{message}\n";
}
# Structured fields, from a call such as
# $logger->info('user logged in', { user => 'alice' })
is($logger->messages()->[0]->{fields}->{user}, 'alice', 'user field');
Control what is printed while the test runs
# Print nothing (the messages are still captured)
my $quiet = Test::Log::Abstraction->new(diag => 'none');
# Print everything
my $loud = Test::Log::Abstraction->new(verbose => 1);
# Print only errors and more serious messages
my $errors = Test::Log::Abstraction->new(diag => 'error');
Test code that checks the log level
# The code under test does: if($logger->is_debug()) { ... }
my $logger = Test::Log::Abstraction->new(level => 'warning');
ok(!$logger->is_debug(), 'debug output is turned off');
Get this module's own messages in another language
my $logger = Test::Log::Abstraction->new(lang => 'de'); # German
my $french = Test::Log::Abstraction->new(country => 'FR'); # French
DESCRIPTION
What this module is
Some code writes log messages through a logger object. In production that object is usually a Log::Abstraction logger. In a test, you give the code a Test::Log::Abstraction object instead.
This object does not write the messages to a file. It keeps them in a list in memory. After the code has run, your test can check the list: was a message logged, at which level, and what did it say?
It never writes to disk, and it does not load any logging backend.
Which methods it has
Log levels. The level methods of Log::Abstraction:
trace,debug,info,notice,warn,error,fatal,critical,alertandemergency. It also accepts the syslog nameswarning,err,crit,emerg,panicandinformational.Other logger methods.
level(),is_debug()and the otheris_<level>()methods,messages()andflush(). Code under test may call these, so they work as they do in Log::Abstraction.Test methods.
like,unlike,has_levelandempty. Each one reports one test result, likeok()in Test::More.Helper methods.
count,clear,verboseandlang.
Which messages are printed
Every message is always stored. This section is only about which messages are also printed in the test output, as TAP comments (lines that start with #).
By default, warning and more serious levels are printed. So if a test causes a warning by accident, you see it. trace, debug, info and notice messages are not printed.
Verbose mode prints every message. Verbose mode is on when you pass verbose => 1 to new(). If you do not pass verbose, it is on when the environment variable TEST_VERBOSE is true (prove -v sets it), or when VERBOSE is true.
To choose the levels, use the diag option of new():
'all'- print every message.'none'- print nothing (verbose mode still prints everything).A level name, such as
'error'- print that level and every more serious level.A list, such as
['info', 'error']- print only these levels.
Put exactly: a message is printed when any one of these is true, and not otherwise:
- 1. Verbose mode is on.
- 2.
diagis'all'. - 3.
diagis a list, and the message's level name is in it. - 4.
diagis a level name, the message's level is a known level, and its number is the same as, or lower than, that level's number.
So a misspelt level (caught by "AUTOLOAD") is printed only by rules 1 to 3; and 'none' or an empty list means only rule 1 can apply.
When a test method fails, the messages that explain the failure are printed under it. So you can see why it failed without running the test again.
Global variables are left alone
No method changes $@, $! or $_, and none of them touches an alarm() timer. So you can log, or test the log, inside an error handler, and $@ still holds the error afterwards. (A method that stops with an error does set $@, as every Perl error does.)
Levels and how serious they are
Each level has a number. A lower number means a more serious message. These are the syslog numbers.
0 emergency, emerg, panic
1 alert
2 critical, crit, fatal
3 error, err
4 warning, warn
5 notice
6 info, informational
7 debug, trace
Language of this module's messages
This module has its own messages: error messages, warnings, and the text that explains a failed test. They can be in English (en), German (de), French (fr) or Simplified Chinese (zh).
The language is chosen like this. The first rule that gives an answer is used:
- 1. The
langoption, for examplelang => 'de'. - 2. The
countryoption, a two-letter country code such as'FR'. - 3. Only when
lang => 'auto': the environment variablesLC_ALL,LC_MESSAGESandLANG, in that order. - 4.
$Test::Log::Abstraction::config{lang}, which is'en'.
The default is English, not the language of your computer. This is on purpose: the test output is then the same on every computer.
If a message has no translation, the English message is used. You can add or change messages with the i18n option of new().
This only changes this module's own messages. The messages that your code logs are never changed.
Default settings
The defaults for new() are in the hash %Test::Log::Abstraction::config. It has the same keys as the options of new(). You can change it in a test, or fill it with Object::Configure:
$Test::Log::Abstraction::config{'diag'} = 'none';
A change only affects loggers that are created after it.
You can also pass the result of Object::Configure straight to new(). Settings in environment variables named Test__Log__Abstraction__KEY then override the arguments; the extra keys that Object::Configure adds (such as its own logger) are ignored:
# In the shell: export Test__Log__Abstraction__diag=none
my $params = Object::Configure::configure('Test::Log::Abstraction', { level => 'error' });
my $logger = Test::Log::Abstraction->new($params);
COMMON PITFALLS
The test methods are tests.
like,unlike,has_levelandemptyeach add one test to the TAP output. If you give a test plan (tests => 5), count them. Or usedone_testing().Messages from earlier steps are still there. A logger keeps every message until you call
clear(). If one logger is used for several steps,likemay match a message from an earlier step.A string pattern is a regular expression.
like('a.c')matches'abc', because.means "any character". To match the text exactly, useqr/\Qa.c\E/.Different names for one level are counted apart.
warnandwarninghave the same number, butcount('warn')andhas_level('warn')do not see messages logged withwarning(). Check the name that the code under test uses.fatalis stored asfatal, but Log::Abstraction stores it aserror.undefis not the same as "no value".An
undefargument to a level method becomes the textundefin the message. (Log::Abstraction drops it.)verbose => undefturns verbose mode off. To use the environment variables instead, do not passverboseat all.diag => undefandlevel => undefuse the default from%config.count(undef)counts all messages.level(undef)returns the level number and changes nothing.like(undef),unlike(undef)andhas_level(undef)stop the test with an error.
One hash reference, or a hash reference at the end.
$logger->info({ a => 1 })logs the text{a => 1}. But$logger->info('text', { a => 1 })logs the texttextand stores{ a => 1 }infields. An empty hash at the end is dropped.Copies are shallow (only one level deep).
When a message has
fields, the fields hash is copied. But a hash or array inside the fields is not copied. If your code changes it later, the stored message changes too. And if the fields contain the logger itself, the logger is never freed (a reference cycle) until you callclear().messages()returns a new list, but the entries in it are the stored entries. Do not change them, unless you want to change what was captured.new()keeps its own copy of thediaglist and thei18ntables. Changing your array or hash afterwards does not change the logger. (A part of ani18ntable that contains itself is left out of the copy; it would only ever render as an empty string.)When you clone a logger with
$logger->new(%options), each option replaces the old option completely. For example,$logger->new(i18n => { de => {...} })replaces the wholei18nhash. The translations in the old hash are not kept.
The
i18noption is merged message by message. You only need to give the messages that you want to change. For each message, this module looks in youri18nhash first, and then in its own list. So any message that you do not give keeps its normal text.A misspelt method name does not stop the test.
$logger->wran('x')stores the message under the namewranand prints a notice. The test still passes, unless you check the messages.prove -vprints everything.prove -vsetsTEST_VERBOSE, which turns verbose mode on. If a test checks what is printed, setverbose => 0or$ENV{TEST_VERBOSE} = 0.level()does not hide messages. It only changes the answers of theis_*methods. Every message is still stored.
ENCODING
This module never changes the text that your code logs. It stores each message exactly as it was given.
Log messages and fields: any text. ASCII, other languages and emoji are all stored safely. It does not matter if the text is a Perl character string (decoded, for example with
use utf8or "decode" in Encode) or a byte string (for example, UTF-8 bytes read from a file).Matching with
likeandunlike. The pattern is matched against the stored text as it is. So the pattern and the message must be the same kind of string. A pattern with a character, such asqr/\x{1F600}/, does not match the same emoji stored as UTF-8 bytes.Printed messages. A Perl character string that has any character above ASCII is printed as UTF-8. A byte string is printed unchanged. Perl cannot always see the difference: a string that was not decoded, and has no character above 255 (such as
"caf\x{e9}"), is treated as bytes. If the output already has an encoding layer (for example, set by Test2::Plugin::UTF8), the text is not encoded again.This module's own messages. German, French and Chinese messages are character strings, and they are printed as UTF-8. There is one problem case: a translated message that includes a logged message that is a non-ASCII byte string. That part of the text is printed wrongly (it is encoded twice). English messages do not have this problem.
Options.
langandcountrymust be ASCII, in the formats given under "new". Level names are ASCII. Templates in thei18noption may contain any characters, but placeholder names must be ASCII letters, digits or_.Test names. Test names are passed to Test::Builder unchanged.
METHODS
new
Create a new test logger.
Purpose
Make a logger that stores every message it is given, so that your test can check the messages later.
Args
All options are optional. Give them as a list (key => value) or as one hash reference.
verbose- true: print every message. False: use thediagrule. If you do not give it, the value of$ENV{TEST_VERBOSE}or$ENV{VERBOSE}is used.diag- which messages to print.'all','none', a level name (print that level and every more serious level), or an array reference of level names. Upper or lower case does not matter. The default is'warning'.level- the level thatlevel()and theis_*methods report. The default is'trace', so everyis_*method returns 1, and the code under test runs all its debug code. This option does not stop any message from being stored.lang- the language of this module's own messages:'en','de','fr','zh', a locale name such as'de_DE.UTF-8', or'auto'(read the environment). Must be 2 or 3 ASCII letters, then optionally_,.,@or-and more text.country- a two-letter country code such as'GB'or'fr'(upper or lower case). Used to choose the language whenlangis not given.i18n- your own message texts, as{ language => { message_key => template } }. See "i18n".
Other options are allowed and ignored. (A Log::Abstraction configuration hash can be passed unchanged.) If you pass an odd number of arguments, they are all ignored.
Three ways to call it
Test::Log::Abstraction->new(%options)- the usual way.$logger->new(%options)- make a clone: a new logger with the same options, the sameverboseandlevelsettings, and a copy of the stored messages. The options that you pass replace the old ones.Test::Log::Abstraction::new(%options)- called as a function. This works too.
Returns
The new logger object.
Side Effects
Reads %ENV to decide on verbose mode, and, with lang => 'auto', to choose the language. Nothing else changes. A clone does not change the original logger.
EXAMPLE
# The usual way
my $logger = Test::Log::Abstraction->new();
# Store everything, print nothing
my $quiet = Test::Log::Abstraction->new(diag => 'none');
# A hash reference works too; messages in German
my $german = Test::Log::Abstraction->new({ country => 'DE' });
# A clone that prints everything; $logger is not changed
my $loud = $logger->new(diag => 'all');
API SPECIFICATION
Input
{
verbose => { type => 'scalar', optional => 1 },
diag => { type => ['string', 'arrayref'], optional => 1 },
level => { type => 'string', optional => 1 },
lang => { type => 'string', optional => 1, matches => qr/\A(?:(?i:auto)|[A-Za-z]{2,3}(?:[_.\@-][\w.\@-]*+)?)\z/ },
country => { type => 'string', optional => 1, matches => qr/\A[A-Za-z]{2}\z/ },
i18n => { type => 'hashref', optional => 1 },
}
Domains (valid / invalid / boundaries):
verbose valid: any plain value, Perl truth decides ('0', '' and undef
are off; '00', '0.0' and ' ' are on); absent: from
TEST_VERBOSE or VERBOSE. Invalid: any reference.
diag valid: 'all' or 'none' (any case); a level name (any case),
which prints severity 0 (emergency) up to that level's
number, 7 (trace) being every level; an array reference of
level names, [] printing nothing and duplicates counting
once; absent or undef: $config{diag}. Invalid: any other
string, including '' and numbers such as '4'; a list
element that is not a level name (or is 'all'); a hash,
code or scalar reference.
level valid: the 16 level names, any case, setting 0 (emergency)
to 7 (trace); absent or undef: $config{level}. Invalid:
'', numbers (even 0 to 7), any other name.
lang valid: 'auto' (any case); 2 or 3 ASCII letters, then
optionally one of _ . @ - and anything else ('de',
'eng', 'de_DE.UTF-8', 'zh-Hant'). A code with no catalogue
('ja', 'eng') gives 'en'. Invalid: 1 letter, 4 or more
letters, '', digits, a non-ASCII letter in the code, any
whitespace or newline.
country valid: exactly 2 ASCII letters, any case; one with no
mapping ('JP') gives 'en'. Invalid: 1 or 3 letters, '',
digits, non-ASCII letters.
i18n valid: a hash reference (even empty). Invalid: any other
type.
An "invalid argument" explanation, and a level or diag name repeated in an error, is at most 200 characters; one longer is cut to 200 and ends with "...". Control characters in it are shown as \xNN.
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }
MESSAGES
All of these stop the program (croak).
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
invalid diag level 'X' X is not a level name Use a name from the level table
diag must be a level name ... diag is a hash or code ref Give a string or an array ref
invalid syslog level 'X' the level option is unknown Use a name from the level table
invalid argument: ... an option has the wrong type Fix the option that is named
or format
PSEUDOCODE
if new() was called on a logger object:
options = the object's options, replaced by the new options
build a logger from the options, with a copy of the messages
copy verbose and level from the object, unless new values were given
else:
if new() was called as a function, the first argument is an option
turn the arguments into a hash (ignore an odd-length list)
check the options
choose the language, then the diag rule, then the level
return the logger
trace, debug, info, notice, warn, error, fatal, critical, alert, emergency
Store a message at this level.
Purpose
These are the methods that the code under test calls to log something. There is one method for each Log::Abstraction level, and one for each syslog name: warning, err, crit, emerg, panic and informational. Each method stores the message under the name that was called, and may print it (see "Which messages are printed").
By default:
trace,debug,info,informational,notice- stored, not printed.warn,warning,error,err,critical,crit,fatal,alert,emergency,emerg,panic- stored and printed.
Args
Any list of values. They are turned into one message like this:
With no values at all,
warnand the more serious levels (warning,error,err,critical,crit,fatal,alert,emergency,emerg,panic) store nothing, as in Log::Abstraction. The less serious levels (trace,debug,info,informational,notice) store an empty message, as Log::Abstraction does.All the values are joined together, with nothing between them.
One newline at the end is removed.
If the only value is an array reference, its items are the parts of the message.
If there are two or more values and the last one is a hash reference, that hash is not part of the message. A copy of it is stored as
fields. An empty hash is dropped.A hash reference in the message is written as
{key => value, ...}, with the keys sorted. An array reference is written as[a, b]. So you can match their contents.undefis written as the textundef. There is no warning.An object is written as Perl normally writes it. If the object has its own text form (overloaded
""), that form is used.A structure that contains itself is written as
(cycle)at the point where it repeats.
Returns
The logger, as in Log::Abstraction. So you can chain calls: $logger->info('a')->info('b').
Side Effects
Adds one entry to the stored messages (none for a call with no values at warn or above; see "Args"). May print the message. The variables $@ and $! are not changed. So you can log inside an error handler without losing the error.
EXAMPLE
$logger->warn('something looks wrong');
$logger->warn('file ', $name, ' is empty'); # joined: one message
$logger->info('started', { pid => $$ }); # message + fields
$logger->error({ error => 'cannot open file' }); # hash as the message
$logger->debug(['part 1, ', 'part 2']); # array of parts
API SPECIFICATION
Input
{
messages => { type => 'arrayref', position => 0, slurp => 1 },
}
Domains: any number of arguments (0 gives an empty message) of any kind and length. Exactly one trailing newline is removed (so "m\n\n" keeps one). A trailing hash after one or more values is fields; an empty one is dropped; a blessed hash is part of the message. Text may be ASCII, decoded text in any script (accents, CJK, emoji and emoji sequences, combining marks, right-to-left text, zero-width characters) or bytes; it is stored with its length unchanged. See "ENCODING" for how it is printed.
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
X() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
is_trace, is_debug, is_info, is_notice, is_warn, is_error, is_critical, is_alert, is_emergency
Ask if a level is turned on.
Purpose
Some code only builds a log message if the level is turned on, for example if($logger->is_debug()) { ... }. These methods answer that question, as Log::Abstraction does.
Args
None.
Returns
1 if the level is turned on, otherwise 0. A level is turned on when its number is the same as, or lower than, the logger's level (see "level"). The default level is trace, so all these methods return 1.
Side Effects
None.
EXAMPLE
$logger->level('warning');
$logger->is_warn(); # 1
$logger->is_error(); # 1 (more serious than warning)
$logger->is_info(); # 0 (less serious than warning)
API SPECIFICATION
Input
{}
Domains: no arguments. Each predicate is 1 exactly when its level's number is at or below the logger's level: at level 0 (emergency) only is_emergency is 1; at level 7 (trace) all are 1.
Output
{ type => 'boolean' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
is_X() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
AUTOLOAD
Handle a call to a method that does not exist.
Purpose
Perl calls this when the code under test calls a method that this class does not have - usually a misspelt level, such as wran. Any name works, even an empty one ($logger->$name() with $name = ''). Instead of stopping the test, the message is stored under the name that was called, and a notice is printed. The notice is always printed, whatever the diag setting, so the mistake is not hidden.
Args
The same as a level method.
Returns
The logger.
Side Effects
Adds one entry, with the called name as its level. Prints no method 'name'.
EXAMPLE
$logger->wran('oops'); # stored at level 'wran'; a notice is printed
is($logger->count('wran'), 1, 'the misspelt call was stored');
API SPECIFICATION
Input
{
messages => { type => 'arrayref', position => 0, slurp => 1 },
}
Domains: any method name, including '', non-ASCII names and very long names. The message is stored under the name in lower case.
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
no method 'X' X is not a method or a level Fix the method name
(notice; the test goes on)
X() must be called on an an unknown method was called Call it on a logger object
object, not on the class on the class name (croak)
messages
Get the stored messages.
Purpose
Let your test look at everything that was logged.
Args
None.
Returns
A reference to a new array. It has one hash reference for each message, oldest first. Each hash has these keys:
level- the level name, in lower case, as it was called.message- the message text.fields- only when fields were given: a hash reference.
The array is a copy, as in Log::Abstraction. Adding or removing items in it does not change the stored messages. But the hashes in it are the stored hashes (see "COMMON PITFALLS").
Side Effects
None.
EXAMPLE
foreach my $entry (@{ $logger->messages() }) {
diag("$entry->{level}: $entry->{message}");
}
my $first = $logger->messages()->[0];
is($first->{level}, 'warn', 'the first message is a warning');
API SPECIFICATION
Input
{}
Output
{ type => 'arrayref' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
messages() must be called on not called on a logger Call it on a logger object
an object, not on the class (croak)
clear
Delete all stored messages.
Purpose
Start again with an empty list, for example between two steps of a test.
Args
None.
Returns
The logger, so you can chain calls.
Side Effects
All stored messages are deleted. The settings (verbose, level, diag, language) do not change. On a logger with no messages, it does nothing, so it is safe to call before every step.
EXAMPLE
$logger->clear();
$logger->clear()->empty('nothing logged yet');
API SPECIFICATION
Input
{}
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
clear() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
count
Count the stored messages.
Purpose
Check how much was logged, in total or at one level.
Args
$level- optional. Count only messages at this level. Upper or lower case does not matter. Different names for the same level are counted apart:count('warn')does not countwarning()calls.
Returns
The number of messages: 0 or more.
Side Effects
None.
EXAMPLE
is($logger->count(), 3, 'three messages in total');
is($logger->count('error'), 1, 'one of them is an error');
API SPECIFICATION
Input
{
level => { type => 'string', optional => 1, position => 0 },
}
Domains: undef counts every message; a string counts messages at that level, any case ('', '0' and unknown names give 0; aliases are separate names). Invalid: any reference.
Output
{ type => 'integer', min => 0 }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
count() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
invalid argument: ... the level is not a string Give a level name
(croak)
like
Test that a stored message matches a pattern.
Purpose
The test passes if at least one stored message matches the pattern.
Args
$pattern- required. Aqr//regular expression, or a string. A string is also used as a regular expression.$name- optional. The name of the test.
Returns
True if the test passed, false if it failed.
Side Effects
Adds one test result to the TAP output. If the test fails, all stored messages are printed under it (at most 20, then a count of the others).
EXAMPLE
$logger->like(qr/updated/, 'the update was logged');
$logger->like(qr/^Cannot open/i, 'the open error was logged');
API SPECIFICATION
Input
{
pattern => { type => ['regex', 'string'], position => 0 },
name => { type => 'string', optional => 1, position => 1 },
}
Domains: pattern - a qr// or a string, which is compiled as a regular expression ('' matches every message). Invalid: undef, any other reference, a string that does not compile or can never match. name - undef, or any string (including '' and non-ASCII text). Invalid: a reference.
Output
{ type => 'boolean' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
like() needs a pattern no pattern was given (croak) Give a qr// or a string
invalid argument: ... the pattern is not a qr// or Give a qr// or a string
a string, does not compile, that is a valid regex
or can never match (croak)
N messages were captured: the test failed; the stored Compare them with the pattern
messages follow (output)
like() must be called on not called on a logger Call it on a logger object
an object, not on the class (croak)
unlike
Test that no stored message matches a pattern.
Purpose
The test passes if no stored message matches the pattern. It also passes when there are no messages.
Args
$pattern- required. Aqr//regular expression, or a string. A string is also used as a regular expression.$name- optional. The name of the test.
Returns
True if the test passed, false if it failed.
Side Effects
Adds one test result to the TAP output. If the test fails, the messages that matched are printed under it.
EXAMPLE
$logger->unlike(qr/fatal/i, 'nothing fatal was logged');
API SPECIFICATION
Input
{
pattern => { type => ['regex', 'string'], position => 0 },
name => { type => 'string', optional => 1, position => 1 },
}
Domains: as for "like". Note that '' matches every message, so unlike('') fails whenever anything was logged.
Output
{ type => 'boolean' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
unlike() needs a pattern no pattern was given (croak) Give a qr// or a string
invalid argument: ... the pattern is not a qr// or Give a qr// or a string
a string, does not compile, that is a valid regex
or can never match (croak)
N messages matched: the test failed; the Look at the listed messages
matching messages follow
unlike() must be called on not called on a logger Call it on a logger object
an object, not on the class (croak)
has_level
Test that something was logged at a level.
Purpose
The test passes if at least one message was stored at this level.
Args
$level- required. The level name. Upper or lower case does not matter. Different names for the same level are different:has_level('warn')does not seewarning()calls.$name- optional. The name of the test.
Returns
True if the test passed, false if it failed.
Side Effects
Adds one test result to the TAP output. If the test fails, all stored messages are printed under it, so you can see which levels were used.
EXAMPLE
$logger->has_level('error', 'the failure was logged');
API SPECIFICATION
Input
{
level => { type => 'string', position => 0 },
name => { type => 'string', optional => 1, position => 1 },
}
Domains: level - a string, any case ('' and unknown names match nothing; aliases are separate names). Invalid: undef, any reference. name - as for "like".
Output
{ type => 'boolean' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
has_level() needs a level name no level was given (croak) Give a level name
invalid argument: ... the level is not a string Give a level name
(croak)
N messages were captured: the test failed; the stored Look at the listed levels
messages follow (output)
has_level() must be called on not called on a logger Call it on a logger object
an object, not on the class (croak)
empty
Test that nothing was logged.
Purpose
The test passes if there are no stored messages. Use it to check that a normal run logs nothing.
Args
$name- optional. The name of the test.
Returns
True if the test passed, false if it failed.
Side Effects
Adds one test result to the TAP output. If the test fails, the stored messages are printed under it.
EXAMPLE
$logger->empty('a normal run logs nothing');
API SPECIFICATION
Input
{
name => { type => 'string', optional => 1, position => 0 },
}
Domains: name - undef, or any string. Invalid: a reference.
Output
{ type => 'boolean' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
N messages were captured: the test failed; the stored Look at the listed messages
messages follow (output)
empty() must be called on not called on a logger Call it on a logger object
an object, not on the class (croak)
verbose
Get or change verbose mode.
Purpose
In verbose mode, every message is printed, whatever the diag rule says. This helps when you are finding out why a test fails.
Args
$value- optional. True turns verbose mode on, false turns it off. Without an argument, nothing changes.
Returns
The setting after the call: 1 (on) or 0 (off).
Side Effects
Changes the setting, when you give an argument.
EXAMPLE
$logger->verbose(1); # print everything from now on
my $on = $logger->verbose(); # 1
API SPECIFICATION
Input
{
value => { type => 'scalar', optional => 1, position => 0 },
}
Domains: absent - get only; otherwise Perl truth decides ('0', '' and undef are off; '0.0' is on).
Output
{ type => 'integer', min => 0, max => 1 }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
verbose() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
level
Get or change the logger's level.
Purpose
Code under test may read or change the level, as it can with "level" in Log::Abstraction. The level only changes the answers of the is_* methods. Every message is still stored.
Args
$name- optional. A level name. Upper or lower case does not matter.
Returns
No argument: the level's number, from 0 to 7.
A known level name: the logger, so you can chain calls.
An unknown level name:
undef, as in Log::Abstraction.
Side Effects
With a known name, the level changes. With an unknown name, nothing changes and a warning is printed.
EXAMPLE
$logger->level('error');
print $logger->level(), "\n"; # 3
API SPECIFICATION
Input
{
name => { type => 'string', optional => 1, position => 0 },
}
Domains: absent or undef - get only; one of the 16 level names, any case - set (0 for emergency to 7 for trace). Anything else - '', numbers such as '0' or '8', unknown names - warns and returns undef.
Output
{ type => ['integer', 'object'], optional => 1 }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
invalid syslog level 'X' X is not a level name Use a name from the level table
(warning; returns undef)
level() must be called on not called on a logger Call it on a logger object
an object, not on the class (croak)
flush
Do nothing.
Purpose
In Log::Abstraction, flush() sends e-mail messages that are waiting. The test logger never sends e-mail, but the code under test may still call flush(), so it exists.
Args
None.
Returns
The logger, so you can chain calls.
Side Effects
None.
EXAMPLE
$logger->flush();
API SPECIFICATION
Input
{}
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
flush() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
lang
Get the language of this module's messages.
Purpose
Let a test check which language was chosen from the lang and country options or the environment.
Args
None.
Returns
A language code: en, de, fr, zh, or a code that you gave in the i18n option.
Side Effects
None.
EXAMPLE
my $lang = Test::Log::Abstraction->new(country => 'FR')->lang(); # 'fr'
API SPECIFICATION
Input
{}
Output
{ type => 'string', matches => qr/\A[a-z]{2,3}\z/ }
MESSAGES
Message Meaning What to do
------------------------------ ---------------------------- ------------------------------
lang() must be called on an not called on a logger Call it on a logger object
object, not on the class (croak)
METHODS FOR SUBCLASSES
These methods are for this class, and classes that inherit from it. A subclass may call them, or replace them. Nothing stops other code calling them, but they are not part of the public interface, and may change.
_emit
Print one line of test output.
Purpose
Every line that this module prints goes through this method. A subclass can replace it to send the lines somewhere else.
Args
$text- the line to print.
Returns
The logger.
Side Effects
Prints the line as a TAP comment, with "diag" in Test::Builder. This works even if the test did not load Test::More. A Perl character string is encoded as UTF-8 first, unless the output already has an encoding layer (see "ENCODING").
EXAMPLE
package My::Logger;
use parent -norequire, 'Test::Log::Abstraction';
# Send the lines to STDERR instead of the TAP output
sub _emit {
my ($self, $text) = @_;
print STDERR "$text\n";
return $self;
}
API SPECIFICATION
Input
{
text => { type => 'string', position => 0 },
}
Output
{ type => 'object', isa => 'Test::Log::Abstraction' }
MESSAGES
None. It never fails.
i18n
Make one of this module's messages, in the logger's language.
Purpose
All the text that this module shows to people comes from here. So the text can be translated, or changed, without changing the code.
Args
$key- the name of the message, such asneeds_pattern.\%args- optional. Values for the placeholders in the message.classis filled in for you (the logger's class).countchooses the singular or plural form.genderchooses a gender form.
How a message template works
A template is a string. %{name}s is replaced by the value called name. After the name you can use any sprintf format letter, for example %{count}d or %{ratio}.2f. %% gives one %. A value that is missing becomes the text undef, with no warning.
A template can also be a hash. The keys are gender names (such as male, female) or plural forms (zero, one, two, few, many, other). The values are templates, so they can be hashes too. other is used when nothing else fits. zero is used for a count of 0, if it is there.
{
zero => 'no messages',
one => '%{count}d message',
other => '%{count}d messages',
}
Where the template is found
The first one found is used:
- 1. Your
i18noption, in the logger's language. - 2. This module's messages, in the logger's language.
- 3. Your
i18noption, in English. - 4. This module's messages, in English.
If the key is not found anywhere, the key itself is returned.
Returns
The finished message, as a Perl character string.
Side Effects
None.
EXAMPLE
# Inside a subclass
my $text = $self->i18n('needs_pattern', { method => 'like' });
# "Test::Log::Abstraction: like() needs a pattern"
# Your own message, with gender and plural forms. i18n() is
# meant for subclasses, so call it from a method of your subclass.
my $logger = My::Logger->new(i18n => {
en => {
logged => {
male => { one => 'He logged %{count}d line', other => 'He logged %{count}d lines' },
other => 'They logged %{count}d lines',
},
},
});
# In a method of My::Logger:
$self->i18n('logged', { gender => 'male', count => 2 }); # 'He logged 2 lines'
API SPECIFICATION
Input
{
key => { type => 'string', position => 0 },
args => { type => 'hashref', optional => 1, position => 1 },
}
Domains: key - any string; undef renders as '', a reference as its string form, and an unknown key is returned as it is. args - a hash reference; anything else is ignored. count - a number selects a plural form (0 uses 'zero' when present); anything else uses 'other'.
Output
{ type => 'string' }
MESSAGES
None. It never fails, for any key or arguments.
PSEUDOCODE
language = the logger's language (for a class name: the configured one)
template = the first one found in the four places listed above
if no template was found, return the key
while the template is a hash:
choose by gender, else by plural form, else 'other'
replace each %{name}format with sprintf(format, the value of name)
return the text
LIMITATIONS
It accepts more than the real logger. The syslog names (
warning,err,crit,emerg,panic,informational) are methods here, but not in Log::Abstraction 0.39. Code that calls them passes its tests, and then stops with an error in production. Astrictoption, which allows only the real Log::Abstraction methods, would be safer.Messages are stored under the name that was called. See "COMMON PITFALLS". Your test must use the same name as the code under test.
level()does not hide messages. Log::Abstraction drops messages below its level. This module stores them all, which is usually what a test wants.Message text is not always the same as in Log::Abstraction.
undefbecomesundefinstead of being dropped, and hashes and arrays are written out as data. A test that compares the exact text may give a different result with the real logger.Mixed encodings in translated output. See "ENCODING".
Internal methods are not locked. Methods whose names start with
_, and the subclass methods_emitandi18n, can be called by any code. Nothing checks the caller: a test helper should not need compiled modules to do that. Do not rely on them outside a subclass.Many modules are needed. Params::Validate::Strict, Params::Get, Readonly, and autodie (with IPC::System::Simple) must be installed, for what is a small test helper. Object::Configure is not used, because it loads Log::Abstraction, and a test logger should not need that.
Simple translation system. Locale::Maketext uses numbered placeholders and has no gender forms. Modules based on gettext need compiled files. Neither fits a small table with named placeholders, so this module has its own. Native speakers have not yet checked the German, French and Chinese texts.
Slow patterns are not stopped.
likeandunlikerun the pattern against every stored message. A pattern with nested quantifiers, such asqr/(a+)+$/, can take a very long time on some messages. This module does not limit the time.How long things take.
like()andhas_level()stop at the first message that matches.unlike()that passes,count()with a level, andmessages()look at every stored message, so their time grows with the number of messages (about 0.1 to 0.25 microseconds a message on a typical machine). A log call costs about 3 microseconds. For tests that log hundreds of thousands of messages, callclear()between phases.One process only. Messages are kept in the memory of the logger object. Messages logged in a child process (after
fork) are not seen by the parent.
DIAGNOSTICS
Each method lists its messages under MESSAGES. All messages can be translated or changed; see "i18n".
The text after invalid argument: explains what was wrong, and may show the value that was given. Other errors and warnings repeat a name you gave: a level name, a diag name, or a method name. Every value repeated like this is cut to 200 characters, and control characters in it (such as a newline, or the escape character that starts a terminal control sequence) are shown as \xNN. So a hostile value cannot make an error message enormous, add a forged ok line to the output, or control your terminal.
SEE ALSO
Log::Abstraction, Test::Builder, Test::Most
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
FORMAL SPECIFICATION
This section describes each method in the Z notation. You do not need it to use the module. It is here so that the behaviour is exact.
State
[TEXT, NAME, KEY, TEMPLATE, TAG, VALUE]
BOOL ::= true | false
SEVERITY == 0 .. 7
BUILTIN == { en, de, fr, zh }
severity : NAME ⇸ SEVERITY
lower : NAME → NAME
catalogue : TAG ⇸ (KEY ⇸ TEMPLATE)
render : seq VALUE → TEXT
─────────
dom catalogue = BUILTIN
∀ n : dom severity • lower n = n
Entry ≙ [ level : NAME; message : TEXT; fields : TEXT ⇸ VALUE ]
Rule ::= all | none | levels⟨⟨ℙ NAME⟩⟩ | atleast⟨⟨SEVERITY⟩⟩
Logger
log : seq Entry
verbose : BOOL
threshold : SEVERITY
lang : TAG
overrides : TAG ⇸ (KEY ⇸ TEMPLATE)
rule : Rule
─────────
lang ∈ BUILTIN ∪ dom overrides
∀ e : ran log • e.level = lower e.level
ΔLogger ≙ [ Logger; Logger' | overrides' = overrides ∧ rule' = rule ]
printed : Logger × NAME → BOOL
─────────
∀ L : Logger; n : NAME •
printed (L, n) = true ⇔
L.verbose = true ∨ L.rule = all
∨ (∃ S : ℙ NAME • L.rule = levels S ∧ n ∈ S)
∨ (∃ t : SEVERITY • L.rule = atleast t ∧ n ∈ dom severity ∧ severity n ≤ t)
severity is the level table under "Levels and how serious they are". lower is Perl's lc. catalogue is the built-in message table, and overrides is the logger's i18n option; so a logger's language may be one that only overrides has. render turns a log call's arguments into its message text (see the level methods' "Args"). rule is the diag option, and printed says whether a stored message is also printed. ΔLogger means the method may change the log and the settings, but never overrides or rule; ΞLogger means it changes nothing.
new
New
Logger'
opts? : NAME ⇸ VALUE
config : NAME ⇸ VALUE
env : NAME ⇸ TEXT
─────────
let get ≙ λ k : NAME • (if k ∈ dom opts? then opts? k else config k) •
lower (get level) ∈ dom severity
log' = ⟨⟩
verbose' = (if verbose ∈ dom opts? then opts? verbose
else env TEST_VERBOSE ∨ env VERBOSE)
threshold' = severity (lower (get level))
overrides' = (if i18n ∈ dom opts? then opts? i18n else ∅)
rule' = rule_of (get diag)
lang' = language (opts?, config, env, overrides')
Clone
ΞLogger
clone! : Logger
─────────
clone!.log = log
clone!.verbose = verbose
clone!.threshold = threshold
clone!.lang = lang
config is "Default settings" and env is %ENV. rule_of turns the diag option into a Rule, and fails (croaks) for anything else. language applies the order under "Language of this module's messages": lang, then country, then (for lang => 'auto') the locale variables in env, then config; the result is the first two or three letters, if BUILTIN or overrides' has that language, and otherwise en.
Clone is $logger->new() with no options. Options that are given replace the matching values, as in New.
trace, debug, info, notice, warn, error, fatal, critical, alert, emergency
Log
ΔLogger
name? : NAME
args? : seq VALUE
─────────
name? ∈ dom severity
args? = ⟨⟩ ∧ severity name? ≤ severity warn ⇒ log' = log
¬ (args? = ⟨⟩ ∧ severity name? ≤ severity warn) ⇒
log' = log ⁀ ⟨⟨ level ↦ name?, message ↦ render args? ⟩⟩
verbose' = verbose ∧ threshold' = threshold ∧ lang' = lang
A stored entry is also printed exactly when printed (θLogger, name?).
is_trace, is_debug, is_info, is_notice, is_warn, is_error, is_critical, is_alert, is_emergency
IsLevel
ΞLogger
name? : NAME
result! : BOOL
─────────
name? ∈ dom severity
result! = true ⇔ severity name? ≤ threshold
AUTOLOAD
Unknown
ΔLogger
name? : NAME
args? : seq VALUE
─────────
name? ¬in; dom severity
log' = log ⁀ ⟨⟨ level ↦ lower name?, message ↦ render args? ⟩⟩
verbose' = verbose ∧ threshold' = threshold ∧ lang' = lang
The notice no method 'name?' is always printed. A name in another case, such as INFO, is not a method, so it comes here, and is stored as info.
messages
Messages
ΞLogger
result! : seq Entry
─────────
result! = log
clear
Clear
ΔLogger
─────────
log' = ⟨⟩
verbose' = verbose ∧ threshold' = threshold ∧ lang' = lang
count
Count
ΞLogger
level? : NAME
result! : ℕ
─────────
result! = # (log ↾ { e : Entry | e.level = lower level? })
CountAll
ΞLogger
result! : ℕ
─────────
result! = # log
like
Like
ΞLogger
pattern? : ℙ TEXT
result! : BOOL
─────────
result! = true ⇔ (∃ e : ran log • e.message ∈ pattern?)
unlike
Unlike
ΞLogger
pattern? : ℙ TEXT
result! : BOOL
─────────
result! = true ⇔ (∀ e : ran log • e.message ¬in; pattern?)
has_level
HasLevel
ΞLogger
level? : NAME
result! : BOOL
─────────
result! = true ⇔ (∃ e : ran log • e.level = lower level?)
empty
Empty
ΞLogger
result! : BOOL
─────────
result! = true ⇔ log = ⟨⟩
verbose
SetVerbose
ΔLogger
value? : BOOL
result! : BOOL
─────────
verbose' = value?
result! = verbose'
log' = log ∧ threshold' = threshold ∧ lang' = lang
GetVerbose
ΞLogger
result! : BOOL
─────────
result! = verbose
level
SetLevel
ΔLogger
name? : NAME
─────────
name? ∈ dom severity ⇒ threshold' = severity name?
name? ¬in; dom severity ⇒ threshold' = threshold
log' = log ∧ verbose' = verbose ∧ lang' = lang
GetLevel
ΞLogger
result! : SEVERITY
─────────
result! = threshold
flush
Flush
ΞLogger
lang
Lang
ΞLogger
result! : LANG
─────────
result! = lang
i18n
I18n
ΞLogger
key? : KEY
result! : TEXT
─────────
let find ≙ λ t : TAG •
{ m : (TAG ⇸ (KEY ⇸ TEMPLATE)) | m ∈ { overrides, catalogue }
∧ t ∈ dom m ∧ key? ∈ dom (m t) } •
find lang ≠ ∅ ⇒ result! = fill (first (find lang) lang key?)
find lang = ∅ ∧ find en ≠ ∅ ⇒ result! = fill (first (find en) en key?)
find lang = ∅ ∧ find en = ∅ ⇒ result! = key?
fill chooses the gender or plural form and fills in the placeholders. first takes overrides before catalogue when both have the key: the i18n option is searched first in each language.
STATE DIAGRAM
A logger has two main states: EMPTY (no stored messages) and CAPTURING (one or more stored messages). Two settings, verbose and level, can change in either state; they do not move the logger between states. Methods that only read or test (like, count, is_debug, and so on) never change the state.
new(%options)
[check options; choose language,
diag rule and level]
|
| invalid option
+----------------------> croak, no logger made
|
v
+-----------------------------------------+
| EMPTY |<----------------+
| messages = () | |
+-----------------------------------------+ |
| ^ |
| | clear() |
+-------+ [nothing to delete; return the logger] |
| |
| trace() ... emergency(), warning() ... panic() | clear()
| [store the message; print it if the diag rule | [delete all
| or verbose allows; $@ and $! are kept. With no | messages;
| arguments, warn() and above store nothing and stay | return the
| in EMPTY (or CAPTURING), as in Log::Abstraction] | logger]
| |
| wran() or any unknown method (AUTOLOAD) |
| [store under that name; always print |
| "no method 'wran'"] |
v |
+-----------------------------------------+ |
| CAPTURING |-----------------+
| messages = (m1, m2, ...) |
+-----------------------------------------+
| ^
| | any level method, or an unknown method
+-------+ [store one more message; maybe print it]
Changes allowed in BOTH states (the state stays the same):
verbose(1) / verbose(0) verbose on / off
[from now on: print every message / use
the diag rule]
level('error') level number = 3
[the is_* answers change; nothing is
hidden]
level('bogus') no change [warning; returns undef]
Read-only calls in BOTH states (the state stays the same):
like, unlike, has_level, empty
[one TAP result; on failure, print the
messages that explain it]
count, messages, lang, is_trace ... is_emergency, flush
[return a value only]
like(undef), has_level(undef), a bad argument, or any method
called on something that is not a logger (the class name, undef,
a plain reference, or another class's object)
[croak; no change]
Copying (the original logger does not change):
EMPTY --- $logger->new(%options) ---> a new logger in EMPTY
CAPTURING --- $logger->new(%options) ---> a new logger in CAPTURING
[the new logger has copies of the messages, and the
same verbose and level, unless new values are given]
End:
EMPTY or CAPTURING --- the last reference goes away ---> destroyed
[DESTROY does nothing; nothing is stored or printed]
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.