1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4#
5# Copyright 2017, Data61
6# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
7# ABN 41 687 119 230.
8#
9# This software may be distributed and modified according to the terms of
10# the BSD 2-Clause license. Note that NO WARRANTY is provided.
11# See "LICENSE_BSD2.txt" for details.
12#
13# @TAG(DATA61_BSD)
14#
15
16'''
17This file contains unit test cases related to the template macros.
18'''
19
20from __future__ import absolute_import, division, print_function, \
21    unicode_literals
22
23import ast, fnmatch, os, re, subprocess, sys, unittest
24
25ME = os.path.abspath(__file__)
26MY_DIR = os.path.dirname(ME)
27
28# Make CapDL importable. Note that we just assume where it is in relation to
29# our own directory.
30sys.path.append(os.path.join(MY_DIR, '../../../../python-capdl'))
31
32# Make CAmkES importable
33sys.path.append(os.path.join(MY_DIR, '../../..'))
34
35from camkes.internal.tests.utils import CAmkESTest, which
36from camkes.templates.macros import NO_CHECK_UNUSED, get_perm
37from camkes.templates import TemplateError
38
39def uname():
40    '''
41    Determine the hardware architecture of this machine. Note that we're only
42    really interested in x86 or x86_64.
43    '''
44    try:
45        machine = subprocess.check_output(['uname', '-m']).strip()
46    except subprocess.CalledProcessError:
47        return None
48    if re.match(r'i\d86$', machine):
49        return 'x86'
50    return machine
51
52class TestMacros(CAmkESTest):
53
54    def test_get_perm(self):
55        conf = {}
56        instance = "bah"
57        iface = "humbug"
58        field = '%s_access' % iface
59        conf[instance] = {}
60
61        self.assertEqual(get_perm(conf, instance, iface), "RWXP")
62        conf[instance][field] = "R"
63        self.assertEqual(get_perm(conf, instance, iface), "R")
64        conf[instance][field] = "FOO"
65        with self.assertRaises(TemplateError):
66            get_perm(conf, instance, iface)
67
68    def test_find_unused_macros(self):
69        '''
70        Find macros intended for the templates that are never actually used in
71        any template.
72        '''
73
74        # First obtain a set of available macros by parsing the source of
75        # macros.py.
76        macrospy = os.path.join(MY_DIR, '../macros.py')
77        with open(macrospy) as f:
78            source = f.read()
79
80        node = ast.parse(source, filename=macrospy)
81
82        macros = set()
83        for i in ast.iter_child_nodes(node):
84            if isinstance(i, ast.FunctionDef):
85                macros.add(i.name)
86
87        # Next get a set of gitignored globs.
88
89        # First up, ignore the tests.
90        ignored = set(('%s/**' % MY_DIR,))
91
92        # Now look at all .gitignores from three directories up.
93        for stem in ('../../..', '../..', '..'):
94            gitignore = os.path.join(MY_DIR, stem, '.gitignore')
95            if os.path.exists(gitignore):
96                with open(gitignore) as f:
97                    for line in (x.strip() for x in f
98                            if x.strip() != '' and not x.startswith('#')):
99                        pattern = os.path.join(os.path.abspath(
100                            os.path.dirname(gitignore)), line)
101                        ignored.add(pattern)
102
103        # Now let's look at all the templates and note macro calls.
104
105        # A regex to match macro calls from the template context. Note that it is
106        # imprecise, so the resulting analysis could generate false negatives.
107        call = re.compile(r'/\*[-\?].*?\bmacros\.([a-zA-Z][a-zA-Z0-9_]*)\b')
108
109        used = set()
110        for root, _, files in os.walk(os.path.abspath(
111                os.path.join(MY_DIR, '..'))):
112            for f in (os.path.join(root, f) for f in files):
113                for pattern in ignored:
114                    try:
115                        if fnmatch.fnmatchcase(f, pattern):
116                            break
117                    except Exception:
118                        # Suppress any errors resulting from invalid lines in
119                        # .gitignore.
120                        pass
121                else:
122                    # This file did not match any of the ignore patterns; scan
123                    # it for macro calls.
124
125                    with open(f) as input:
126                        source = input.read()
127                    for m in call.finditer(source):
128                        used.add(m.group(1))
129
130        unused = macros - used - NO_CHECK_UNUSED
131        if len(unused) > 0:
132            [print("Unused macro: %s" % u) for u in unused]
133        self.assertSetEqual(unused, set())
134
135if __name__ == '__main__':
136    unittest.main()
137