Skip to content

Latest commit

 

History

History
625 lines (453 loc) · 20.1 KB

TextRender.md

File metadata and controls

625 lines (453 loc) · 20.1 KB

TextRender

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

CAF::TextRender

Basic usage has 2 main modes:

  • generate text : the CAF::TextRender instance has auto-stringification

     use 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 from CAF::TextRender instance

     use 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

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

Newline / chomp behaviour

TT can easily generate unwanted/unneeded newlines. The chomp behaviour can be summarised as follows

Name Tag Modifier
NONE +
ONE -
COLLAPSE =
GREEDY ~

Test::Quattor::RegexpTest

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

metaconfig

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.

ncm-metaconfig

The metaconfig component is a component that has a number of services, 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).

Development example

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

Add new service

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

Target

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 hostnames
  • port: an integer
  • master: boolean with possible values TRUE or FALSE
  • 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.

Prepare

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"

Create the schema

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 and string are pan builtin types (see the panbook for more info)
  • type_hostname is a type that is available from the main pan/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, the ncm-metaconfig/target/pan directory will be created with correct directoy structure.

Create config template for metaconfig component (optional)

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.

Make TT file to match desired output

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.

Add unittests

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).

simple unittest

The easiest example is a single object template with a single regexp file.

profile

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).

regular expression

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).

Location flags in the RegexpTest

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().
verify

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)

config based unittest

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.

profile

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.

regular expressions

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"$
^}$
verify

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)

other possible tests

  • 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 the option field in the output, and possibly also the quoted value.

Result filestructure

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.

Test::Quattor::TextRender::Component

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)