Difference between revisions of "Unit Module"

From BaseX Documentation
Jump to navigation Jump to search
 
(32 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
This [[Module Library|XQuery Module]] contains annotations and functions for performing XQUnit tests.
 
This [[Module Library|XQuery Module]] contains annotations and functions for performing XQUnit tests.
 
With {{Version|7.9}}, the unit testing functionality has been further stabilized:
 
 
* Tests are now started with a [[Commands#TEST|TEST]] command.
 
* Tests can also be executed from the BaseX GUI.
 
* XQUnit functions can now contain updating functions.<br/> 
 
  
 
=Introduction=
 
=Introduction=
  
The more complex a software application grows, the more error-prone it gets. This is why testing frameworks have been developed, which provide a standardized, automatized way for testing software. The [http://en.wikipedia.org/wiki/XUnit XUnit] frameworks (such as SUnit or JUnit) allow testing of atomic unit of a program, such as single functions and algorithms.
+
The more complex a software application grows, the more error-prone it gets. This is why testing frameworks have been developed, which provide a standardized, automated way of testing software. The [https://en.wikipedia.org/wiki/XUnit XUnit] frameworks (such as SUnit or JUnit) allow testing of atomic units of a program, such as single functions and algorithms.
  
 
This module borrows heavily from the existing frameworks: it provides various annotations for testing XQuery functions. Unit functions are provided to assert the validity of arbitrary conditions expressed in XQuery and to raise errors whenever a condition is not satisfied. Some additional functions exist to run all unit tests of the current module or a set of specified library modules.
 
This module borrows heavily from the existing frameworks: it provides various annotations for testing XQuery functions. Unit functions are provided to assert the validity of arbitrary conditions expressed in XQuery and to raise errors whenever a condition is not satisfied. Some additional functions exist to run all unit tests of the current module or a set of specified library modules.
Line 15: Line 9:
 
=Usage=
 
=Usage=
  
Tests are not started from within XQuery anymore. Instead, a [[Commands#TEST|TEST]] command
+
Tests are started via the {{Command|TEST}} command. It compiles all XQuery modules in a given file
is now available, which compiles all XQuery modules in a given file or directory and runs all
+
or directory and runs all functions that are annotated with {{Code|%unit:test}}. A test report is
functions that are annotated with {{Code|%unit:test}}. A test report is generated and returned,
+
generated and returned, which resembles the format returned by other xUnit testing frameworks,
which resembles the format returned by other xUnit testing frameworks, such as the Maven
+
such as the Maven Surefire Plugin ([[#Result|see below]]).
Surefire Plugin ([[#Result|see below]]).
 
  
 
=Conventions=
 
=Conventions=
  
All annotations, functions and errors in this module are assigned to the {{Code|http://basex.org/modules/unit}} namespace, which is statically bound to the {{Code|unit}} prefix.<br/>
+
All annotations, functions and errors in this module are assigned to the <code><nowiki>http://basex.org/modules/unit</nowiki></code> namespace, which is statically bound to the {{Code|unit}} prefix.<br/>
  
 
=Annotations=
 
=Annotations=
  
==%unit:test==
+
==unit:test==
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
 
| width='120' | '''Syntax'''
 
| width='120' | '''Syntax'''
|{{Code|%unit:test}}<br/>{{Code|%unit:test("expected", <ERROR>)}}
+
|{{Code|%unit:test}}<br/>{{Code|%unit:test("expected", CODE)}}
|-
+
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
|With this annotation, a function can be marked as unit test. It will be evaluated whenever a test report is created for the module in which this function is located.<br/>If an optional error code is specified and if the function expression does not raise that error, the test will fail.
+
|With this annotation, a function can be marked as unit test. It will be evaluated if a test report is created for the module in which this function is located.<br/><code>error</code> can be supplied as additional string argument. It is followed by <code>CODE</code>, which must be a valid [[XQuery 3.0#Expanded QNames|EQName]] string. If the function expression does not raise that error, the test will fail.
 +
|- valign="top"
 +
| '''Examples'''
 +
|
 +
* The following test will be successful, as it does nothing (and, hence, nothing wrong):
 +
<pre lang='xquery'>
 +
declare %unit:test function local:void() { () };
 +
</pre>
 +
* The following test will be successful, as the function body will raise <code>err:XPTY0004</code>:
 +
<pre lang='xquery'>
 +
declare %unit:test('expected', "err:XPTY0004") function local:add() {
 +
  123 + 'strings and integers cannot be added'
 +
};
 +
</pre>
 
|}
 
|}
  
==%unit:before==
+
==unit:before==
 
 
{{Mark|Updated with Version 8.0}}: filter argument added.
 
  
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
 
| width='120' | '''Syntax'''
 
| width='120' | '''Syntax'''
|{{Code|%unit:before}}<br/>{{Code|%unit:before("function")}}
+
|{{Code|%unit:before}}<br/>{{Code|%unit:before(FUNCTION)}}
|-
+
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
|A function decorated with this annotation will be evaluated '''before each''' unit test. If a function name is specified as argument, it will only be evaluated before that function.
+
|A function decorated with this annotation will be evaluated '''before each''' unit test as a separate transaction.<br/><code>FUNCTION</code> can be supplied as additional argument. It must be a valid [[XQuery 3.0#Expanded QNames|EQName]] string. If specified, the function will only be evaluated before a function with the given name is tested. This extension is e. g. helpful if the results of updates need to be tested.
 +
|- valign="top"
 +
| '''Examples'''
 +
|
 +
* The first function will be evaluated before the actual test:
 +
<pre lang='xquery'>
 +
declare %updating %unit:before("local:check") function local:before-check() {
 +
  db:create('test-db')
 +
};
 +
declare %updating %unit:test function local:check() {
 +
  unit:assert(db:exists('test-db'))
 +
};
 +
</pre>
 
|}
 
|}
  
==%unit:after==
+
==unit:after==
 
 
{{Mark|Updated with Version 8.0}}: filter argument added.
 
  
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
 
| width='120' | '''Syntax'''
 
| width='120' | '''Syntax'''
|{{Code|%unit:after}}<br/>{{Code|%unit:after("function")}}
+
|{{Code|%unit:after}}<br/>{{Code|%unit:after(FUNCTION)}}
|-
+
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
|A function decorated with this annotation will be evaluated '''after each''' unit test. If a function name is specified as argument, it will only be evaluated before that function.
+
|A function decorated with this annotation will be evaluated '''after each''' unit test as a separate transaction.<br/><code>FUNCTION</code> can be supplied as additional argument. It must be a valid [[XQuery 3.0#Expanded QNames|EQName]] string. If specified, the function will only be evaluated after a function with the given name is tested.
 
|}
 
|}
  
==%unit:before-module==
+
==unit:before-module==
 +
 
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
 
| width='120' | '''Syntax'''
 
| width='120' | '''Syntax'''
 
|{{Code|%unit:before-module}}
 
|{{Code|%unit:before-module}}
|-
+
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
|If a function is decorated with this annotation, it will be evaluated '''before all''' unit tests in the current module.
+
|If a function is decorated with this annotation, it will be evaluated '''before all''' unit tests in the current module as a separate transaction.
 
|}
 
|}
  
==%unit:after-module==
+
==unit:after-module==
 +
 
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
 
| width='120' | '''Syntax'''
 
| width='120' | '''Syntax'''
 
|{{Code|%unit:after-module}}
 
|{{Code|%unit:after-module}}
|-
+
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
|If a function is decorated with this annotation, it will be evaluated '''after all''' unit tests in the current module.
+
|If a function is decorated with this annotation, it will be evaluated '''after all''' unit tests in the current module as a separate transaction.
 
|}
 
|}
  
==%unit:ignore==
+
==unit:ignore==
 +
 
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
 
| width='120' | '''Syntax'''
 
| width='120' | '''Syntax'''
|{{Code|%unit:ignore}}<br/>{{Code|%unit:ignore("message")}}
+
|{{Code|%unit:ignore}}<br/>{{Code|%unit:ignore(MESSAGE)}}
|-
+
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
 
|If a function is decorated with this annotation, it will temporarily be ignored by the test suite runner.
 
|If a function is decorated with this annotation, it will temporarily be ignored by the test suite runner.
Line 95: Line 112:
 
=Functions=
 
=Functions=
  
{{Mark|Updated with Version 8.0}}: failure argument can now be an arbitrary item.
+
==unit:assert==
  
==unit:assert==
 
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
| width='120' | '''Signatures'''
+
| width='120' | '''Signature'''
|{{Func|unit:assert|$test as item()*|empty-sequence()}}<br />{{Func|unit:assert|$test as item()*, $info as item()|empty-sequence()}}<br />
+
|<pre>unit:assert(
|-
+
  $test as item()*,
 +
  $info as item()   := ()
 +
) as empty-sequence()</pre>
 +
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
 
|Asserts that the effective boolean value of the specified {{Code|$test}} is true and returns an empty sequence. Otherwise, raises an error. The ''effective boolean value'' of an expression can be explicitly computed by using the {{Code|fn:boolean}} function.<br/>The default failure message can be overridden with the {{Code|$info}} argument.
 
|Asserts that the effective boolean value of the specified {{Code|$test}} is true and returns an empty sequence. Otherwise, raises an error. The ''effective boolean value'' of an expression can be explicitly computed by using the {{Code|fn:boolean}} function.<br/>The default failure message can be overridden with the {{Code|$info}} argument.
|-
+
|- valign="top"
 
| '''Errors'''
 
| '''Errors'''
|{{Error|UNIT0001|#Errors}} the assertion failed, or an error was raised.
+
|{{Error|fail|#Errors}} the assertion failed, or an error was raised.
 
|}
 
|}
  
Line 113: Line 132:
  
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
| width='120' | '''Signatures'''
+
| width='120' | '''Signature'''
|{{Func|unit:assert-equals|$returned as item()*, $expected as item()*|empty-sequence()}}<br />{{Func|unit:assert-equals|$returned as item()*, $expected as item()*, $info as item()|empty-sequence()}}<br />
+
|<pre>unit:assert-equals(
|-
+
  $returned as item()*,
 +
  $expected as item()*,
 +
  $info     as item()   := ()
 +
) as empty-sequence()</pre>
 +
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
|Asserts that the specified arguments are equal according to the rules of the {{Code|fn:deep-equals}} function. Otherwise, raises an error.<br/>The default failure message can be overridden with the {{Code|$info}} argument.
+
|Asserts that the specified arguments are equal according to the rules of the [https://www.w3.org/TR/xpath-functions-31/#func-deep-equal {{Code|fn:deep-equal}} function]. Otherwise, raises an error.<br/>The default failure message can be overridden with the {{Code|$info}} argument.
|-
+
|- valign="top"
 
| '''Errors'''
 
| '''Errors'''
|{{Error|UNIT0001|#Errors}} the assertion failed, or an error was raised.
+
|{{Error|fail|#Errors}} the assertion failed, or an error was raised.
 
|}
 
|}
  
 
==unit:fail==
 
==unit:fail==
 
{{Mark|Updated with Version 8.0}}: 0-argument signature adeded.
 
  
 
{| width='100%'
 
{| width='100%'
|-
+
|- valign="top"
| width='120' | '''Signatures'''
+
| width='120' | '''Signature'''
|{{Func|unit:fail||empty-sequence()}}<br />{{Func|unit:fail|$info as item()|empty-sequence()}}<br />
+
|<pre>unit:fail(
|-
+
  $info as item() := ()
 +
) as empty-sequence()</pre>
 +
|- valign="top"
 
| '''Summary'''
 
| '''Summary'''
 
|Raises a unit error. The default failure message can be overridden with the {{Code|$info}} argument.
 
|Raises a unit error. The default failure message can be overridden with the {{Code|$info}} argument.
|-
+
|- valign="top"
 
| '''Errors'''
 
| '''Errors'''
|{{Error|UNIT0001|#Errors}} default error raised by this function.
+
|{{Error|fail|#Errors}} default error raised by this function.
 
|}
 
|}
  
Line 146: Line 169:
 
==Query==
 
==Query==
  
<pre class='brush:xquery'>
+
<pre lang='xquery'>
 
module namespace test = 'http://basex.org/modules/xqunit-tests';
 
module namespace test = 'http://basex.org/modules/xqunit-tests';
  
Line 185: Line 208:
 
    
 
    
 
(:~ Function demonstrating an unexpected success. :)
 
(:~ Function demonstrating an unexpected success. :)
declare %unit:test("expected", "FORG0001") function test:unexpected-success() {
+
declare %unit:test("expected", "err:FORG0001") function test:unexpected-success() {
 
   ()
 
   ()
 
};
 
};
 
    
 
    
 
(:~ Function demonstrating an expected failure. :)
 
(:~ Function demonstrating an expected failure. :)
declare %unit:test("expected", "FORG0001") function test:expected-failure() {
+
declare %unit:test("expected", "err:FORG0001") function test:expected-failure() {
 
   1 + <a/>
 
   1 + <a/>
 
};
 
};
Line 214: Line 237:
 
==Result==
 
==Result==
  
<pre class='brush:xml'>
+
<pre lang="xml">
 
<testsuites time="PT0.256S">
 
<testsuites time="PT0.256S">
 
   <testsuite name="file:///C:/Users/user/Desktop/test.xqm" time="PT0.212S" tests="8" failures="4" errors="1" skipped="1">
 
   <testsuite name="file:///C:/Users/user/Desktop/test.xqm" time="PT0.212S" tests="8" failures="4" errors="1" skipped="1">
Line 256: Line 279:
 
! width="110"|Code
 
! width="110"|Code
 
|Description
 
|Description
|-
+
|- valign="top"
|{{Code|UNIT0001}}
+
|{{Code|fail}}
 
|An assertion failed, or an error was raised.
 
|An assertion failed, or an error was raised.
|-
+
|- valign="top"
|{{Code|UNIT0002}}
+
|{{Code|no-args}}
 
|A test function must have no arguments.
 
|A test function must have no arguments.
|-
+
|- valign="top"
|{{Code|UNIT0003}}
+
|{{Code|private}}
|A test function is not public.
+
|A test function must not be private.
|-
 
|{{Code|UNIT0004}}
 
|An annotation was declared twice.
 
|-
 
|{{Code|UNIT0005}}
 
|An annotation has invalid arguments.
 
|-
 
|{{Code|UNIT0006}}
 
|A test function returns items.
 
 
|}
 
|}
  
 
=Changelog=
 
=Changelog=
 +
 +
;Version 9.0
 +
 +
* Updated: error codes updated; errors now use the module namespace
 +
 +
;Version 8.0.2
 +
 +
* Updated: (expected) errors are compared by QNames instead of local names (including namespaces).
  
 
;Version 8.0
 
;Version 8.0
* Added: [[#unit:fail|unit:fail]], 0-argument signature.
+
 
 +
* Deleted: {{Code|UNIT0006}} (ignore results returned by functions).
 +
* Added: {{Function||unit:fail}}, 0-argument signature.
 
* Updated: the info argument of functions can now be an arbitrary item.
 
* Updated: the info argument of functions can now be an arbitrary item.
 
* Updated: infos are now represented in an <code>info</code> child element.
 
* Updated: infos are now represented in an <code>info</code> child element.
* Updated: [[#unit:before|unit:before]] and [[#unit:after|unit:after]] can be extended by a filter argument.
+
* Updated: {{Function||unit:before}} and {{Function||unit:after}} can be extended by a filter argument.
  
 
;Version 7.9
 
;Version 7.9
  
 
* Added: TEST command
 
* Added: TEST command
* Removed: [[#unit:test|unit:test]], [[#unit:test-uris|unit:test-uris]]
+
* Removed: {{Function||unit:test}}, {{Function||unit:test-uris}}
  
 
;Version 7.8
 
;Version 7.8
  
* Added: [[#unit:assert-equals|unit:assert-equals]]
+
* Added: {{Function||unit:assert-equals}}
 
* Updated: enhanced test report output
 
* Updated: enhanced test report output
  
 
This module was introduced with Version 7.7.
 
This module was introduced with Version 7.7.
 
[[Category:XQuery]]
 

Latest revision as of 16:19, 7 December 2023

This XQuery Module contains annotations and functions for performing XQUnit tests.

Introduction[edit]

The more complex a software application grows, the more error-prone it gets. This is why testing frameworks have been developed, which provide a standardized, automated way of testing software. The XUnit frameworks (such as SUnit or JUnit) allow testing of atomic units of a program, such as single functions and algorithms.

This module borrows heavily from the existing frameworks: it provides various annotations for testing XQuery functions. Unit functions are provided to assert the validity of arbitrary conditions expressed in XQuery and to raise errors whenever a condition is not satisfied. Some additional functions exist to run all unit tests of the current module or a set of specified library modules.

Usage[edit]

Tests are started via the TEST command. It compiles all XQuery modules in a given file or directory and runs all functions that are annotated with %unit:test. A test report is generated and returned, which resembles the format returned by other xUnit testing frameworks, such as the Maven Surefire Plugin (see below).

Conventions[edit]

All annotations, functions and errors in this module are assigned to the http://basex.org/modules/unit namespace, which is statically bound to the unit prefix.

Annotations[edit]

unit:test[edit]

Syntax %unit:test
%unit:test("expected", CODE)
Summary With this annotation, a function can be marked as unit test. It will be evaluated if a test report is created for the module in which this function is located.
error can be supplied as additional string argument. It is followed by CODE, which must be a valid EQName string. If the function expression does not raise that error, the test will fail.
Examples
  • The following test will be successful, as it does nothing (and, hence, nothing wrong):
declare %unit:test function local:void() { () };
  • The following test will be successful, as the function body will raise err:XPTY0004:
declare %unit:test('expected', "err:XPTY0004") function local:add() {
  123 + 'strings and integers cannot be added'
};

unit:before[edit]

Syntax %unit:before
%unit:before(FUNCTION)
Summary A function decorated with this annotation will be evaluated before each unit test as a separate transaction.
FUNCTION can be supplied as additional argument. It must be a valid EQName string. If specified, the function will only be evaluated before a function with the given name is tested. This extension is e. g. helpful if the results of updates need to be tested.
Examples
  • The first function will be evaluated before the actual test:
declare %updating %unit:before("local:check") function local:before-check() {
  db:create('test-db')
};
declare %updating %unit:test function local:check() {
  unit:assert(db:exists('test-db'))
};

unit:after[edit]

Syntax %unit:after
%unit:after(FUNCTION)
Summary A function decorated with this annotation will be evaluated after each unit test as a separate transaction.
FUNCTION can be supplied as additional argument. It must be a valid EQName string. If specified, the function will only be evaluated after a function with the given name is tested.

unit:before-module[edit]

Syntax %unit:before-module
Summary If a function is decorated with this annotation, it will be evaluated before all unit tests in the current module as a separate transaction.

unit:after-module[edit]

Syntax %unit:after-module
Summary If a function is decorated with this annotation, it will be evaluated after all unit tests in the current module as a separate transaction.

unit:ignore[edit]

Syntax %unit:ignore
%unit:ignore(MESSAGE)
Summary If a function is decorated with this annotation, it will temporarily be ignored by the test suite runner.

Functions[edit]

unit:assert[edit]

Signature
unit:assert(
  $test  as item()*,
  $info  as item()   := ()
) as empty-sequence()
Summary Asserts that the effective boolean value of the specified $test is true and returns an empty sequence. Otherwise, raises an error. The effective boolean value of an expression can be explicitly computed by using the fn:boolean function.
The default failure message can be overridden with the $info argument.
Errors fail: the assertion failed, or an error was raised.

unit:assert-equals[edit]

Signature
unit:assert-equals(
  $returned  as item()*,
  $expected  as item()*,
  $info      as item()   := ()
) as empty-sequence()
Summary Asserts that the specified arguments are equal according to the rules of the fn:deep-equal function. Otherwise, raises an error.
The default failure message can be overridden with the $info argument.
Errors fail: the assertion failed, or an error was raised.

unit:fail[edit]

Signature
unit:fail(
  $info  as item()  := ()
) as empty-sequence()
Summary Raises a unit error. The default failure message can be overridden with the $info argument.
Errors fail: default error raised by this function.

Example[edit]

The following XQUnit module tests.xqm contains all available unit annotations:

Query[edit]

module namespace test = 'http://basex.org/modules/xqunit-tests';

(:~ Initializing function, which is called once before all tests. :)
declare %unit:before-module function test:before-all-tests() {
  ()
};
  
(:~ Initializing function, which is called once after all tests. :)
declare %unit:after-module function test:after-all-tests() {
  ()
};
  
(:~ Initializing function, which is called before each test. :)
declare %unit:before function test:before() {
  ()
};
  
(:~ Initializing function, which is called after each test. :)
declare %unit:after function test:after() {
  ()
};
  
(:~ Function demonstrating a successful test. :)
declare %unit:test function test:assert-success() {
  unit:assert(<a/>)
};
  
(:~ Function demonstrating a failure using unit:assert. :)
declare %unit:test function test:assert-failure() {
  unit:assert((), 'Empty sequence.')
};
  
(:~ Function demonstrating a failure using unit:assert-equals. :)
declare %unit:test function test:assert-equals-failure() {
  unit:assert-equals(4 + 5, 6)
};
  
(:~ Function demonstrating an unexpected success. :)
declare %unit:test("expected", "err:FORG0001") function test:unexpected-success() {
  ()
};
  
(:~ Function demonstrating an expected failure. :)
declare %unit:test("expected", "err:FORG0001") function test:expected-failure() {
  1 + <a/>
};
  
(:~ Function demonstrating the creation of a failure. :)
declare %unit:test function test:failure() {
  unit:fail("Failure!")
};
  
(:~ Function demonstrating an error. :)
declare %unit:test function test:error() {
  1 + <a/>
};
  
(:~ Skipping a test. :)
declare %unit:test %unit:ignore("Skipped!") function test:skipped() {
  ()
};

By running TEST tests.xqm, the following report will be generated (timings may differ):

Result[edit]

<testsuites time="PT0.256S">
  <testsuite name="file:///C:/Users/user/Desktop/test.xqm" time="PT0.212S" tests="8" failures="4" errors="1" skipped="1">
    <testcase name="assert-success" time="PT0.016S"/>
    <testcase name="assert-failure" time="PT0.005S">
      <failure line="30" column="15">
        <info>Empty sequence.</info>
      </failure>
    </testcase>
    <testcase name="assert-equals-failure" time="PT0.006S">
      <failure line="35" column="22">
        <returned item="1" type="xs:integer">9</returned>
        <expected item="1" type="xs:integer">6</expected>
        <info>Item 1: 6 expected, 9 returned.</info>
      </failure>
    </testcase>
    <testcase name="unexpected-success" time="PT0.006S">
      <failure>
        <expected>FORG0001</expected>
      </failure>
    </testcase>
    <testcase name="expected-failure" time="PT0.004S"/>
    <testcase name="failure" time="PT0.004S">
      <failure line="50" column="13">
        <info>Failure!</info>
      </failure>
    </testcase>
    <testcase name="error" time="PT0.004S">
      <error line="55" column="6" type="FORG0001">
        <info>Cannot cast to xs:double: "".</info>
      </error>
    </testcase>
    <testcase name="skipped" skipped="Skipped!" time="PT0S"/>
  </testsuite>
</testsuites>

Errors[edit]

Code Description
fail An assertion failed, or an error was raised.
no-args A test function must have no arguments.
private A test function must not be private.

Changelog[edit]

Version 9.0
  • Updated: error codes updated; errors now use the module namespace
Version 8.0.2
  • Updated: (expected) errors are compared by QNames instead of local names (including namespaces).
Version 8.0
  • Deleted: UNIT0006 (ignore results returned by functions).
  • Added: unit:fail, 0-argument signature.
  • Updated: the info argument of functions can now be an arbitrary item.
  • Updated: infos are now represented in an info child element.
  • Updated: unit:before and unit:after can be extended by a filter argument.
Version 7.9
Version 7.8

This module was introduced with Version 7.7.