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, alert and emergency. It also accepts the syslog names warning, err, crit, emerg, panic and informational.

  • Other logger methods. level(), is_debug() and the other is_<level>() methods, messages() and flush(). Code under test may call these, so they work as they do in Log::Abstraction.

  • Test methods. like, unlike, has_level and empty. Each one reports one test result, like ok() in Test::More.

  • Helper methods. count, clear, verbose and lang.

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. diag is 'all'.
3. diag is a list, and the message's level name is in it.
4. diag is 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 lang option, for example lang => 'de'.
2. The country option, a two-letter country code such as 'FR'.
3. Only when lang => 'auto': the environment variables LC_ALL, LC_MESSAGES and LANG, 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_level and empty each add one test to the TAP output. If you give a test plan (tests => 5), count them. Or use done_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, like may 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, use qr/\Qa.c\E/.

  • Different names for one level are counted apart. warn and warning have the same number, but count('warn') and has_level('warn') do not see messages logged with warning(). Check the name that the code under test uses. fatal is stored as fatal, but Log::Abstraction stores it as error.

  • undef is not the same as "no value".

    • An undef argument to a level method becomes the text undef in the message. (Log::Abstraction drops it.)

    • verbose => undef turns verbose mode off. To use the environment variables instead, do not pass verbose at all.

    • diag => undef and level => undef use the default from %config.

    • count(undef) counts all messages. level(undef) returns the level number and changes nothing.

    • like(undef), unlike(undef) and has_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 text text and stores { a => 1 } in fields. 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 call clear().

    • 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 the diag list and the i18n tables. Changing your array or hash afterwards does not change the logger. (A part of an i18n table 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 whole i18n hash. The translations in the old hash are not kept.

  • The i18n option is merged message by message. You only need to give the messages that you want to change. For each message, this module looks in your i18n hash 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 name wran and prints a notice. The test still passes, unless you check the messages.

  • prove -v prints everything. prove -v sets TEST_VERBOSE, which turns verbose mode on. If a test checks what is printed, set verbose => 0 or $ENV{TEST_VERBOSE} = 0.

  • level() does not hide messages. It only changes the answers of the is_* 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 utf8 or "decode" in Encode) or a byte string (for example, UTF-8 bytes read from a file).

  • Matching with like and unlike. 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 as qr/\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. lang and country must be ASCII, in the formats given under "new". Level names are ASCII. Templates in the i18n option 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 the diag rule. 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 that level() and the is_* methods report. The default is 'trace', so every is_* 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 when lang is 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 same verbose and level settings, 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, warn and 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.

  • undef is written as the text undef. 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 count warning() 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. A qr// 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. A qr// 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 see warning() 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 as needs_pattern.

  • \%args - optional. Values for the placeholders in the message. class is filled in for you (the logger's class). count chooses the singular or plural form. gender chooses 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 i18n option, in the logger's language.
2. This module's messages, in the logger's language.
3. Your i18n option, 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. A strict option, 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. undef becomes undef instead 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 _emit and i18n, 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. like and unlike run the pattern against every stored message. A pattern with nested quantifiers, such as qr/(a+)+$/, can take a very long time on some messages. This module does not limit the time.

  • How long things take. like() and has_level() stop at the first message that matches. unlike() that passes, count() with a level, and messages() 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, call clear() 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.