Generating structured text is best done with [CAF::TextRender
][caf_textrender_docs].
This document guides through the usage and testing of CAF::TextRender
and
also describes ncm-metaconfig
, which is the metacomponent build around
CAF::TextRender
.
TODO add correct/final url [caf_textrender_docs]: http://docs-test-caf.readthedocs.org/en/latest/CAF/CAF::TextRender
Basic usage has 2 main modes:
-
generate text : the
CAF::TextRender
instance has auto-stringificationuse CAF::TextRender; my $module = 'mymodule'; my $trd = CAF::TextRender->new($module, $contents, log => $self); print "$trd"; # stringification
-
write text to file : get a
CAF::FileWriter
instance with text fromCAF::TextRender
instanceuse CAF::TextRender; $module = "mymodule"; $trd = CAF::TextRender->new($module, $contents, log => $self); my $fh = $trd->filewriter('/some/path'); die "Problem rendering the text" if (!defined($fh)); $fh->close();
Besides the logger, the 2 main parameters are the module
and the contents
.
The contents is a hash-reference with the data that is used to generate
the text (e.g. from a $cfg->getElement('/some/pan/path')->getTree()
).
The module
is what defines how the text is generated.
It is either one of the following reserved values
- json (using
JSON::XS
) - yaml (using
YAML::XS
), - properties (using
Config::Properties
), - tiny (using
Config::Tiny
), - general (using
Config::General
)
(The builtin modules can have issues with reproducability, e.g. ordering or a default timestamp.)
Or, for any other value, Template::Toolkit
(TT) is used,
and the module
then indicates the relative path of the template to use.
The absolute path of the TT files is determined by 2 optional parameters:
the absolute includepath
(defaults to /usr/share/templates/quattor
)
shouldn't be modified, but the relpath
(defaults to metaconfig
) should.
A module mytest/main
with relpath mycode
will use a
TT file /usr/share/templates/quattor/mycode/mytest/main.tt
.
The relpath
is important for creating the TT files: when the
INCLUDE
directive is used, TT searches starting from the includepath
,
so in this example the main.tt
might look like
[% data.name %]
[% INCLUDE 'shared/data' %]
which will look for the absolute file /usr/share/templates/quattor/shared/data.tt
.
CAF::TextRender
does not allow you to include files from a directory lower then relpath
(e.g. module
../cleverhack
will not work).
Template::Toolkit
is a templating framework
Example template
Hello [% world %]
with content a perl hashref
{ world => 'Quattor' }
will generate
$ perl -e 'use Template; my $tttext="Hello [% world %]\n"; Template->new()->process(\$tttext, { world => "Quattor" });'
Hello Quattor
TODO minimal version
TODO add url with examples
TT can easily generate unwanted/unneeded newlines.
The chomp
behaviour can be summarised as follows
Name | Tag Modifier |
---|---|
NONE | + |
ONE | - |
COLLAPSE | = |
GREEDY | ~ |
Testing the generated text (and in particular the TT files used to generate it)
can be done via regular expressions and e.g. the like
method from Test::More
.
Test::Quattor::RegexpTest
provides an easy way to do this.
A RegexpTest
is a text file with 3 blocks separated by a ---
marker.
The first block is the description, the second block a list of flags (one per line) and the third block has all the regular expressions.
An example RegexpTest looks like
Verify mycode
---
---
^line 1
^line 3
with an empty flags block (using the defaults ordered
and multiline
).
If we create a file src/test/resources/rt_mycode
with this content, we can now test
generated text against this RegexpTest using
use Test::Quattor::RegexpTest;
use CAF::TextRender;
my $module = 'mymodule';
my $trd = CAF::TextRender->new($module, $contents, log => $self);
my $rt = Test::Quattor::RegexpTest->new(
regexp => 'src/test/resources/rt_mycode',
text => "$trd",
);
$rt->test();
With the default flags, each line is compiled as a multiline regular expression and matched against the text.
The matches are also checked if they are ordered. In the example above line 3
is expected to match in the text
following line 1
. But it does not need to be the next line (e.g. there could be a line 2
in between).
Each match is a test and each verification of the ordering also.
TODO add correct/final url
The metaconfig
component together with Test::Quattor::TextRender::Metaconfig
gives a
high-level interface to the previous classes.
Configuration files will be rendered using CAF::TextRender
and one can validate the modules using RegexpTests.
The metaconfig component is a component that has a number of service
s, each service is the file to generate.
The pan configuration of such a service can be
include 'metaconfig/myown/schema';
bind "/software/components/metaconfig/services/{/etc/myown.config}/contents" = my_own_type;
prefix "/software/components/metaconfig/services/{/etc/myown.config}";
"module" = "myown/main";
"contents" = nlist("some", "data");
"daemons" = nlist("myownservice", "reload");
Here /etc/myown.config
is the metaconfig service and the file that will be generated by CAF::TextRender
.
The contents
is the data that is passed to CAF::TextRender
, together with the module
.
The daemons
is an optional nlist of service(s) and their actions.
The actions are handled by CAF::Service
and this is triggered when
the generated text is different from the current one.
Other optional attributes are mode
, owner
, group
, backup
and preamble
(a fixed header).
Start with forking the upstream configuration-modules-core repository,
and clone your personal fork in your workspace
(replace stdweird
with your own github username in the example below).
Also add the upstream
repository (using https
protocol).
GHLOGIN=stdweird
git clone git@github.com:$GHLOGIN/configuration-modules-core.git
cd configuration-modules-core
git remote add upstream https://github.com/quattor/configuration-modules-core
TODO: set PERL5LIB
to the quattor install path
TODO: set QUATTOR_TEST_TEMPLATE_LIBRARY_CORE
to path of a
clone of the template library core repository
(by default, it looks in the parent path of configuration-modules-core
).
Check your environment by running the metaconfig unittests. No tests should fail when the environment is setup properly.
cd ncm-metaconfig
mvn clean test
Pick a good and relevant name for the service (in this case we will add the imaginary example
service).
Set the variable service
in your shell (it is used in further command-line examples).
service=example
Our imaginary example
service requires a text config file with path
/etc/example/exampled.conf
and has following structure
name = {
hosts = server1,server2
port = 800
master = FALSE
description = "My example"
}
where following fields are mandatory:
hosts
: a comma separated list of hostnamesport
: an integermaster
: boolean with possible valuesTRUE
orFALSE
description
: a quoted string
The service has also an optional fields option
, also a quoted string.
Upon changes of the config file, the exampled
service needs to be restarted.
This type of configuration is ideally suited for metaconfig and TT.
Make a new branch where you will work in and that you will use to create the pull-request (PR) when finished
git checkout -b ${service}_service
Create the initial directory structure (from the ncm-metaconfig
path).
cd src/main/metaconfig
mkdir -p $service/tests/{profiles,regexps} $service/pan
Add some typical files (some of the files are not mandatory, but are simply best practice).
cd $service
echo -e "declaration template metaconfig/$service/schema;\n" > pan/schema.pan
echo -e "unique template metaconfig/$service/config;\n\ninclude 'metaconfig/$service/schema';" > pan/config.pan
echo -e "object template config;\n\ninclude 'metaconfig/$service/config';\n" > tests/profiles/config.pan
mkdir tests/regexps/config
echo -e 'Base test for config\n---\nmultiline\n---\n$wontmatch^\n' > tests/regexps/config/base
Commit this initial structure
git add ./
git commit -a -m "initial structure for service $service"
The schema needs to be created in the pan
subdirectory of the service directory src/main/metaconfig/$service
.
The file should be called schema.pan
.
declaration template metaconfig/example/schema;
include 'pan/types';
type example_service = {
'hosts' : type_hostname[]
'port' : type_port
'master' : boolean
'description' : string
'option' ? string
};
long
,boolean
andstring
are pan builtin types (see the panbook for more info)type_hostname
is a type that is available from the mainpan/types
template as part of the core template library.- the template namespace
metaconfig/example
does not match the location of the file, but this is intentional and is resolved by the unittest. During the tests, thencm-metaconfig/target/pan
directory will be created with correct directoy structure.
A reference config file can now also be created, with e.g. the type binding to the correct path and configuration of the
restart action and the TT module to load. The file config.pan
should be created in the same pan
directory as schema.pan
.
unique template metaconfig/example/config;
include 'metaconfig/example/schema';
bind "/software/components/metaconfig/services/{/etc/example/exampled.conf}/contents" = example_service;
prefix "/software/components/metaconfig/services/{/etc/example/exampled.conf}";
"daemon" = "exampled";
"module" = "example/main";
This will expect the TT module with relative filename example/main.tt
.
Create the main.tt
file with content in the src/main/metaconfig/$service
directory
name = {
[% FILTER indent -%]
hosts = [% hosts.join(',') %]
port = [% port %]
master = [% master ? "TRUE" : "FALSE" %]
description = "[% description %]"
[% IF option.defined -%]
option = "[% option %]"
[% END -%]
[% END -%]
}
FILTER indent
creates the indentation- TT can easily introduce newline issues, so be careful if the config files are sensitive to this.
Each unittest consists of 2 parts:
- an object template (in
src/main/metaconfig/example/tests/profile
) - the resulting profile has the required attributes
- the tests (try to) use the same path as
ncm-metaconfig
(profile/CCM/CAF) - one or more RegexpTests that contain regular expressions that will be tested against the output produced by the TT module and the profile.
- multiple files in directory
src/main/metaconfig/example/tests/regexps/<name_of_object_template>
- one file
src/main/metaconfig/example/tests/regexps/<name_of_object_template>
The object template is compiled in JSON format using the pan-compiler.
The testsuite takes care of the compilation, TT output generation, and running the tests.
Only templates with .pan
suffix that are either unique
, structure
or object
templates are considered,
all other will get an (non-fatal) error message. Subdirectories will not be checked for object templates.
The object template and the (one or more) corresponding RegexpTest(s) together form a
single unittest for the service; all unittests for this single service
are ran via in a single perl src/test/perl/service-example.t
unittest, and should look like
use Test::More;
use Test::Quattor::TextRender::Metaconfig;
my $u = Test::Quattor::TextRender::Metaconfig->new(
service => 'example',
)->test();
done_testing;
(To be complete, the ncm-metaconfig
component has lots of perl service unittests,
together the perl unittests for the component itself).
The easiest example is a single object template with a single regexp file.
The default pan basepath for the TextRender
attributes like module
and contents
is /metaconfig
.
Create the profile tests/profiles/simple.pan
as follows:
object template simple;
"/metaconfig/module" = "example/main";
prefix "/metaconfig/contents";
"hosts" = list("server1", "server2");
"port" = 800;
"master" = false;
"description" = "My example";
- the schema is not validated in this
simple
template, but it can easily be done by adding
include 'metaconfig/example/schema';
bind "/metaconfig/example/contents" = example_service;
But the preferred way is to create a proper config.pan
file and use that
(see config example below).
Make a 3 block text file tests/regexps/simple
, with ---
as block separator as follows
Simple test
---
unordered
nomultiline
---
name
hosts
port
master
description
This will search the output for the words name
, hosts
, port
, master
and description
.
This is good for illustrating the principle, but is a lousy unittest. Check the config
unittest below for proper testing.
The filename simple
has to match the object template you want to test with (in this case the simple.pan
template).
The required attributes for ncm-metaconfig
(e.g. module and contents) are retrieved from the location default /metaconfig
(e.g. the contents will $cfg->getElement('/metaconfig/contents')->getTree()
.
To select another path, 2 additional location flags are supproted:
- an absolute path starting with a single
/
is interpreted as a metaconfig service (/etc/config
will result in contents from$cfg->getElement('/software/componentes/metaconfig/service/{/etc/config}/contents')->getTree();
- an absolute path starting with 2
/
s is interpreted as an absolute panpath (e.g.//some/path
will look for contents from$cfg->getElement('/some/path/contents')->getTree()
.
You can verify this single unittest for the example
service using
QUATTOR_TEST_SUITE_FILTER=simple mvn clean test -Dunittest=service-example.t
The QUATTOR_TEST_SUITE_FILTER
environment variable is a regular expression pattern that will
filter the tests to run (matching tests are run).
(Run this from the configuration-modules-core/ncm-metaconfig
directory)
It is better to use a full blown template as will be used in the actual profiles. The added
advantage here is the config.pan
and schema.pan
from the pan
directory are tested as well.
The profile tests/profiles/config.pan
is similar to the simple one
(it are the same values after all we want to set),
but by targetting metaconfig
usage, a different prefix is required.
object template config;
include 'metaconfig/example/config';
prefix "/software/components/metaconfig/services/{/etc/example/exampled.conf}/contents";
"hosts" = list("server1", "server2");
"port" = 800;
"master" = false;
"description" = "My example";
The type binding and definition of the TT module are part of the pan/config.pan
template, and this usage is very
close to actual usage.
We will now make several regexptests, each in their own file and
grouped in a directory called config
(matching the object profile name).
The filenames in the directory are not relevant (but no addiditional directory structure is allowed).
We need to set the location flag to point to the test infrastructure which metaconfig-controlled file this is supposed to test.
In principle only one of the regexp tests should set this flag (and if multiple ones are set, they all have to be equal). You cannot test different metaconfig file paths from the same profile.
Lets start with a regexptest identical to the simple
test above, tests/regexps/config/base
:
Simple base test
---
/etc/example/exampled.conf
unordered
nomultiline
---
name
hosts
port
master
description
A 2nd better regexptest tests/regexps/config/not_so_simple
uses the default flags multiline
and ordered,
where the regular expressions are all interpreted as multiline regular expressions.
Basic multiline test
---
/etc/example/exampled.conf
---
^name
^\s{4}hosts
^\s{4}port
^\s{4}master
^\s{4}description
= ### COUNT 5
This test also uses the directive ### COUNT X
(with leading space; X is number, can be 0 or more), where this regular
expression is expected to occur exactly X times (in this case, we expect 5 =
characters).
The COUNT
directive ignores the ordering. It is the total number of matches.
A 3rd regexptest tests/regexps/config/neg
checks if certain regular expression do not match using the negate
flag.
Basic negate test
---
/etc/example/exampled.conf
negate
---
^hosts
^port
^master
^description
This tests that the expected fields can't start at the beginning of the line,
whitespace must be inserted before.
(The FILTER indent
TT inserts 4 spaces, as tested with the \s{4}
in the multiline regexp above.)
If one only needs to check that a single regular expression does not occur, one can also use ### COUNT 0
, without
having to make a separate regexp test with the negate flag.
(Setting the negate
flag silently ignores the order flag).
A 4th regexp test tests/regexps/config/value
uses full value checks, which is interesting to have, but harder to maintain and review.
Basic value test
---
/etc/example/exampled.conf
---
^name\s=\s\{
^\s{4}hosts\s=\sserver1,server2$
^\s{4}port\s=\s800$
^\s{4}master\s=\sFALSE$
^\s{4}description\s=\s"My example"$
^}$
You can verify this single unittest for the example
service using
QUATTOR_TEST_SUITE_FILTER=config mvn clean test -Dunittest=service-example.t
(this will run all 4 regexptest files)
- a test for the optional
option
field: a new test profile is required that has the optional field configured, and it also requires one or more regexp tests to verify at least theoption
field in the output, and possibly also the quoted value.
Generating all files as discussed above generates following file tree in the configuration-modules-core
ncm-metaconfig/src/main/metaconfig/example/main.tt
ncm-metaconfig/src/main/metaconfig/example/pan/config.pan
ncm-metaconfig/src/main/metaconfig/example/pan/schema.pan
ncm-metaconfig/src/main/metaconfig/example/tests/profiles/config.pan
ncm-metaconfig/src/main/metaconfig/example/tests/profiles/simple.pan
ncm-metaconfig/src/main/metaconfig/example/tests/regexps/config/base
ncm-metaconfig/src/main/metaconfig/example/tests/regexps/config/neg
ncm-metaconfig/src/main/metaconfig/example/tests/regexps/config/not_so_simple
ncm-metaconfig/src/main/metaconfig/example/tests/regexps/config/value
ncm-metaconfig/src/main/metaconfig/example/tests/regexps/simple
ncm-metaconfig/src/test/perl/service-example.t
Also see the PR for this example service.
Another test module Test::Quattor::TextRender::Component
exists to facilitate the usage and testing of
TT files in other compoments.
TODO: the tests and TT files in a src/main/resources
directory with the TT files and a tests
subdirectory
TODO: difference in the pan schema files (they are in normal location e.g. src/main/pan/component/mycomponent/schema.pan
)