## Name Test::Log::Abstraction - Capture log output in tests and assert on it ## Version 0.002.0 ## Synopsis ### Check What Your Code Logged ```perl 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 ```perl 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 ```perl 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 ```perl 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 ```perl # 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 ```perl # 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 ```perl 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](https://metacpan.org/pod/Log%3A%3AAbstraction) 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](https://metacpan.org/pod/Log%3A%3AAbstraction): `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_()` methods, `messages()` and `flush()`. Code under test may call these, so they work as they do in [Log::Abstraction](https://metacpan.org/pod/Log%3A%3AAbstraction). - **Test methods.** `like`, `unlike`, `has_level` and `empty`. Each one reports one test result, like `ok()` in [Test::More](https://metacpan.org/pod/Test%3A%3AMore). - **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"](#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](https://metacpan.org/pod/Object%3A%3AConfigure): ``` $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](https://metacpan.org/pod/Object%3A%3AConfigure) straight to `new()`. Settings in environment variables named `Test__Log__Abstraction__KEY` then override the arguments; the extra keys that [Object::Configure](https://metacpan.org/pod/Object%3A%3AConfigure) adds (such as its own `logger`) are ignored: ```perl # 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](https://metacpan.org/pod/Log%3A%3AAbstraction) 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](https://metacpan.org/pod/Log%3A%3AAbstraction) 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](https://metacpan.org/pod/Encode#decode)) 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](https://metacpan.org/pod/Test2%3A%3APlugin%3A%3AUTF8)), 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"](#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](https://metacpan.org/pod/Test%3A%3ABuilder) 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"](#i18n). Other options are allowed and ignored. (A [Log::Abstraction](https://metacpan.org/pod/Log%3A%3AAbstraction) 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 ```perl # 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 ```perl { 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 ```perl { 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](https://metacpan.org/pod/Log%3A%3AAbstraction) 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"](#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](https://metacpan.org/pod/Log%3A%3AAbstraction). The less serious levels (`trace`, `debug`, `info`, `informational`, `notice`) store an empty message, as [Log::Abstraction](https://metacpan.org/pod/Log%3A%3AAbstraction) 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](https://metacpan.org/pod/Log%3A%3AAbstraction). 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"](#args)). May print the message. The variables `$@` and `$!` are not changed. So you can log inside an error handler without losing the error. #### Example ```perl $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 ```perl { 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"](#encoding) for how it is printed. ##### Output ```perl { 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](https://metacpan.org/pod/Log%3A%3AAbstraction) 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"](#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 ```perl { 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 ```perl { 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 ```perl { 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](https://metacpan.org/pod/Log%3A%3AAbstraction). Adding or removing items in it does not change the stored messages. But the hashes in it are the stored hashes (see ["COMMON PITFALLS"](#common-pitfalls)). #### Side Effects None. #### Example ```perl 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 ```perl { 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 ```perl { 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 ```perl { 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 ```perl { 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 ```perl { 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 ```perl { 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 ```perl { pattern => { type => ['regex', 'string'], position => 0 }, name => { type => 'string', optional => 1, position => 1 }, } ``` Domains: as for ["like"](#like). Note that '' matches every message, so unlike('') fails whenever anything was logged. ##### Output ```perl { 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 ```perl { 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"](#like). ##### Output ```perl { 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 ```perl { name => { type => 'string', optional => 1, position => 0 }, } ``` Domains: name - undef, or any string. Invalid: a reference. ##### Output ```perl { 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 ```perl $logger->verbose(1); # print everything from now on my $on = $logger->verbose(); # 1 ``` #### Api Specification ##### Input ```perl { 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 ```perl { 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](https://metacpan.org/pod/Log%3A%3AAbstraction#level). 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](https://metacpan.org/pod/Log%3A%3AAbstraction). #### 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 ```perl { 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 ```perl { 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](https://metacpan.org/pod/Log%3A%3AAbstraction), `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 ```perl { 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 ```perl my $lang = Test::Log::Abstraction->new(country => 'FR')->lang(); # 'fr' ``` #### Api Specification ##### Input ``` {} ``` ##### Output ```perl { 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](https://metacpan.org/pod/Test%3A%3ABuilder#diag). This works even if the test did not load [Test::More](https://metacpan.org/pod/Test%3A%3AMore). A Perl character string is encoded as UTF-8 first, unless the output already has an encoding layer (see ["ENCODING"](#encoding)). #### Example ```perl 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 ```perl { text => { type => 'string', position => 0 }, } ``` ##### Output ```perl { 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. ```perl { 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 ```perl # 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 ```perl { 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 ```perl { 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](https://metacpan.org/pod/Log%3A%3AAbstraction) 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](https://metacpan.org/pod/Log%3A%3AAbstraction) methods, would be safer. - **Messages are stored under the name that was called.** See ["COMMON PITFALLS"](#common-pitfalls). Your test must use the same name as the code under test. - **`level()` does not hide messages.** [Log::Abstraction](https://metacpan.org/pod/Log%3A%3AAbstraction) 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](https://metacpan.org/pod/Log%3A%3AAbstraction).** `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"](#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](https://metacpan.org/pod/Params%3A%3AValidate%3A%3AStrict), [Params::Get](https://metacpan.org/pod/Params%3A%3AGet), [Readonly](https://metacpan.org/pod/Readonly), and [autodie](https://metacpan.org/pod/autodie) (with [IPC::System::Simple](https://metacpan.org/pod/IPC%3A%3ASystem%3A%3ASimple)) must be installed, for what is a small test helper. [Object::Configure](https://metacpan.org/pod/Object%3A%3AConfigure) is not used, because it loads [Log::Abstraction](https://metacpan.org/pod/Log%3A%3AAbstraction), and a test logger should not need that. - **Simple translation system.** [Locale::Maketext](https://metacpan.org/pod/Locale%3A%3AMaketext) 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"](#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 - [Test Dashboard](https://nigelhorne.github.io/Test-Log-Abstraction/coverage/) [Log::Abstraction](https://metacpan.org/pod/Log%3A%3AAbstraction), [Test::Builder](https://metacpan.org/pod/Test%3A%3ABuilder), [Test::Most](https://metacpan.org/pod/Test%3A%3AMost) ## Author Nigel Horne, `` ## 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"](#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"](#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"](#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"](#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? ∉ 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 ∉ 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? ∉ 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. ```perl 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.