Galtman, Amanda. “Accumulators in XSLT and XSpec: Developing, Debugging, and Testing XSLT 3 Accumulators.” Presented at Balisage: The Markup Conference 2023, Washington, DC, July 31 - August 4, 2023. In Proceedings of Balisage: The Markup Conference 2023. Balisage Series on Markup Technologies, vol. 28 (2023). https://doi.org/10.4242/BalisageVol28.Galtman01.
Balisage: The Markup Conference 2023 July 31 - August 4, 2023
Balisage Paper: Accumulators in XSLT and XSpec
Developing, Debugging, and Testing XSLT 3 Accumulators
Amanda Galtman is an independent XML software developer who contributes to the
XSpec infrastructure and a couple of other open source projects. She writes
about XSpec at https://medium.com/@xspectacles. Previously, she was an XML
software developer at MathWorks.
Accumulators in XSLT 3, while not necessarily an everyday staple of XSLT code
bases, can provide elegant solutions to certain challenges in both streaming and
non-streaming XSLT applications. The debugging and testing techniques you might use
for accumulators are a little different from techniques you use for templates and
functions. After briefly reviewing what makes accumulators special and powerful,
this paper describes and compares several debugging and testing techniques for
accumulators. Whether you need a near-term bug diagnosis or an automated XSpec test
for long-term code health, you'll learn ways to access, understand, and maintain
your accumulator's behavior.
XSLT version 3 introduces the accumulator, a special and elegant
way you can associate data with nodes of a tree and retrieve that data as needed.
To
incorporate an accumulator into your XSLT stylesheet, you design the data association
logic to serve your needs, implement the design using one or more accumulator
rules, and retrieve the data wherever you need it using dedicated
accumulator functions. At some point in your work, you will
likely want to examine or debug the accumulator's behavior, checking interactively
that
your implementation matches the design you intended. If your XSLT code is significant
or
long-lasting, you might want to write tests that check the behavior in an automated
way.
The debugging and testing techniques you might use for accumulators are a little
different from techniques you use for templates and functions. My purpose here is
to
make debugging and testing accumulators easier for you, so you can use accumulators
more
efficiently and effectively.
Background About Accumulators
If XSLT accumulators are new to you, first pause to consider the word "accumulator."
This word suggests the XSLT accumulator's capability of computing a value progressively,
especially during streamed processing of an XML tree that may be too large to fit
in
memory. In some cases, "accumulate" aptly describes the task the accumulator performs.
However, try not to take the word "accumulator" too literally, because some XSLT
accumulators, instead of collecting more and more things, behave more like a:
Counter, potentially resettable at relevant spots in the tree
Stack or queue, which can add and remove items
Tracker of some kind of state, based on patterns of interest
Keeping your mind open to these other roles can help you identify when an accumulator
is a good fit for a problem you need to solve. Remembering that the feature's name
describes only a subset of functionality is similar to how you remember that XSLT
"stylesheets" can perform many useful tasks unrelated to styling.
When you first hear that an accumulator associates user-defined data with nodes, you
might think it is like calling a user-defined function on nodes to compute and retrieve
data. While you can use accumulators in that manner, accumulators also have some
distinctive capabilities. Let's look at them.
Inertia
Suppose you're scanning the body of an HTML document from top to bottom and for
any paragraph, you want to retrieve its section depth — that is, the level of the
nearest heading that the paragraph is underneath. When you see an
<h1> element, you know that everything afterward is at depth 1
until you reach an <h2> element, at which point everything afterward
is at depth 2, and so on. The headings are significant because that's where the
depth value might change. In between headings, the depth stays the same.
The dedicated functions that retrieve data from an accumulator natively know how
to find and return the last computed value. If you implement the depth computation
using an accumulator, you don't have to write code to traverse the document in
reverse document order, in search of prior headings. Your accumulator declaration
concisely focuses on the headings alone. The accumulator value remains unchanged
until a heading triggers a match against one of the accumulator rules.
The XSLT code snippet above declares an accumulator that associates the heading
level with each heading element up to <h3>. In the thought
experiment where you're scanning the sample HTML body shown, when you see an
<h1> element, the match="h1" accumulator rule in
XSLT says the accumulator value is 1. When you see the next HTML
paragraph and the one after that, inertia says the heading depth is the most recent
accumulator value, which is 1. The XSLT code snippet has no accumulator
rule with match="p" that says to keep using the last value, because
that behavior comes for free.
Also, notice that this example expresses accumulator rules for heading elements
while retrieving the depth value at paragraph elements. The nodes where you
explicitly compute associated data don't have to be the same as the nodes for which
you want to retrieve data. Thanks to the implicit inertia
behavior, you can retrieve the data for any context node (except that the
accumulator does not associate values with attribute nodes or namespace
nodes).
Running Counts and Accumulation
Not only can you access the last computed value, but you can also use it to
compute a new value. Within an accumulator rule, you can reference the special
variable named $value, which stores the latest value of the accumulator
before the rule recomputes the value. This capability is useful if you want to
compute a running total of HTML headings seen so far, up to any given context
paragraph.
More generally, the $value variable helps you implement running
statistics or accumulate data in a stack or queue. The $value variable
can also be handy for debugging using XSLT
Messages.
Note
In this example, the computation of new accumulator value in
<xsl:accumulator-rule> uses a child
<xsl:sequence> element rather than a select
attribute. Both ways work, but we will see in XSLT
Messages that using a child element here is a bit more
debugging-friendly.
Start/End Distinction in Accumulator Rules
To illustrate another important feature of accumulators, imagine writing XSLT to
style an XML document that includes some content that should be kept internal only,
such as author remarks and content intended for a future release. You must not only
suppress the internal content from the main flow but also ignore this content while
generating things like tables of content or lists of cited bibliographic entries.
Content might even be internal-only for multiple reasons, such as an author remark
nested inside a future-release subsection.
Let's design an accumulator that tells us when a particular XML element is
internal-only due to its own markup or its ancestry. For this example, suppose the
author remarks use an XML element named <remark> and the
future-release content is any element that has an attribute
condition="future".
Imagine a television news program whose scrolling chyron shows the serialized XML
content shown above. The significant points as far as internal-only status goes
are:
Start tags of either <remark> or any element with
condition="future". When you see such start tags, an
internal-only region begins or deepens.
End tags of either <remark> or any element with
condition="future". When you see such end tags, an
internal-only region ends or becomes shallower. At this point, you have
already seen all the element's descendants, an observation whose relevance
will appear soon.
These significant points become the focus of our accumulator rules. The
<xsl:accumulator-rule> element supports an attribute named
phase. This attribute enables you to construct different
accumulator values based on whether the XSLT engine traversing the tree has first
reached the node before visiting its descendants or is revisiting the node after
visiting all its descendants. In this way, a node has two accumulator values, not
one, even if the node has no descendants. The values of phase are
"start" (default) and "end". The distinction is
exactly what we need for the accumulator in this example. Here are two
approaches:
The internal-depth accumulator records the depth of
internal-only regions. For instance, this accumulator associates the integer
value 2 with the text node having content "For next year,
maybe", and the nonzero value indicates that this text node is
internal-only. The text node having content "Additional content" is
associated with the value 1, which is also nonzero and
indicates internal-only status. By contrast, the text node having content
"Sample external content" is associated with the value
0.
The internal-elem accumulator behaves like a stack, recording
sequences of internal-only element names (with the most recent at the
beginning). For instance, this accumulator associates the sequence
('remark', 'section') with the text node having content
"For next year, maybe", and the nonempty value indicates that this text node
is internal-only.
Recording element names might seem like overkill, but it depends on the
situation. The additional information beyond depth could be helpful for
debugging, testing, or hypothetical downstream processing that depends on
the specific reason why a node is internal-only.
Before/After Distinction in Accumulator Functions
So far, we have glossed over the mechanism for retrieving the accumulator value
associated with a particular node. XSLT 3.0 defines functions that return the value
that a specified accumulator associates with the context node. Just as accumulator
rules distinguish between the situations before and after the XSLT processor has
visited descendants when assigning accumulator values, the retrieval capabilities
distinguish when retrieving accumulator values. That is why XSLT 3.0 provides not
one accumulator function but two: accumulator-before and
accumulator-after. For instance, in the preceding section's
examples,
/section/remark/accumulator-before('internal-depth') returns
1 because the phase="start" rule has just
incremented the accumulator value.
/section/remark/accumulator-after('internal-depth') returns
0 because the phase="end" rule has just
decremented the accumulator value.
Both functions return the same value if the context node is
/section/para. Neither this element nor its descendants
match any accumulator rule in the 'internal-depth' accumulator
declaration. As a result, inertia preserves the accumulator value while the
processor visits this element, its descendants, and this element
again.
Both functions return the same value if the context node is a text node. A
text node has no descendants, and text nodes do not match any accumulator
rule in the 'internal-depth' accumulator declaration.
Therefore, inertia preserves the accumulator value while the processor
visits a given text node twice.
The before/after distinction in the two accumulator functions does not always
correspond to start/end phases in accumulator rules; the point is how the context
node and its descendants relate to the accumulator rules. For instance, the XSLT 3.0
specification includes the following accumulator example that counts words [XSLT].
If such an accumulator operates on an HTML document,
/html/accumulator-before('word-count') returns zero because no text
nodes have been visited yet. By contrast,
/html/accumulator-after('word-count') returns the total number of
words in the HTML document because all text nodes containing words have been
visited.
Preventing Bugs
From the last section, you know that an accumulator associates data with tree nodes
before and after visiting their descendants, and it holds the last data value until
the
next visit to a node that matches an accumulator rule. Now, let's start to look at
ways
you can use accumulators productively and avoid mistakes. It's better to prevent a
mistake than to discover, diagnose, and fix it. This section contains tips for
preventing accumulator-related bugs.
When Not to Use an Accumulator
One potential mistake that's best to detect as early as possible is trying to use
an accumulator when it's not suitable for the situation. For example:
Inspired by an accumulator's counting behavior, you might try to make an
accumulator count how many times your XSLT code processes a certain XML
element. However, that would be a design mistake, because the tree traversal
for accumulator computations is not the same as how templates or functions
access the tree. An accumulator can say, "This is the third title I've seen
so far in this document" but not, "This is the third time I've seen this
title so far during processing: once when creating the table of contents,
once when rendering the heading, and once when rendering a hyperlink that
points here."
Suppose the original document order is not that significant in an XML
document you are processing, such as when the XSLT sorts subsections of a
topic. An accumulator might be a good fit either for a task unaffected by
the sorting or if the accumulator operates on nodes of an already-sorted
tree. On the other hand, you might decide that storing a sorted tree would
add too much complexity and a non-accumulator solution would be
better.
Suppose you are processing documents that can refer to other documents,
and you need to track the document URIs as you follow the references.
Inspired by mentions of accumulators that implement stacks, you might try to
use an accumulator to track the URIs. However, if "following" the references
means applying XSLT to the referenced documents using the XPath
transform function, an accumulator might not be a suitable
solution. I am not aware of a capability for passing an accumulator and its
values across an XSLT stylesheet boundary via the
transform function. The case where I saw this situation
implemented the stack using a tunnel parameter instead of an
accumulator.
This situation contrasts with one that came up on an XSL mailing list
recently, where a user wanted to accumulate data across topics referenced by
a DITA map [M]. This situation used doc
rather than transform to access the referenced documents, and
an accumulator worked fine because it was crossing a tree boundary, not an
XSLT stylesheet boundary.
An accumulator might not be a good substitute for a user-defined function
on attribute nodes, because accumulators do not associate values with
attribute nodes. If you consider the desired behavior in terms of element
nodes, you can judge whether an accumulator that matches element nodes and
computes its value related to an element's attributes has any advantages in
clarity or functionality compared to an ordinary function.
Avoiding Coding Mistakes
Suppose you've determined that an accumulator is a suitable solution to your
problem, and you're ready to start coding it. The following list can help you avoid
potential coding errors:
As you think about the match patterns for which you will create
accumulator rules, think about how the patterns might occur in the actual
trees you need to process. Plan for potential interactions among
patterns:
Avoid ambiguous rule matches. If a node matches multiple patterns,
using separate accumulator rules for them will lead to a warning
about the ambiguous rule match, and the accumulator rule that the
processor applies may or may not be what you want. Avoiding
ambiguous rule matches is why the 'internal-depth'
example in Figure 5 uses the same
accumulator rule for both <remark> elements and
elements having the condition="future"
attribute.
Note
Accumulator rules do not support a priority
attribute, as template rules do.
Consider nesting. If a node can match a pattern in a nested
arrangement, think through the possibilities and eventually try them
first-hand, to make sure your design is sound. In the
'internal-depth' example, the potential for nested
internal-only patterns is why the accumulator computes the numeric
depth instead of returning a Boolean value (i.e., a node is or is
not internal-only).
If your design requires resetting the accumulator value in some way or
shedding data that was accumulated earlier in the traversal, remember to
implement the reset and do it correctly for your situation. For a resettable
counter, consider whether you should reset the value to zero or decrement it
by one. For a stack, consider whether you should empty the stack or remove
one value from the beginning or the end.
Make sure your accumulator is applicable to the tree
where you want to attach the data. Details of your situation determine
whether the applicability comes for free, and the rules are in the Applicability of Accumulators section of the XSLT 3.0
specification. Sometimes, you need to add a use-accumulators
attribute that lists either accumulator names or the token
#all. For example, <xsl:mode
use-accumulators="#all"/> makes all accumulators applicable to
documents containing nodes in the initial match selection.
Debugging Techniques
Suppose you've created an accumulator and you need some visibility into its behavior.
Maybe you want to check that your in-progress work is on the right track. Maybe you
have
all the pieces in place, but something doesn't work as expected.
Choose a tree that you'd like to have the accumulator associate data with, such as
a
sample document or a tree whose transformation exhibits a problem. While accumulators
can operate on trees that aren't rooted at document nodes, some debugging techniques
are
more straightforward if the tree is captured in an XML file.
This section describes the following approaches to debugging or viewing values of
your
accumulator:
In Oxygen XML Editor, the XSLT debugger provides visibility into many aspects of
an evolving transformation, including the execution stack and variables. This
debugger can show you accumulator values, too (at least for non-streaming
transformations). After you select XML and XSLT files for the debugging
configuration, place one or more breakpoints at templates, functions, their
contents, or lines in the source document. Remember, though, that the XSLT
processor's tree traversal aimed at computing accumulator values is not the same as
the process of evaluating templates and functions. Placing an Oxygen debugger
breakpoint at or within <xsl:accumulator> is not particularly useful.[1]
What is useful for viewing accumulator values is the
XPath Watch (XWatch) view in the Oxygen
debugger [XW]. Here are two approaches you can use separately or
together:
Watch the expressions accumulator-before(...) and
accumulator-after(...), providing the name of your
accumulator as the input argument to these functions. When the
transformation is paused at a breakpoint, the XWatch pane shows the
accumulator values at the context node. You can also select a row in the
XWatch pane and view the value in a tree format, in the Nodes/Values Set pane. As you step through the
transformation and the context node changes, the XWatch pane changes the
values it shows because the accumulator functions operate at the context
node.
Watch an expression that explicitly sets the context node, such as
//body/h1[1]/accumulator-before('word-count'). This
approach enables you to zero in on any node of interest, and it works at any
XSLT breakpoint that the execution reaches, even if the node of interest
hasn't yet become the context node for any template. To see multiple nodes'
accumulator values together, you can use multiple rows in the XWatch view or
specify a path that yields a node sequence of any length (e.g., remove the
[1] predicate in the last expression).
The next screen captures illustrate panes of the Oxygen debugger when paused at an
XSLT breakpoint. At this breakpoint, the first <remark> element in
the XML source document is the context node. In the XWatch pane, the first row shows
the accumulator's value at the subsection remark's text content. The second and
third rows shows the accumulator's values at the context node (first
<remark> element) after and before, respectively, the
accumulator's document traversal has reached the context node's descendants.
Trace from Saxon (PE or EE)
If you have a license for Saxon-PE or Saxon-EE, you can configure your accumulator
to emit trace messages during the transformation, whenever the value changes. Each
message includes the accumulator name, the word BEFORE or AFTER, an XPath expression
for the node that matched the accumulator rule, and the new value of the accumulator
for that node. To configure your code in this way, modify the
<xsl:accumulator> element by setting the
saxon:trace attribute to the value 1,
true, or yes [S]. Remember to
declare the namespace of this attribute,
xmlns:saxon="http://saxon.sf.net/". (Consider including the prefix
in the value of an exclude-result-prefixes attribute of
<xsl:stylesheet>.)
Sample output looks like the following.
Report of Accumulator Values
An option similar to trace messages is to create a report of accumulator values
associated with a tree. The xslt-accumulator-tools repository on
GitHub, which I created to complement this paper, includes an accumulator report
generator named acc-reporter.xsl [G]. The rest of
this section is about this report generator, although you can always write your own
XSLT code to produce a custom-designed report.
The premise underlying acc-reporter.xsl is that you have some
accumulator declaration in an XSLT file, have one or more documents in XML files,
and want to see the values the accumulator associates with the nodes in the
documents.
For a given accumulator and XML document, this tool produces an HTML report having
two sections. The first section lists the following background information, for
traceability:
URI of the XML document.
URI of the XSLT file containing the accumulator declaration.
Code in the <xsl:accumulator> element. Recording this code
in the HTML report can be useful if you iteratively generate reports while
modifying your accumulator definition in the XSLT file.
The next section is a two-column table, where:
The first column shows element tags and non-element nodes from the XML
document. For elements, separate listings for start and end tags distinguish
between the accumulator's values before and after visiting the element's
descendants. Global parameters in acc-reporter.xsl enable you
to control whether the report suppresses whitespace-only text nodes and at
what length the report truncates content of text and comment nodes.
Truncation is meant to help you focus on the nodes and values, not
voluminous text content that would require you to scroll a lot.
The second column shows associated accumulator values. To reduce clutter,
the report populates this column only for the document node's values and
wherever an accumulator changes its value.
For specific instructions on generating accumulator reports using a system command
or an Oxygen transformation scenario, see the xslt-accumulator-tools
GitHub repository. [G]
XSLT Messages
The <xsl:message> instruction is a common debugging approach to
get information about a transformation running outside a debugger environment. If
you want the accumulator computations themselves to produce messages, follow these
guidelines:
Insert <xsl:message> as a child or other descendant of
<xsl:accumulator-rule>, not as a child of
<xsl:accumulator>.
Where <xsl:message> is inside
<xsl:accumulator-rule>, the latter should contain a
sequence constructor, not a select attribute. If you wrote the
accumulator rule with a select attribute and want to insert
<xsl:message>, modify the rule as in the following
example.
In your message content, don't call the accumulator-before or
accumulator-after functions to refer to the same
accumulator whose declaration contains the message. If you try that, you
will get an error message that says the accumulator requires access to its
own value.
You can, however, use accumulator-before or
accumulator-after functions to refer to a different
accumulator. Doing so is helpful if you split a complicated computation
across multiple accumulators that build on each other in a non-circular
way.
You can use the special variable named $value in the message
content. This variable stores the value of the accumulator
before evaluating the content of this accumulator
rule.
As usual, you can insert <xsl:message> inside templates and
functions. Here are some tips for reporting on accumulator values in messages within
templates and functions:
In your message content, you can call the accumulator-before
and accumulator-after functions to refer to an applicable
accumulator.
In your message content, don't use the special $value
variable, which is accessible only within
<xsl:accumulator-rule>.
The <xsl:message> instruction doesn't have to be in a
template rule for the kind of node whose associated accumulator value you
want to report. You can insert the message anywhere convenient that (a) will
be reached during the transformation and (b) can assume your node of
interest as the context node. (Requirement (b) is relevant for stream
processing.) Within your message, you can change the context node to your
node of interest and then call the accumulator function.
The ability to change context at will is convenient if you want to report
accumulator values for multiple tree nodes and prefer to consolidate
temporary debugging code in as few locations in your XSLT stylesheet as
possible.
The next code sample illustrates changing the context node and calling accumulator
functions, in a non-streaming application.
Where you place the <xsl:message> instructions can depend on your
preferences as well as your immediate goal or context in the development process.
For instance, if you have just written an accumulator declaration and want to check
that the rules are correct before using accumulator values somewhere else in the
stylesheet, you might favor messages within the accumulator rules because that's
where your attention is. On the other hand, if you are investigating a bug that
involves a particular template or function, that may be a natural place for messages
about accumulator values, variable values, and other data to aid your
investigation.
Testing Techniques
Debugger tools, trace messages, reports, and <xsl:message> outputs
serve useful purposes that are typically transient purposes. In production, you don't
run your transformation in a debugger and you are likely to suppress or remove code
that
makes logs unwieldy. There's nothing wrong with the transience of the debugging
techniques described earlier; you solve your immediate problem, remove extraneous
logging, and move on. At the same time, you might gain value from automated tests
that
persist even after you have finished checking an accumulator rule interactively or
fixing a bug. This section illustrates ways to test behavior of some accumulators
using
XSpec [XS], which is an open-source software product for testing XSLT,
XQuery, and Schematron code. This section assumes prior familiarity with XSpec.
Motivations for Testing Accumulators
First, let's discuss why you might want to test an accumulator in an automated
way. In general, automated testing can help you maintain your code's correct
behavior as you add features, fix bugs, and refactor code. The time you invest in
creating automated tests pays dividends when you can change XSLT code quickly with
confidence that you haven't broken anything (or that you've given the breakage
careful consideration and documented it in release notes for end users, if
appropriate). Unless a piece of XSLT code is trivial or has a very short life span
from the start of development to the end of usage, you should consider whether and
how it should have automated tests. If you're already familiar with XSpec tests for
templates and functions, you might still wonder why an accumulator deserves special
testing attention. Consider whether these reasons pertain to any of your accumulator
code:
One way to begin an investigation of a bug is to write an XSpec test that
reproduces the bug. Running the test repeatedly as you modify the code or
insert debugging messages can help you diagnose and fix the bug efficiently.
After you have fixed the bug, you might as well keep the test to guard
against recurrences of the bug. This bug-fixing workflow can apply to code
whether or not it involves an accumulator. When you suspect the cause of an
XSLT bug is in an accumulator declaration, you might find it handy to know
how to make a test that focuses on that declaration.
Some accumulators are complicated. Maybe you have one accumulator whose
computations depend on another accumulator. Maybe your accumulator operates
on XML documents that can exhibit a variety of pattern combinations,
including important edge cases whose support must be maintained. Greater
complexity in the accumulator is likely to provide a greater return on your
test writing investment.
If the templates and functions that reference your accumulator are large
or complicated themselves, you might like the modularity of testing the
accumulator in isolation from them.
XSpec Test Importing XSLT Under Test
By default, an XSpec test of XSLT code gets compiled into a stylesheet that
imports the XSLT code you are testing. Because the import operation mingles your
XSLT code with the compiled test logic, the testing logic can access your XSLT
accumulators. If your XSpec file uses this default import mechanism (i.e., you are
not using the nondefault run-as="external" testing mechanics due to one
of the use cases described in [E]), you can use the examples in
this section.
Compared to the example in XSpec Test Running XSLT
as External Transformation, the examples in this section are more concise
and do not require you to augment your XSLT stylesheet by importing wrapper
functions.
Dedicated Test Scenario
The first example illustrates an XSpec test file that focuses specifically on
testing an XSLT accumulator.
Relevant points of this sample test include the following:
At the top of the XSpec file, the <x:description>
element includes run-as="import". (This setting is
equivalent to omitting the run-as attribute, because
"import" is the default value.) Making the compiled
XSpec test import your XSLT stylesheet makes the accumulator available
to your <x:expect> assertions.
Note
If you specify run-as="external", then this
accumulator testing technique will not work. See XSpec Test Running XSLT as External
Transformation for an alternate testing technique. For
more information about external transformations in XSpec, see [E].
The scenario defines a tree with which you'd like the accumulator to
associate data. It is convenient, though not strictly necessary, to
store the tree in an XSpec variable. The name of the variable is not
critical, nor is the choice to read it from a separate XML file.
The scenario calls a function using <x:call>, to avoid
an error from the XSpec compiler. The specific function (in this case,
the standard true function in XPath) and its return value
are not important in this scenario.
The scenario has <x:expect> elements whose purpose is
to check that the accumulator has the expected values at certain nodes
of the tree. The label can be whatever descriptive text you want. The
test attribute establishes a context node and calls
either accumulator-before or
accumulator-after. The select attribute
provides the expected value of the specified accumulator function at
that context node.
<x:expect
label="At start of subsection remark, 2 element names in stack"
test="$myv:tree/section/section/remark/accumulator-before('internal-elem')"
select="('remark', 'section')"/>
If you prefer, you can omit the select attribute and
write a Boolean expression in the test attribute, as in the
following variation on the check for an empty sequence.
<x:expect
label="At end of document, stack is empty"
test="empty(exactly-one($myv:tree)/accumulator-after('internal-elem'))"/>
If the expected accumulator value is a node, you can provide it as a
child of the <x:expect> element instead of using the
select attribute.
If the expected accumulator value is an empty sequence, you should make
sure the accumulator function is operating on a nonempty context node so
the function does not return an empty sequence for the wrong reason. By
applying the exactly-one or one-or-more
function to the node(s) at which you want to call an accumulator
function, you can rule out an empty context node.
Notice that a single scenario can check the accumulator values at multiple
nodes of the tree. You can even have a single scenario that checks accumulator
values on multiple trees (such as $myv:tree and
$myv:tree2 variables), though you might find it clearer to have
a separate scenario for each tree.
Accumulator Expectations Within Scenarios Serving Other Purposes
As an alternative to creating dedicated test scenarios for accumulators, you
can check accumulator values in test scenarios that primarily serve other
purposes. You might choose this approach for any of these reasons:
You find it natural or more maintainable to check the accumulator
values near where you test the templates or functions that call
accumulator-before or
accumulator-after.
The way you organize your XSpec scenarios is not meant to align with
XSLT code units like templates and functions.
Your bug-investigation process began with some existing scenario, and
you see no urgency in refactoring the XSpec test code later.
The next example illustrates a test for a template rule that also includes
checking some expected values of an accumulator.
XSpec Test Running XSLT as External Transformation
When your XSpec test uses the nondefault run-as="external" option,
the compiled test logic invokes the XSLT code you are testing as an external
transformation. This mechanism has benefits, but a drawback is that your XSLT
accumulators are not directly accessible in <x:expect> elements,
while the standard accumulator functions are not directly accessible in
<x:call>. You can still test the accumulators, however, and this
section illustrates one design. The design works in the default
run-as="import" situation, too, so you retain the flexibility to
change run-as later without having to rewrite tests.
At a high level, the arrangement has these parts:
An XSLT module that defines thin wrappers around the standard
accumulator-before and accumulator-after
functions.
In your XSLT stylesheet, an instruction that imports the module containing
wrapper functions.
In your XSpec test file, a dedicated scenario (or descendant scenario of
any depth) for each expected value you want to check for your accumulator.
Each scenario uses <x:call> to call a wrapper function to
retrieve an accumulator value.
Items 1 and 2 make the accumulator functions (indirectly) accessible during XSpec
testing, while item 3 makes your accumulator accessible. Item 3 is less concise than
the examples in XSpec Test Importing XSLT Under
Test, but the underlying capabilities are analogous.
Note
If you prefer not to modify your production XSLT code, an alternative is to
introduce a separate "test harness" XSLT file that includes your production XSLT
code and imports the module containing wrapper functions. In XSpec, point the
x:description/@stylesheet attribute to the test harness rather
than your production XSLT code.
Relevant points of this sample test include the following:
As in the examples in XSpec Test Importing
XSLT Under Test, the scenario defines a tree with which you'd
like the accumulator to associate data.
Each lowest-level scenario uses <x:call> to call a wrapper
function, at:accumulator-before or
at:accumulator-after. The first function parameter is the
context node whose accumulator value you want to retrieve, and the second
parameter is the name of the accumulator.
The scenario has an <x:expect> element that checks the
output of the specified accumulator function. The select
attribute provides the expected value of the accumulator function at that
context node. XSpec compares this expected value with the result of the
<x:call> function call.
<x:expect label="2 internal-only elements in stack"
select="('remark', 'section')"/>
If you prefer, you can omit the select attribute and write a
Boolean expression in the test attribute, as in the following
variation on the check for an empty sequence.
<x:expect label="Empty" test="empty($x:result)"/>
If the expected accumulator value is a node, you can provide it as content
of the <x:expect> element instead of using the
select attribute.
Conclusion
After reviewing distinctive characteristics of XSLT accumulators and looking at some
examples, we explored several debugging and testing techniques for accumulators. The
next table highlights some points of comparison among the techniques discussed. Notice
that the short-term use cases vary, so it's helpful to be familiar with multiple
techniques instead of only one.
Table I
Comparison of Debugging and Testing Techniques
Approach
Additional Software Needed
Basic Procedure
Short-Term Use Case
Long-Term Benefit?
Oxygen debugger
Oxygen (Syncro Soft)
Configure debugger, breakpoints, and XPath watch expressions; run
transformation interactively; analyze interim states
Explore transformation behavior in various ways, including viewing selected
values of accumulator
No
Trace
Saxon PE or EE (Saxonica)
Set saxon:trace attribute; run transformation; analyze log
View values for entire tree
Maybe, if baseline logs are kept for later comparison
Report
XSLT Accumulator Tools, from GitHub
Configure parameters; run transformation; analyze report
View values for entire tree
Maybe, if baseline reports are kept for later comparison
XSLT messages
None
Insert message instructions in accumulator rules, templates, or functions;
run transformation; analyze log
View selected values or identify which rules are being matched
Maybe
XSpec tests
XSpec, from GitHub
Write test scenarios describing expected behavior; run test; check results
and analyze failures, if any
View and verify selected values
Yes
Having more visibility into the data an XSLT accumulator associates with a tree should
help XSLT/XSpec developers work with accumulators more successfully throughout the
software development life cycle.
[1] Also not useful is calling the accumulator-before or
accumulator-after function within the XPath 3.1 field in the Oxygen toolbar in the non-debugging
perspective. You'll get an error message about the function not being found,
because it is available only within an XSLT transformation, not standalone
XPath expressions.