import cnfvar and unit tests
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Fri, 1 Dec 2017 10:08:39 +0000 (11:08 +0100)
committerPhilipp Gesang <philipp.gesang@intra2net.com>
Fri, 1 Dec 2017 10:08:44 +0000 (11:08 +0100)
cnfvar.py          <- intranator/backup-crypto
cnfvar_unittest.py <- autotest-intranator/backup-crypto

Consequently, cnfvar.py has already been adapted for Python 3,
the unit tests have not.

src/cnfvar.py [new file with mode: 0644]
test/cnfvar_unittest.py [new file with mode: 0755]

diff --git a/src/cnfvar.py b/src/cnfvar.py
new file mode 100644 (file)
index 0000000..0418a7e
--- /dev/null
@@ -0,0 +1,632 @@
+#!/usr/bin/env python
+
+"""
+
+SUMMARY
+------------------------------------------------------
+cnfvar
+
+Copyright: Intra2net AG
+
+
+CONTENTS
+------------------------------------------------------
+Represent CNF_VARs as recursive structures.
+
+.. todo:: Decide on some facility for automatic fixup of line number values. The
+    internal representation is recursive so line numbers are not needed to
+    establish a variable hierarchy. They might as well be omitted from the
+    json input and only added when writing cnf. For the time being a lack
+    of the "number" field is interpreted as an error. Though it should at
+    least optionally be possible to omit the numbers entirely and have a
+    function add them after the fact. This might as well be added to
+    :py:func:`is_cnf` though it would be counterintuitive to have a predicate
+    mutate its argument. So maybe it could return a second argument to
+    indicate a valid structure that needs fixup or something like that.
+
+.. note:: The variable values of get_cnf seems to be encoded in latin1, and
+          set_cnf seems to require latin1-encoded values (not var names).
+          Function :py:func:`read_cnf` converts this to unicode and functions
+          :py:func:`dump_cnf_string`, :py:func:`print_cnf`, and
+          :py:func:`write_cnf` convert unicode back to latin1.
+
+
+INTERFACE
+------------------------------------------------------
+"""
+
+import functools
+import sys
+import json
+import re
+import io
+
+#
+#                                   helpers
+#
+
+# Sadly, the Intranator is still stuck with one leg in the 90s.
+#
+
+
+def to_latin1(s):
+    """ take given unicode value and convert it to a latin1-encoded string """
+    return s.encode("latin-1")
+
+
+def from_latin1(s):
+    """ take given latin1-encoded string value and convert it to unicode """
+    return s.decode("latin-1")
+
+#
+#                                  traversal
+#
+
+
+class InvalidCNF(Exception):
+
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return "Malformed CNF_VAR: \"%s\"" % self.msg
+
+
+def walk_cnf(cnf, nested, fun, acc):
+    """
+    :type cnf: cnf list
+    :type nested: bool
+    :type fun: ('a -> bool -> (cnf stuff) -> 'a)
+    :type acc: 'a
+    :rtype: 'a
+    """
+    for var in cnf:
+        acc = fun(acc,
+                  nested,
+                  comment=var.get("comment", None),
+                  data=var.get("data", None),
+                  instance=var.get("instance", None),
+                  number=var.get("number", None),
+                  parent=var.get("parent", None),
+                  varname=var.get("varname", None))
+        children = var.get("children", None)
+        if children is not None:
+            acc = walk_cnf(children, True, fun, acc)
+    return acc
+
+
+#
+#                                 validation
+#
+
+def is_string(s):
+    return isinstance(s, str) or isinstance(s, unicode)
+
+
+def is_valid(acc,
+             nested,
+             comment,
+             data,
+             instance,
+             number,
+             parent,
+             varname):
+    if varname is None:
+        raise InvalidCNF("CNF_VAR lacks a name.")
+    elif not is_string(varname):
+        raise InvalidCNF("Varname field of CNF_VAR \"%s\" is not a string."
+                         % varname)
+    elif varname == "":
+        raise InvalidCNF("Varname field of CNF_VAR is the empty string.")
+
+    if comment is not None:
+        if not is_string(comment):
+            raise InvalidCNF("Comment field of CNF_VAR \"%s\" is not a string."
+                             % varname)
+
+    if data is None:
+        raise InvalidCNF("Data field of CNF_VAR \"%s\" is empty."
+                         % varname)
+    elif not is_string(data):
+        raise InvalidCNF("Data field of CNF_VAR \"%s\" is not a string."
+                         % varname)
+
+    if instance is None:
+        raise InvalidCNF("Instance field of CNF_VAR \"%s\" is empty."
+                         % varname)
+    elif not isinstance(instance, int):
+        raise InvalidCNF("Instance field of CNF_VAR \"%s\" is not an integer."
+                         % varname)
+
+    if number is None:
+        raise InvalidCNF("Number field of CNF_VAR \"%s\" is empty."
+                         % varname)
+    elif not isinstance(number, int):
+        raise InvalidCNF("Number field of CNF_VAR \"%s\" is not an integer."
+                         % varname)
+    elif number < 1:
+        raise InvalidCNF("Number field of CNF_VAR \"%s\" must be positive, not %d."
+                         % (varname, number))
+    else:
+        other = acc.get(number, None)
+        if other is not None:  # already in use
+            raise InvalidCNF("Number field of CNF_VAR \"%s\" already used by variable %s."
+                             % (varname, other))
+        acc[number] = varname
+
+    if nested is True:
+        if parent is None:
+            raise InvalidCNF("Parent field of nested CNF_VAR \"%s\" is empty."
+                             % varname)
+        elif not isinstance(parent, int):
+            raise InvalidCNF("Parent field of CNF_VAR \"%s\" is not an integer."
+                             % varname)
+    else:
+        if parent is not None:
+            raise InvalidCNF("Flat CNF_VAR \"%s\" has nonsensical parent field \"%s\"."
+                             % (varname, parent))
+    return acc
+
+
+def is_cnf(root):
+    """
+    is_cnf -- Predicate testing "CNF_VAR-ness". Folds the :py:func:`is_valid`
+    predicate over the argument which can be either a well-formed CNF
+    dictionary or a list of CNF_VARs.
+
+    :type root: cnfvar or cnf list
+    :rtype: bool
+
+    Not that if it returns at all, ``is_cnf()`` returns ``True``. Any non
+    well-formed member of the argument will cause the predicate to bail out
+    with an exception during traversal.
+    """
+    t_root = type(root)
+    if t_root is list:
+        cnf = root
+    elif t_root is dict:
+        cnf = root["cnf"]
+    else:
+        raise InvalidCNF(root)
+    return walk_cnf(cnf, False, is_valid, {}) is not None
+
+#
+#                               deserialization
+#
+
+# the easy part: JSON reader for get_cnf -j
+
+
+def read_cnf_json(cnfdata):
+    cnf_json = json.loads(cnfdata)
+    if is_cnf(cnf_json) is False:
+        raise TypeError("Invalid CNF_VAR.")
+    return cnf_json
+
+# the moderately more complicated part: CNF reader
+
+
+def advance(cns):
+    current, next, stream = cns
+    if next is None:  # reached end of stream
+        return None
+    current = next
+
+    try:
+        next = stream.pop()
+        next = next.strip()
+    except IndexError:
+        next = None
+
+    if current == "":  # skip blank lines
+        return advance((current, next, stream))
+    return (current, next, stream)
+
+
+def prepare(raw):
+    """
+    :type raw: str
+    :rtype: (str * str option * str list) option
+    """
+    lines = raw.splitlines()
+    lines.reverse()
+    try:
+        first = lines.pop()
+    except IndexError:
+        return None
+
+    try:
+        second = lines.pop()
+    except IndexError:
+        second = None
+
+    first = first.strip()
+    if first == "":
+        return advance((first, second, lines))
+
+    return (first, second, lines)
+
+
+def peek(cns):
+    _, next, _ = cns
+    return next
+
+
+def get(cnf):
+    current, _, _ = cns
+    return current
+
+
+class MalformedCNF(Exception):
+
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return "Malformed CNF file: \"%s\"" % self.msg
+
+grab_parent_pattern = re.compile("""
+                                    ^            # match from start
+                                    \d+          # line number
+                                    \s+          # spaces
+                                    \((\d+)\)    # parent
+                                 """,
+                                 re.VERBOSE)
+
+
+def get_parent(line):
+    match = re.match(grab_parent_pattern, line)
+    if match is None:  # -> no parent
+        return None
+    return int(match.groups()[0])
+
+base_line_pattern = re.compile("""
+                                    ^                    # match from start
+                                    \s*                  # optional spaces
+                                    (\d+)                # line number
+                                    \s+                  # spaces
+                                    ([A-Z][A-Z0-9_]*)    # varname
+                                    \s*                  # optional spaces
+                                    ,                    # delimiter
+                                    \s*                  # optional spaces
+                                    (-1|\d+)             # instance
+                                    \s*                  # optional spaces
+                                    :                    # delimiter
+                                    \s*                  # optional spaces
+                                    \"([^\"]*)\"         # quoted string (data)
+                                    \s*                  # optional spaces
+                                    (                    # bgroup
+                                     \#                  # comment leader
+                                     \s*                 # optional spaces
+                                     .*                  # string (comment)
+                                    )?                   # egroup, optional
+                                    $                    # eol
+                               """,
+                               re.VERBOSE)
+
+
+def read_base_line(line):
+    if len(line.strip()) == 0:
+        return None  # ignore empty lines
+    if line[0] == "#":
+        return None  # ignore comments
+
+    match = re.match(base_line_pattern, line)
+    if match is None:
+        raise MalformedCNF("Syntax error in line \"\"\"%s\"\"\"" % line)
+    number, varname, instance, data, comment = match.groups()
+    return {
+        "number": int(number),
+        "varname": varname,
+        "instance": int(instance),
+        "data": data and from_latin1(data) or "",
+        "comment": comment and from_latin1(comment[1:].strip()) or None,
+    }
+
+child_line_pattern = re.compile("""
+                                     ^                    # match from start
+                                     \s*                  # optional spaces
+                                     (\d+)                # line number
+                                     \s+                  # spaces
+                                     \((\d+)\)            # parent
+                                     \s+                  # spaces
+                                     ([A-Z][A-Z0-9_]*)    # varname
+                                     \s*                  # optional spaces
+                                     ,                    # delimiter
+                                     \s*                  # optional spaces
+                                     (-1|\d+)             # instance
+                                     \s*                  # optional spaces
+                                     :                    # delimiter
+                                     \s*                  # optional spaces
+                                     \"([^\"]*)\"         # quoted string (data)
+                                     \s*                  # optional spaces
+                                     (                    # bgroup
+                                      \#                  # comment leader
+                                      \s*                 # optional spaces
+                                      .*                  # string (comment)
+                                     )?                   # egroup, optional
+                                     $                    # eol
+                                """,
+                                re.VERBOSE)
+
+
+def read_child_line(line):
+    if len(line.strip()) == 0:
+        return None  # ignore empty lines
+    if line[0] == "#":
+        return None  # ignore comments
+
+    match = re.match(child_line_pattern, line)
+    if match is None:
+        raise MalformedCNF("Syntax error in child line \"\"\"%s\"\"\"" % line)
+    number, parent, varname, instance, data, comment = match.groups()
+    return {
+        "number": int(number),
+        "parent": int(parent),
+        "varname": varname,
+        "instance": int(instance),
+        "data": data and from_latin1(data) or "",
+        "comment": comment and from_latin1(comment[1:].strip()) or None,
+    }
+
+
+def parse_cnf_children(state, parent):
+    lines = []
+    current = get(state)
+    while True:
+        cnf_line = read_child_line(current)
+        if cnf_line is not None:
+            lines.append(cnf_line)
+            state = advance(state)
+            if state is None:
+                break
+            current = get(state)
+            new_parent = get_parent(current)
+            if new_parent is None:
+                # drop stack
+                return (state, lines, None)
+            if new_parent > parent:
+                # parent is further down in hierarchy -> new level
+                (state, children, new_parent) = parse_cnf_children(
+                    state, new_parent)
+                cnf_line["children"] = children
+                current = get(state)
+            if new_parent < parent:
+                # parent is further up in hierarchy -> pop level
+                return (state, lines, new_parent)
+            # new_parent == parent -> continue parsing on same level
+    return (state, lines, parent)
+
+
+def parse_cnf_root(state):
+    lines = []
+    current = get(state)
+    while state:
+        cnf_line = read_base_line(current)
+        if cnf_line is not None:
+            lines.append(cnf_line)
+            state = advance(state)
+            if state is None:  # -> nothing left to do
+                break
+            current = get(state)
+            parent = get_parent(current)  # peek at next line
+            if parent is not None:  # -> recurse into children
+                (state, children, _parent) = parse_cnf_children(state, parent)
+                cnf_line["children"] = children
+                if state is None:
+                    break
+                current = get(state)
+    return lines
+
+
+def read_cnf(data):
+    state = prepare(data)
+    if state is None:
+        raise InvalidCNF("Empty input string.")
+
+    cnf = parse_cnf_root(state)
+    if is_cnf(cnf) is False:
+        raise TypeError("Invalid CNF_VAR.")
+    return {"cnf": cnf}
+
+
+def renumber_vars(root, parent=None):
+    """
+    renumber_vars -- Number cnfvars linearly.
+    """
+    if isinstance(root, dict):
+        root = root["cnf"]
+    i = parent or 0
+    for var in root:
+        i += 1
+        var["number"] = i
+        if parent is not None:
+            var["parent"] = parent
+        children = var.get("children", None)
+        if children is not None:
+            i = renumber_vars(children, i)
+    return i
+
+#
+#                                serialization
+#
+
+cnf_line_nest_indent = "  "
+cnf_line_base_fmt = "%d %s,%d: \"%s\""
+cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\""
+
+
+def format_cnf_vars(da, var):
+    """
+    Return a list of formatted cnf_line strings.
+    """
+
+    depth, acc = da
+    line = None
+    if depth > 0:
+        line = cnf_line_child_fmt \
+            % (var["number"],
+               cnf_line_nest_indent * depth,
+               var["parent"],
+               to_latin1(var["varname"]).decode (),
+               var["instance"],
+               to_latin1(var["data"]).decode ())
+    else:
+        line = cnf_line_base_fmt \
+            % (var["number"],
+               to_latin1(var["varname"]).decode (),
+               var["instance"],
+               to_latin1(var["data"]).decode ())
+
+    comment = var.get("comment", None)
+    if comment and comment is not "":
+        line = line + (" # %s" % to_latin1(comment))
+
+    acc.append(line)
+
+    children = var.get("children", None)
+    if children is not None:
+        (_, acc) = functools.reduce(format_cnf_vars, children, (depth + 1, acc))
+    return (depth, acc)
+
+
+def cnf_root(root):
+    if not isinstance(root, dict):
+        raise TypeError(
+            "Expected dictionary of CNF_VARs, got %s." % type(root))
+    cnf = root.get("cnf", None)
+    if not isinstance(cnf, list):
+        raise TypeError("Expected list of CNF_VARs, got %s." % type(cnf))
+    return cnf
+
+
+def print_cnf_raw(root, out=None):
+    if root is not None:
+        out.write(root)
+
+
+def write_cnf_raw(*argv, **kw_argv):
+    print_cnf_raw(*argv, **kw_argv)
+
+
+def output_cnf(root, out, renumber=False):
+    cnf = cnf_root(root)
+    # Cthulhu (a.k.a Autotest) hates us so much he replaced the innocuous
+    # stdout with some chimera that isn't derived from ``file``, rendering
+    # the following sanity check moot.
+    # if not isinstance(out, file):
+    #     raise TypeError("%s (%s) is not a stream." % (out, type(out)))
+    if renumber is True:
+        _count = renumber(root)
+    if is_cnf(cnf) is True:
+        (_, lines) = functools.reduce(format_cnf_vars, cnf, (0, []))
+        out.write("\n".join(lines))
+        out.write("\n")
+
+
+def dump_cnf_string(root, renumber=False):
+    """
+    dump_cnf_string -- Serialize CNF var structure, returning the result as a
+    string.
+    """
+    cnf = cnf_root(root)
+    out = io.StringIO()
+    output_cnf(root, out, renumber=renumber)
+    res = out.getvalue()
+    out.close()
+    return res
+
+
+def print_cnf(root, out=None, renumber=False):
+    if root is not None:
+        output_cnf(root, out or sys.stdout, renumber=renumber)
+
+
+def write_cnf(*argv, **kw_argv):
+    print_cnf(*argv, **kw_argv)
+
+
+def output_json(root, out, renumber=False):
+    # Sanity check incompatible with Autotest. Who needs a seatbelt anyways?
+    # if not isinstance(out, file):
+        #raise TypeError("%s (%s) is not a stream." % (out, type(out)))
+    if renumber is True:
+        _count = renumber_vars(root)
+    if is_cnf(root) is True:
+        data = json.dumps(root)
+        out.write(data)
+        out.write("\n")
+
+
+def dump_json_string(root, renumber=False):
+    """
+    dump_json_string -- Serialize CNF var structure as JSON, returning the
+    result as a string.
+    """
+    out = io.StringIO()
+    output_json(root, out, renumber=renumber)
+    res = out.getvalue()
+    out.close()
+    return res
+
+
+def print_cnf_json(root, out=None, renumber=False):
+    if root is not None:
+        output_json(root, out or sys.stdout, renumber=renumber)
+
+
+def write_cnf_json(*argv, **kw_argv):
+    print_cnf_json(*argv, **kw_argv)
+
+#
+#                                  querying
+#
+
+
+def get_vars(cnf, data=None, instance=None):
+    """
+    get_vars -- Query a CNF_VAR structure. Skims the *toplevel* of the CNF_VAR
+    structure for entries with a matching `data` or `instance` field.
+
+    :type cnf: CNF_VAR
+    :type data: str
+    :type instance: int
+    :rtype: CNF_VAR
+    :returns: The structure containing only references to the
+             matching variables. Containing an empty list of
+             variables in case there is no match.
+    """
+    cnf = cnf["cnf"]
+    if cnf:
+        criterion = lambda _var: False
+        if data:
+            if instance:
+                criterion = lambda var: var[
+                    "data"] == data and var["instance"] == instance
+            else:
+                criterion = lambda var: var["data"] == data
+        elif instance:
+            criterion = lambda var: var["instance"] == instance
+
+        return {"cnf": [var for var in cnf if criterion(var) is True]}
+
+    return {"cnf": []}
+
+#
+#                         entry point for development
+#
+
+
+def main(argv):
+    if len(argv) > 1:
+        first = argv[1]
+        if first == "-":
+            cnf = read_cnf(sys.stdin.read())
+            print_cnf(cnf)
+        elif first == "test":
+            cnf = read_cnf(sys.stdin.read())
+            cnff = get_vars(cnf, instance=2, data="FAX")
+            print_cnf(cnff)
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/test/cnfvar_unittest.py b/test/cnfvar_unittest.py
new file mode 100755 (executable)
index 0000000..37e5fe3
--- /dev/null
@@ -0,0 +1,359 @@
+#!/usr/bin/env python
+# This Python file uses the following encoding: utf-8
+
+import unittest
+import cnfvar
+
+#
+#                                  test data
+#
+
+# model cnf tree
+demo_cnfvar = {"cnf": [
+    {
+        "varname": "MY_FAVORITE_CNF_VAR",
+        "instance": 1337,
+        "number": 42,
+        "data": "string conf content",
+        "comment": ""
+    },
+    {
+        "varname": "SOME_NESTED_CNF_VAR",
+        "instance": 0,
+        "number": 23,
+        "data": "999",
+        "comment": "",
+        "children": [
+            {
+                "varname": "SOME_CHILD_VAR",
+                "instance": 1,
+                "number": 24,
+                "data": "2014",
+                "parent": 23,
+                "comment": ""
+            }
+        ]
+    },
+]}
+
+# duplicate line number
+demo_invalid_cnfvar = {"cnf": [
+    {
+        "varname": "SOME_PARTICULARLY_TASTY_CNF_VAR",
+        "instance": 1337,
+        "number": 23,
+        "data": "classic wingers",
+        "comment": ""
+    },
+    {
+        "varname": "EXTRAORDINARILY_FANCY_CNF_VAR",
+        "instance": 1,
+        "number": 42,
+        "data": "ab mentions",
+        "comment": ""
+    },
+    {
+        "varname": "ANOTHER_POPULAR_CNF_VAR",
+        "instance": 0,
+        "number": 42,
+        "data": "notches",
+        "comment": ""
+    }
+]}
+
+demo_jsoncnf = """
+{
+    "cnf" : [
+        {
+            "children" : [
+                {
+                    "comment" : "",
+                    "data" : "1",
+                    "instance" : 0,
+                    "number" : 2,
+                    "parent" : 1,
+                    "varname" : "FIREWALL_SERVICEGROUP_PREDEFINED_ID"
+                },
+                {
+                    "children" : [
+                        {
+                            "comment" : "",
+                            "data" : "",
+                            "instance" : 0,
+                            "number" : 4,
+                            "parent" : 3,
+                            "varname" : "FIREWALL_SERVICEGROUP_TYPE_COMMENT"
+                        },
+                        {
+                            "comment" : "",
+                            "data" : "80",
+                            "instance" : 0,
+                            "number" : 5,
+                            "parent" : 3,
+                            "varname" : "FIREWALL_SERVICEGROUP_TYPE_TCPUDP_DST_PORT"
+                        }
+                    ],
+                    "comment" : "",
+                    "data" : "TCP",
+                    "instance" : 0,
+                    "number" : 3,
+                    "parent" : 1,
+                    "varname" : "FIREWALL_SERVICEGROUP_TYPE"
+                }
+            ],
+            "comment" : "",
+            "data" : "http",
+            "instance" : 1,
+            "number" : 1,
+            "varname" : "FIREWALL_SERVICEGROUP"
+        },
+        {
+            "children" : [
+                {
+                    "comment" : "",
+                    "data" : "2",
+                    "instance" : 0,
+                    "number" : 7,
+                    "parent" : 6,
+                    "varname" : "FIREWALL_SERVICEGROUP_PREDEFINED_ID"
+                },
+                {
+                    "children" : [
+                        {
+                            "comment" : "",
+                            "data" : "",
+                            "instance" : 0,
+                            "number" : 9,
+                            "parent" : 8,
+                            "varname" : "FIREWALL_SERVICEGROUP_TYPE_COMMENT"
+                        },
+                        {
+                            "comment" : "",
+                            "data" : "443",
+                            "instance" : 0,
+                            "number" : 10,
+                            "parent" : 8,
+                            "varname" : "FIREWALL_SERVICEGROUP_TYPE_TCPUDP_DST_PORT"
+                        }
+                    ],
+                    "comment" : "",
+                    "data" : "TCP",
+                    "instance" : 0,
+                    "number" : 8,
+                    "parent" : 6,
+                    "varname" : "FIREWALL_SERVICEGROUP_TYPE"
+                }
+            ],
+            "comment" : "",
+            "data" : "https",
+            "instance" : 2,
+            "number" : 6,
+            "varname" : "FIREWALL_SERVICEGROUP"
+        }
+    ]
+}
+"""
+
+demo_latin1crap = r"""
+{ "cnf" : [
+    {
+        "children" : [
+            {
+                "comment" : "",
+                "data" : "0",
+                "instance" : 0,
+                "number" : 2,
+                "parent" : 1,
+                "varname" : "USER_DISABLED"
+            },
+            {
+                "comment" : "",
+                "data" : "Administrator",
+                "instance" : 0,
+                "number" : 3,
+                "parent" : 1,
+                "varname" : "USER_FULLNAME"
+            },
+            {
+                "comment" : "",
+                "data" : "INBOX/Kalender",
+                "instance" : 0,
+                "number" : 4,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_CALENDAR"
+            },
+            {
+                "comment" : "",
+                "data" : "INBOX/Kontakte",
+                "instance" : 0,
+                "number" : 5,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_CONTACTS"
+            },
+            {
+                "comment" : "",
+                "data" : "INBOX/Entwürfe",
+                "instance" : 0,
+                "number" : 6,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_DRAFTS"
+            },
+            {
+                "comment" : "",
+                "data" : "INBOX/Notizen",
+                "instance" : 0,
+                "number" : 7,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_NOTES"
+            },
+            {
+                "comment" : "",
+                "data" : "INBOX/Gesendete Elemente",
+                "instance" : 0,
+                "number" : 8,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_OUTBOX"
+            },
+            {
+                "comment" : "",
+                "data" : "INBOX/Aufgaben",
+                "instance" : 0,
+                "number" : 9,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_TASKS"
+            },
+
+            {
+                "comment" : "",
+                "data" : "INBOX/Gelöschte Elemente",
+                "instance" : 0,
+                "number" : 10,
+                "parent" : 1,
+                "varname" : "USER_GROUPWARE_FOLDER_TRASH"
+            },
+            {
+                "comment" : "",
+                "data" : "1",
+                "instance" : 0,
+                "number" : 11,
+                "parent" : 1,
+                "varname" : "USER_GROUP_MEMBER_REF"
+            },
+            {
+                "comment" : "",
+                "data" : "2",
+                "instance" : 1,
+                "number" : 12,
+                "parent" : 1,
+                "varname" : "USER_GROUP_MEMBER_REF"
+            },
+            {
+                "comment" : "",
+                "data" : "",
+                "instance" : 0,
+                "number" : 13,
+                "parent" : 1,
+                "varname" : "USER_LOCALE"
+            },
+            {
+                "comment" : "",
+                "data" : "idkfa",
+                "instance" : 0,
+                "number" : 14,
+                "parent" : 1,
+                "varname" : "USER_PASSWORD"
+            },
+            {
+                "comment" : "",
+                "data" : "30",
+                "instance" : 0,
+                "number" : 15,
+                "parent" : 1,
+                "varname" : "USER_TRASH_DELETEDAYS"
+            }
+        ],
+        "comment" : "",
+        "data" : "admin",
+        "instance" : 1,
+        "number" : 1,
+        "varname" : "USER"
+    }
+]}
+"""
+
+demo_cnf_group = """
+1 GROUP,1: "Administratoren"
+2    (1) GROUP_ACCESS_GO_ONLINE_ALLOWED,0: "1"
+3    (1) GROUP_EMAILFILTER_BAN_FILTERLIST_REF,0: "-1"
+4    (1) GROUP_EMAIL_RELAY_RIGHTS,0: "RELAY_FROM_INTRANET"
+5    (1) GROUP_PROXY_PROFILE_REF,0: "1"
+"""
+
+demo_cnf_filter = """
+1 EMAILFILTER_BAN_FILTERLIST,1: "Vordefiniert: Alles verboten"
+2    (1) EMAILFILTER_BAN_FILTERLIST_ENCRYPTED,0: "BLOCK"
+3    (1) EMAILFILTER_BAN_FILTERLIST_EXTENSIONS,0: ""
+4    (1) EMAILFILTER_BAN_FILTERLIST_MIMETYPES,0: ""
+5       (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,0: "text/plain"
+6       (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,1: "text/html"
+7    (1) EMAILFILTER_BAN_FILTERLIST_MODE,0: "ALLOW"
+8    (1) EMAILFILTER_BAN_FILTERLIST_PREDEFINED_ID,0: "1"
+"""
+
+demo_cnf_comments = """
+1 EMAILFILTER_BAN_FILTERLIST,1: "Vordefiniert: Alles verboten"
+2    (1) EMAILFILTER_BAN_FILTERLIST_ENCRYPTED,0: "BLOCK"
+3    (1) EMAILFILTER_BAN_FILTERLIST_EXTENSIONS,0: ""
+4    (1) EMAILFILTER_BAN_FILTERLIST_MIMETYPES,0: "" # foo
+5       (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,0: "text/plain"#bar
+6       (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,1: "text/html"
+7    (1) EMAILFILTER_BAN_FILTERLIST_MODE,0: "ALLOW"     # baz
+8    (1) EMAILFILTER_BAN_FILTERLIST_PREDEFINED_ID,0: "1"
+"""
+
+
+#
+#                                 test class
+#
+
+class CnfVarUnittest(unittest.TestCase):
+
+    def test_print_cnf(self):
+        with open("/dev/null", "w") as devnull:
+            cnfvar.print_cnf(demo_cnfvar, out=devnull)
+
+    def test_parse_cnf_simple(self):
+        cnf = cnfvar.read_cnf(demo_cnf_group)
+        with open("/dev/null", "w") as devnull:
+            cnfvar.print_cnf_json(cnf, out=devnull)
+
+    def test_parse_cnf_nested(self):
+        cnf = cnfvar.read_cnf(demo_cnf_filter)
+        with open("/dev/null", "w") as devnull:
+            cnfvar.print_cnf_json(cnf, out=devnull)
+
+    def test_parse_cnf_comments(self):
+        cnf = cnfvar.read_cnf(demo_cnf_comments)
+        with open("/dev/null", "w") as devnull:
+            cnfvar.print_cnf_json(cnf, out=devnull)
+
+    def test_print_cnf_garbage(self):
+        try:
+            with open("/dev/null", "w") as devnull:
+                cnfvar.print_cnf(demo_invalid_cnfvar, out=devnull)
+        except cnfvar.InvalidCNF, exn:
+            print "Caught the duplicate line, bravo!"
+
+    def test_read_json(self):
+        cnf = cnfvar.read_cnf_json(demo_jsoncnf)
+        with open("/dev/null", "w") as devnull:
+            cnfvar.print_cnf(cnf, out=devnull)
+
+    def test_read_json_nonascii(self):
+        cnf = cnfvar.read_cnf_json(demo_latin1crap)
+        with open("/dev/null", "w") as devnull:
+            cnfvar.print_cnf(cnf, out=devnull)
+
+
+if __name__ == '__main__':
+    unittest.main()