summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/tester/inx.py
blob: a30eb7ee70401d8b744a45be9e9cfcc5bf525394 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python
# coding=utf-8
"""
Test elements extra logic from svg xml lxml custom classes.
"""

from ..utils import PY3
from ..inx import InxFile

INTERNAL_ARGS = ('help', 'output', 'id', 'selected-nodes')
ARG_TYPES = {
    'Boolean': 'bool',
    'Color': 'color',
    'str': 'string',
    'int': 'int',
    'float': 'float',
}

class InxMixin(object):
    """Tools for Testing INX files, use as a mixin class:

    class MyTests(InxMixin, TestCase):
        def test_inx_file(self):
            self.assertInxIsGood("some_inx_file.inx")
    """
    def assertInxIsGood(self, inx_file): # pylint: disable=invalid-name
        """Test the inx file for consistancy and correctness"""
        self.assertTrue(PY3, "INX files can only be tested in python3")

        inx = InxFile(inx_file)
        if 'help' in inx.ident or inx.script.get('interpreter', None) != 'python':
            return
        cls = inx.extension_class
        # Check class can be matched in python file
        self.assertTrue(cls, 'Can not find class for {}'.format(inx.filename))
        # Check name is reasonable for the class
        if not cls.multi_inx:
            self.assertEqual(
                cls.__name__, inx.slug,
                "Name of extension class {}.{} is different from ident {}".format(
                    cls.__module__, cls.__name__, inx.slug))
            self.assertParams(inx, cls)

    def assertParams(self, inx, cls): # pylint: disable=invalid-name
        """Confirm the params in the inx match the python script"""
        params = dict([(param.name, self.parse_param(param)) for param in inx.params])
        args = dict(self.introspect_arg_parser(cls().arg_parser))
        mismatch_a = list(set(params) ^ set(args) & set(params))
        mismatch_b = list(set(args) ^ set(params) & set(args))
        self.assertFalse(mismatch_a, "{}: Inx params missing from arg parser".format(inx.filename))
        self.assertFalse(mismatch_b, "{}: Script args missing from inx xml".format(inx.filename))

        for param in args:
            if params[param]['type'] and args[param]['type']:
                self.assertEqual(
                    params[param]['type'],
                    args[param]['type'],
                    "Type is not the same for {}:param:{}".format(inx.filename, param))

    def introspect_arg_parser(self, arg_parser):
        """Pull apart the arg parser to find out what we have in it"""
        for action in arg_parser._optionals._actions: # pylint: disable=protected-access
            for opt in action.option_strings:
                # Ignore params internal to inkscape (thus not in the inx)
                if opt.startswith('--') and opt[2:] not in INTERNAL_ARGS:
                    yield (opt[2:], self.introspect_action(action))

    @staticmethod
    def introspect_action(action):
        """Pull apart a single action to get at the juicy insides"""
        return {
            'type': ARG_TYPES.get((action.type or str).__name__, 'string'),
            'default': action.default,
            'choices': action.choices,
            'help': action.help,
        }

    @staticmethod
    def parse_param(param):
        """Pull apart the param element in the inx file"""
        if param.param_type in ('optiongroup', 'notebook'):
            options = param.options
            return {
                'type': None,
                'choices': options,
                'default': options and options[0] or None,
            }
        param_type = param.param_type
        if param.param_type in ('path',):
            param_type = 'string'
        return {
            'type': param_type,
            'default': param.text,
            'choices': None,
        }