ruff: Fix EXE001 "Shebang is present but file is not executable"
[kconfig-hardened-check.git] / kernel_hardening_checker / test_engine.py
1 #!/usr/bin/env python3
2
3 """
4 This tool is for checking the security hardening options of the Linux kernel.
5
6 Author: Alexander Popov <alex.popov@linux.com>
7
8 This module performs unit-testing of the kernel-hardening-checker engine.
9 """
10
11 # pylint: disable=missing-function-docstring,line-too-long
12
13 import unittest
14 import io
15 import sys
16 import json
17 import inspect
18 from typing import Union, Optional, List, Dict, Tuple
19 from .engine import StrOrBool, ChecklistObjType, KconfigCheck, CmdlineCheck, SysctlCheck, VersionCheck, OR, AND
20 from .engine import populate_with_data, perform_checks, override_expected_value
21
22
23 ResultType = List[Union[Dict[str, StrOrBool], str]]
24
25
26 class TestEngine(unittest.TestCase):
27     """
28     Example test scenario:
29
30         # 1. prepare the checklist
31         config_checklist = [] # type: List[ChecklistObjType]
32         config_checklist += [KconfigCheck('reason_1', 'decision_1', 'KCONFIG_NAME', 'expected_1')]
33         config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'cmdline_name', 'expected_2')]
34         config_checklist += [SysctlCheck('reason_3', 'decision_3', 'sysctl_name', 'expected_3')]
35
36         # 2. prepare the parsed kconfig options
37         parsed_kconfig_options  = {}
38         parsed_kconfig_options['CONFIG_KCONFIG_NAME'] = 'UNexpected_1'
39
40         # 3. prepare the parsed cmdline options
41         parsed_cmdline_options  = {}
42         parsed_cmdline_options['cmdline_name'] = 'expected_2'
43
44         # 4. prepare the parsed sysctl options
45         parsed_sysctl_options  = {}
46         parsed_sysctl_options['sysctl_name'] = 'expected_3'
47
48         # 5. prepare the kernel version
49         kernel_version = (42, 43, 44)
50
51         # 6. run the engine
52         self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, kernel_version)
53
54         # 7. check that the results are correct
55         result = [] # type: ResultType
56         self.get_engine_result(config_checklist, result, 'json')
57         self.assertEqual(...
58     """
59
60     maxDiff = None
61
62     @staticmethod
63     def run_engine(checklist: List[ChecklistObjType],
64                    parsed_kconfig_options: Optional[Dict[str, str]],
65                    parsed_cmdline_options: Optional[Dict[str, str]],
66                    parsed_sysctl_options: Optional[Dict[str, str]],
67                    kernel_version: Optional[Tuple[int, int, int]]) -> None:
68         # populate the checklist with data
69         if parsed_kconfig_options:
70             populate_with_data(checklist, parsed_kconfig_options, 'kconfig')
71         if parsed_cmdline_options:
72             populate_with_data(checklist, parsed_cmdline_options, 'cmdline')
73         if parsed_sysctl_options:
74             populate_with_data(checklist, parsed_sysctl_options, 'sysctl')
75         if kernel_version:
76             populate_with_data(checklist, kernel_version, 'version')
77
78         # now everything is ready, perform the checks
79         perform_checks(checklist)
80
81         # print the table with the results
82         print(f'\n{inspect.stack()[1].function}():')
83         print('=' * 121)
84         for opt in checklist:
85             opt.table_print('verbose', True) # verbose mode, with_results
86             print()
87             print('=' * 121)
88
89         # print the results in JSON
90         result = []
91         for opt in checklist:
92             result.append(opt.json_dump(True)) # with_results
93         print(json.dumps(result))
94         print()
95
96     @staticmethod
97     def get_engine_result(checklist: List[ChecklistObjType], result: ResultType, result_type: str) -> None:
98         assert(result_type in ('json', 'stdout', 'stdout_verbose')), \
99                f'invalid result type "{result_type}"'
100
101         if result_type == 'json':
102             for opt in checklist:
103                 result.append(opt.json_dump(True)) # with_results
104             return
105
106         captured_output = io.StringIO()
107         stdout_backup = sys.stdout
108         sys.stdout = captured_output
109         for opt in checklist:
110             if result_type == 'stdout_verbose':
111                 opt.table_print('verbose', True) # verbose mode, with_results
112             else:
113                 opt.table_print(None, True) # normal mode, with_results
114         sys.stdout = stdout_backup
115         result.append(captured_output.getvalue())
116
117     def test_simple_kconfig(self) -> None:
118         # 1. prepare the checklist
119         config_checklist = [] # type: List[ChecklistObjType]
120         config_checklist += [KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1')]
121         config_checklist += [KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2')]
122         config_checklist += [KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3')]
123         config_checklist += [KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'is not set')]
124         config_checklist += [KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'is present')]
125         config_checklist += [KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'is present')]
126         config_checklist += [KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'is not off')]
127         config_checklist += [KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'is not off')]
128         config_checklist += [KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'is not off')]
129         config_checklist += [KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is not off')]
130
131         # 2. prepare the parsed kconfig options
132         parsed_kconfig_options  = {}
133         parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
134         parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2'
135         parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
136         parsed_kconfig_options['CONFIG_NAME_7'] = 'really_not_off'
137         parsed_kconfig_options['CONFIG_NAME_8'] = 'off'
138         parsed_kconfig_options['CONFIG_NAME_9'] = '0'
139
140         # 3. run the engine
141         self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)
142
143         # 4. check that the results are correct
144         result = [] # type: ResultType
145         self.get_engine_result(config_checklist, result, 'json')
146         self.assertEqual(
147                 result,
148                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
149                  {'option_name': 'CONFIG_NAME_2', 'type': 'kconfig', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "UNexpected_2"', 'check_result_bool': False},
150                  {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: is not found', 'check_result_bool': False},
151                  {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'is not set', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'OK: is not found', 'check_result_bool': True},
152                  {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'is present', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: is present', 'check_result_bool': True},
153                  {'option_name': 'CONFIG_NAME_6', 'type': 'kconfig', 'desired_val': 'is present', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: is not present', 'check_result_bool': False},
154                  {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: is not off, "really_not_off"', 'check_result_bool': True},
155                  {'option_name': 'CONFIG_NAME_8', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_8', 'reason': 'reason_8', 'check_result': 'FAIL: is off', 'check_result_bool': False},
156                  {'option_name': 'CONFIG_NAME_9', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: is off, "0"', 'check_result_bool': False},
157                  {'option_name': 'CONFIG_NAME_10', 'type': 'kconfig', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}]
158         )
159
160     def test_simple_cmdline(self) -> None:
161         # 1. prepare the checklist
162         config_checklist = [] # type: List[ChecklistObjType]
163         config_checklist += [CmdlineCheck('reason_1', 'decision_1', 'name_1', 'expected_1')]
164         config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2')]
165         config_checklist += [CmdlineCheck('reason_3', 'decision_3', 'name_3', 'expected_3')]
166         config_checklist += [CmdlineCheck('reason_4', 'decision_4', 'name_4', 'is not set')]
167         config_checklist += [CmdlineCheck('reason_5', 'decision_5', 'name_5', 'is present')]
168         config_checklist += [CmdlineCheck('reason_6', 'decision_6', 'name_6', 'is present')]
169         config_checklist += [CmdlineCheck('reason_7', 'decision_7', 'name_7', 'is not off')]
170         config_checklist += [CmdlineCheck('reason_8', 'decision_8', 'name_8', 'is not off')]
171         config_checklist += [CmdlineCheck('reason_9', 'decision_9', 'name_9', 'is not off')]
172         config_checklist += [CmdlineCheck('reason_10', 'decision_10', 'name_10', 'is not off')]
173
174         # 2. prepare the parsed cmdline options
175         parsed_cmdline_options  = {}
176         parsed_cmdline_options['name_1'] = 'expected_1'
177         parsed_cmdline_options['name_2'] = 'UNexpected_2'
178         parsed_cmdline_options['name_5'] = ''
179         parsed_cmdline_options['name_7'] = ''
180         parsed_cmdline_options['name_8'] = 'off'
181         parsed_cmdline_options['name_9'] = '0'
182
183         # 3. run the engine
184         self.run_engine(config_checklist, None, parsed_cmdline_options, None, None)
185
186         # 4. check that the results are correct
187         result = [] # type: ResultType
188         self.get_engine_result(config_checklist, result, 'json')
189         self.assertEqual(
190                 result,
191                 [{'option_name': 'name_1', 'type': 'cmdline', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
192                  {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "UNexpected_2"', 'check_result_bool': False},
193                  {'option_name': 'name_3', 'type': 'cmdline', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: is not found', 'check_result_bool': False},
194                  {'option_name': 'name_4', 'type': 'cmdline', 'desired_val': 'is not set', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'OK: is not found', 'check_result_bool': True},
195                  {'option_name': 'name_5', 'type': 'cmdline', 'desired_val': 'is present', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: is present', 'check_result_bool': True},
196                  {'option_name': 'name_6', 'type': 'cmdline', 'desired_val': 'is present', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: is not present', 'check_result_bool': False},
197                  {'option_name': 'name_7', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: is not off, ""', 'check_result_bool': True},
198                  {'option_name': 'name_8', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_8', 'reason': 'reason_8', 'check_result': 'FAIL: is off', 'check_result_bool': False},
199                  {'option_name': 'name_9', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: is off, "0"', 'check_result_bool': False},
200                  {'option_name': 'name_10', 'type': 'cmdline', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}]
201         )
202
203     def test_simple_sysctl(self) -> None:
204         # 1. prepare the checklist
205         config_checklist = [] # type: List[ChecklistObjType]
206         config_checklist += [SysctlCheck('reason_1', 'decision_1', 'name_1', 'expected_1')]
207         config_checklist += [SysctlCheck('reason_2', 'decision_2', 'name_2', 'expected_2')]
208         config_checklist += [SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3')]
209         config_checklist += [SysctlCheck('reason_4', 'decision_4', 'name_4', 'is not set')]
210         config_checklist += [SysctlCheck('reason_5', 'decision_5', 'name_5', 'is present')]
211         config_checklist += [SysctlCheck('reason_6', 'decision_6', 'name_6', 'is present')]
212         config_checklist += [SysctlCheck('reason_7', 'decision_7', 'name_7', 'is not off')]
213         config_checklist += [SysctlCheck('reason_8', 'decision_8', 'name_8', 'is not off')]
214         config_checklist += [SysctlCheck('reason_9', 'decision_9', 'name_9', 'is not off')]
215         config_checklist += [SysctlCheck('reason_10', 'decision_10', 'name_10', 'is not off')]
216
217         # 2. prepare the parsed sysctl options
218         parsed_sysctl_options  = {}
219         parsed_sysctl_options['name_1'] = 'expected_1'
220         parsed_sysctl_options['name_2'] = 'UNexpected_2'
221         parsed_sysctl_options['name_5'] = ''
222         parsed_sysctl_options['name_7'] = ''
223         parsed_sysctl_options['name_8'] = 'off'
224         parsed_sysctl_options['name_9'] = '0'
225
226         # 3. run the engine
227         self.run_engine(config_checklist, None, None, parsed_sysctl_options, None)
228
229         # 4. check that the results are correct
230         result = [] # type: ResultType
231         self.get_engine_result(config_checklist, result, 'json')
232         self.assertEqual(
233                 result,
234                 [{'option_name': 'name_1', 'type': 'sysctl', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
235                  {'option_name': 'name_2', 'type': 'sysctl', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "UNexpected_2"', 'check_result_bool': False},
236                  {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: is not found', 'check_result_bool': False},
237                  {'option_name': 'name_4', 'type': 'sysctl', 'desired_val': 'is not set', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'OK: is not found', 'check_result_bool': True},
238                  {'option_name': 'name_5', 'type': 'sysctl', 'desired_val': 'is present', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: is present', 'check_result_bool': True},
239                  {'option_name': 'name_6', 'type': 'sysctl', 'desired_val': 'is present', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: is not present', 'check_result_bool': False},
240                  {'option_name': 'name_7', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: is not off, ""', 'check_result_bool': True},
241                  {'option_name': 'name_8', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_8', 'reason': 'reason_8', 'check_result': 'FAIL: is off', 'check_result_bool': False},
242                  {'option_name': 'name_9', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: is off, "0"', 'check_result_bool': False},
243                  {'option_name': 'name_10', 'type': 'sysctl', 'desired_val': 'is not off', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: is off, not found', 'check_result_bool': False}]
244         )
245
246     def test_complex_or(self) -> None:
247         # 1. prepare the checklist
248         config_checklist = [] # type: List[ChecklistObjType]
249         config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
250                                 KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'))]
251         config_checklist += [OR(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'),
252                                 KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'))]
253         config_checklist += [OR(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
254                                 KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6'))]
255         config_checklist += [OR(KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'expected_7'),
256                                 KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'is not set'))]
257         config_checklist += [OR(KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'expected_9'),
258                                 KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is present'))]
259         config_checklist += [OR(KconfigCheck('reason_11', 'decision_11', 'NAME_11', 'expected_11'),
260                                 KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'is not off'))]
261
262         # 2. prepare the parsed kconfig options
263         parsed_kconfig_options  = {}
264         parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
265         parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2'
266         parsed_kconfig_options['CONFIG_NAME_3'] = 'UNexpected_3'
267         parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4'
268         parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
269         parsed_kconfig_options['CONFIG_NAME_6'] = 'UNexpected_6'
270         parsed_kconfig_options['CONFIG_NAME_10'] = 'UNexpected_10'
271         parsed_kconfig_options['CONFIG_NAME_12'] = 'really_not_off'
272
273         # 3. run the engine
274         self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)
275
276         # 4. check that the results are correct
277         result = [] # type: ResultType
278         self.get_engine_result(config_checklist, result, 'json')
279         self.assertEqual(
280                 result,
281                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
282                  {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'OK: CONFIG_NAME_4 is "expected_4"', 'check_result_bool': True},
283                  {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'expected_5', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'FAIL: "UNexpected_5"', 'check_result_bool': False},
284                  {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'expected_7', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: CONFIG_NAME_8 is not found', 'check_result_bool': True},
285                  {'option_name': 'CONFIG_NAME_9', 'type': 'kconfig', 'desired_val': 'expected_9', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'OK: CONFIG_NAME_10 is present', 'check_result_bool': True},
286                  {'option_name': 'CONFIG_NAME_11', 'type': 'kconfig', 'desired_val': 'expected_11', 'decision': 'decision_11', 'reason': 'reason_11', 'check_result': 'OK: CONFIG_NAME_12 is not off', 'check_result_bool': True}]
287         )
288
289     def test_complex_and(self) -> None:
290         # 1. prepare the checklist
291         config_checklist = [] # type: List[ChecklistObjType]
292         config_checklist += [AND(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
293                                  KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'))]
294         config_checklist += [AND(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'),
295                                  KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'))]
296         config_checklist += [AND(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
297                                  KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6'))]
298         config_checklist += [AND(KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'expected_7'),
299                                  KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'is present'))]
300         config_checklist += [AND(KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'expected_9'),
301                                  KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'is not off'))]
302         config_checklist += [AND(KconfigCheck('reason_11', 'decision_11', 'NAME_11', 'expected_11'),
303                                  KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'is not off'))]
304
305         # 2. prepare the parsed kconfig options
306         parsed_kconfig_options  = {}
307         parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
308         parsed_kconfig_options['CONFIG_NAME_2'] = 'expected_2'
309         parsed_kconfig_options['CONFIG_NAME_3'] = 'expected_3'
310         parsed_kconfig_options['CONFIG_NAME_4'] = 'UNexpected_4'
311         parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
312         parsed_kconfig_options['CONFIG_NAME_6'] = 'expected_6'
313         parsed_kconfig_options['CONFIG_NAME_7'] = 'expected_7'
314         parsed_kconfig_options['CONFIG_NAME_9'] = 'expected_9'
315         parsed_kconfig_options['CONFIG_NAME_10'] = '0'
316         parsed_kconfig_options['CONFIG_NAME_11'] = 'expected_11'
317
318         # 3. run the engine
319         self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)
320
321         # 4. check that the results are correct
322         result = [] # type: ResultType
323         self.get_engine_result(config_checklist, result, 'json')
324         self.assertEqual(
325                 result,
326                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
327                  {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: CONFIG_NAME_4 is not "expected_4"', 'check_result_bool': False},
328                  {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'expected_5', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'FAIL: "UNexpected_5"', 'check_result_bool': False},
329                  {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'expected_7', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'FAIL: CONFIG_NAME_8 is not present', 'check_result_bool': False},
330                  {'option_name': 'CONFIG_NAME_9', 'type': 'kconfig', 'desired_val': 'expected_9', 'decision': 'decision_9', 'reason': 'reason_9', 'check_result': 'FAIL: CONFIG_NAME_10 is off', 'check_result_bool': False},
331                  {'option_name': 'CONFIG_NAME_11', 'type': 'kconfig', 'desired_val': 'expected_11', 'decision': 'decision_11', 'reason': 'reason_11', 'check_result': 'FAIL: CONFIG_NAME_12 is off, not found', 'check_result_bool': False}]
332         )
333
334     def test_complex_nested(self) -> None:
335         # 1. prepare the checklist
336         config_checklist = [] # type: List[ChecklistObjType]
337         config_checklist += [AND(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
338                                  OR(KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'),
339                                     KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3')))]
340         config_checklist += [AND(KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'),
341                                  OR(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
342                                     KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6')))]
343         config_checklist += [OR(KconfigCheck('reason_7', 'decision_7', 'NAME_7', 'expected_7'),
344                                  AND(KconfigCheck('reason_8', 'decision_8', 'NAME_8', 'expected_8'),
345                                      KconfigCheck('reason_9', 'decision_9', 'NAME_9', 'expected_9')))]
346         config_checklist += [OR(KconfigCheck('reason_10', 'decision_10', 'NAME_10', 'expected_10'),
347                                  AND(KconfigCheck('reason_11', 'decision_11', 'NAME_11', 'expected_11'),
348                                      KconfigCheck('reason_12', 'decision_12', 'NAME_12', 'expected_12')))]
349
350         # 2. prepare the parsed kconfig options
351         parsed_kconfig_options  = {}
352         parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1'
353         parsed_kconfig_options['CONFIG_NAME_2'] = 'UNexpected_2'
354         parsed_kconfig_options['CONFIG_NAME_3'] = 'expected_3'
355         parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4'
356         parsed_kconfig_options['CONFIG_NAME_5'] = 'UNexpected_5'
357         parsed_kconfig_options['CONFIG_NAME_6'] = 'UNexpected_6'
358         parsed_kconfig_options['CONFIG_NAME_7'] = 'UNexpected_7'
359         parsed_kconfig_options['CONFIG_NAME_8'] = 'expected_8'
360         parsed_kconfig_options['CONFIG_NAME_9'] = 'expected_9'
361         parsed_kconfig_options['CONFIG_NAME_10'] = 'UNexpected_10'
362         parsed_kconfig_options['CONFIG_NAME_11'] = 'UNexpected_11'
363         parsed_kconfig_options['CONFIG_NAME_12'] = 'expected_12'
364
365         # 3. run the engine
366         self.run_engine(config_checklist, parsed_kconfig_options, None, None, None)
367
368         # 4. check that the results are correct
369         result = [] # type: ResultType
370         self.get_engine_result(config_checklist, result, 'json')
371         self.assertEqual(
372                 result,
373                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
374                  {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: CONFIG_NAME_5 is not "expected_5"', 'check_result_bool': False},
375                  {'option_name': 'CONFIG_NAME_7', 'type': 'kconfig', 'desired_val': 'expected_7', 'decision': 'decision_7', 'reason': 'reason_7', 'check_result': 'OK: CONFIG_NAME_8 is "expected_8"', 'check_result_bool': True},
376                  {'option_name': 'CONFIG_NAME_10', 'type': 'kconfig', 'desired_val': 'expected_10', 'decision': 'decision_10', 'reason': 'reason_10', 'check_result': 'FAIL: "UNexpected_10"', 'check_result_bool': False}]
377         )
378
379     def test_version(self) -> None:
380         # 1. prepare the checklist
381         config_checklist = [] # type: List[ChecklistObjType]
382         config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
383                                 VersionCheck((41, 101, 0)))]
384         config_checklist += [AND(KconfigCheck('reason_2', 'decision_2', 'NAME_2', 'expected_2'),
385                                  VersionCheck((43, 1, 0)))]
386         config_checklist += [OR(KconfigCheck('reason_3', 'decision_3', 'NAME_3', 'expected_3'),
387                                 VersionCheck((42, 42, 101)))]
388         config_checklist += [AND(KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'),
389                                  VersionCheck((42, 44, 1)))]
390         config_checklist += [OR(KconfigCheck('reason_5', 'decision_5', 'NAME_5', 'expected_5'),
391                                 VersionCheck((42, 43, 44)))]
392         config_checklist += [AND(KconfigCheck('reason_6', 'decision_6', 'NAME_6', 'expected_6'),
393                                  VersionCheck((42, 43, 45)))]
394
395         # 2. prepare the parsed kconfig options
396         parsed_kconfig_options  = {}
397         parsed_kconfig_options['CONFIG_NAME_2'] = 'expected_2'
398         parsed_kconfig_options['CONFIG_NAME_4'] = 'expected_4'
399         parsed_kconfig_options['CONFIG_NAME_6'] = 'expected_6'
400
401         # 3. prepare the kernel version
402         kernel_version = (42, 43, 44)
403
404         # 4. run the engine
405         self.run_engine(config_checklist, parsed_kconfig_options, None, None, kernel_version)
406
407         # 5. check that the results are correct
408         result = [] # type: ResultType
409         self.get_engine_result(config_checklist, result, 'json')
410         self.assertEqual(
411                 result,
412                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK: version >= (41, 101, 0)', 'check_result_bool': True},
413                  {'option_name': 'CONFIG_NAME_2', 'type': 'kconfig', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: version < (43, 1, 0)', 'check_result_bool': False},
414                  {'option_name': 'CONFIG_NAME_3', 'type': 'kconfig', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'OK: version >= (42, 42, 101)', 'check_result_bool': True},
415                  {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: version < (42, 44, 1)', 'check_result_bool': False},
416                  {'option_name': 'CONFIG_NAME_5', 'type': 'kconfig', 'desired_val': 'expected_5', 'decision': 'decision_5', 'reason': 'reason_5', 'check_result': 'OK: version >= (42, 43, 44)', 'check_result_bool': True},
417                  {'option_name': 'CONFIG_NAME_6', 'type': 'kconfig', 'desired_val': 'expected_6', 'decision': 'decision_6', 'reason': 'reason_6', 'check_result': 'FAIL: version < (42, 43, 45)', 'check_result_bool': False}]
418         )
419
420     def test_stdout(self) -> None:
421         # 1. prepare the checklist
422         config_checklist = [] # type: List[ChecklistObjType]
423         config_checklist += [OR(KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1'),
424                                 CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2'),
425                                 SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3'))]
426         config_checklist += [AND(KconfigCheck('reason_4', 'decision_4', 'NAME_4', 'expected_4'),
427                                  CmdlineCheck('reason_5', 'decision_5', 'name_5', 'expected_5'),
428                                  SysctlCheck('reason_6', 'decision_6', 'name_6', 'expected_6'))]
429
430         # 2. prepare the parsed kconfig options
431         parsed_kconfig_options  = {}
432         parsed_kconfig_options['CONFIG_NAME_1'] = 'UNexpected_1'
433
434         # 3. prepare the parsed cmdline options
435         parsed_cmdline_options  = {}
436         parsed_cmdline_options['name_2'] = 'expected_2'
437         parsed_cmdline_options['name_5'] = 'UNexpected_5'
438
439         # 4. prepare the parsed sysctl options
440         parsed_sysctl_options  = {}
441         parsed_sysctl_options['name_6'] = 'expected_6'
442
443         # 5. run the engine
444         self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, None)
445
446         # 6. check that the results are correct
447         json_result = [] # type: ResultType
448         self.get_engine_result(config_checklist, json_result, 'json')
449         self.assertEqual(
450                 json_result,
451                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK: name_2 is "expected_2"', 'check_result_bool': True},
452                  {'option_name': 'CONFIG_NAME_4', 'type': 'kconfig', 'desired_val': 'expected_4', 'decision': 'decision_4', 'reason': 'reason_4', 'check_result': 'FAIL: name_5 is not "expected_5"', 'check_result_bool': False}]
453         )
454
455         stdout_result = [] # type: ResultType
456         self.get_engine_result(config_checklist, stdout_result, 'stdout')
457         self.assertEqual(
458                 stdout_result,
459                 [
460 '\
461 CONFIG_NAME_1                           |kconfig| expected_1 |decision_1|     reason_1     | OK: name_2 is "expected_2"\
462 CONFIG_NAME_4                           |kconfig| expected_4 |decision_4|     reason_4     | FAIL: name_5 is not "expected_5"\
463 '               ]
464         )
465
466         stdout_result = []
467         self.get_engine_result(config_checklist, stdout_result, 'stdout_verbose')
468         self.assertEqual(
469                 stdout_result,
470                 [
471 '\
472     <<< OR >>>                                                                             | OK: name_2 is "expected_2"\n\
473 CONFIG_NAME_1                           |kconfig| expected_1 |decision_1|     reason_1     | FAIL: "UNexpected_1"\n\
474 name_2                                  |cmdline| expected_2 |decision_2|     reason_2     | OK\n\
475 name_3                                  |sysctl | expected_3 |decision_3|     reason_3     | None\
476 '\
477 '\
478     <<< AND >>>                                                                            | FAIL: name_5 is not "expected_5"\n\
479 CONFIG_NAME_4                           |kconfig| expected_4 |decision_4|     reason_4     | None\n\
480 name_5                                  |cmdline| expected_5 |decision_5|     reason_5     | FAIL: "UNexpected_5"\n\
481 name_6                                  |sysctl | expected_6 |decision_6|     reason_6     | OK\
482 '               ]
483         )
484
485     def test_value_overriding(self) -> None:
486         # 1. prepare the checklist
487         config_checklist = [] # type: List[ChecklistObjType]
488         config_checklist += [KconfigCheck('reason_1', 'decision_1', 'NAME_1', 'expected_1')]
489         config_checklist += [CmdlineCheck('reason_2', 'decision_2', 'name_2', 'expected_2')]
490         config_checklist += [SysctlCheck('reason_3', 'decision_3', 'name_3', 'expected_3')]
491
492         # 2. prepare the parsed kconfig options
493         parsed_kconfig_options  = {}
494         parsed_kconfig_options['CONFIG_NAME_1'] = 'expected_1_new'
495
496         # 3. prepare the parsed cmdline options
497         parsed_cmdline_options  = {}
498         parsed_cmdline_options['name_2'] = 'expected_2_new'
499
500         # 4. prepare the parsed sysctl options
501         parsed_sysctl_options  = {}
502         parsed_sysctl_options['name_3'] = 'expected_3_new'
503
504         # 5. run the engine
505         self.run_engine(config_checklist, parsed_kconfig_options, parsed_cmdline_options, parsed_sysctl_options, None)
506
507         # 6. check that the results are correct
508         result = [] # type: ResultType
509         self.get_engine_result(config_checklist, result, 'json')
510         self.assertEqual(
511                 result,
512                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'FAIL: "expected_1_new"', 'check_result_bool': False},
513                  {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "expected_2_new"', 'check_result_bool': False},
514                  {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: "expected_3_new"', 'check_result_bool': False}]
515         )
516
517         # 7. override expected value and perform the checks again
518         override_expected_value(config_checklist, 'CONFIG_NAME_1', 'expected_1_new')
519         perform_checks(config_checklist)
520
521         # 8. check that the results are correct
522         result = []
523         self.get_engine_result(config_checklist, result, 'json')
524         self.assertEqual(
525                 result,
526                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1_new', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
527                  {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'FAIL: "expected_2_new"', 'check_result_bool': False},
528                  {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: "expected_3_new"', 'check_result_bool': False}]
529         )
530
531         # 9. override expected value and perform the checks again
532         override_expected_value(config_checklist, 'name_2', 'expected_2_new')
533         perform_checks(config_checklist)
534
535         # 10. check that the results are correct
536         result = []
537         self.get_engine_result(config_checklist, result, 'json')
538         self.assertEqual(
539                 result,
540                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1_new', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
541                  {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2_new', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'OK', 'check_result_bool': True},
542                  {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'FAIL: "expected_3_new"', 'check_result_bool': False}]
543         )
544
545         # 11. override expected value and perform the checks again
546         override_expected_value(config_checklist, 'name_3', 'expected_3_new')
547         perform_checks(config_checklist)
548
549         # 12. check that the results are correct
550         result = []
551         self.get_engine_result(config_checklist, result, 'json')
552         self.assertEqual(
553                 result,
554                 [{'option_name': 'CONFIG_NAME_1', 'type': 'kconfig', 'desired_val': 'expected_1_new', 'decision': 'decision_1', 'reason': 'reason_1', 'check_result': 'OK', 'check_result_bool': True},
555                  {'option_name': 'name_2', 'type': 'cmdline', 'desired_val': 'expected_2_new', 'decision': 'decision_2', 'reason': 'reason_2', 'check_result': 'OK', 'check_result_bool': True},
556                  {'option_name': 'name_3', 'type': 'sysctl', 'desired_val': 'expected_3_new', 'decision': 'decision_3', 'reason': 'reason_3', 'check_result': 'OK', 'check_result_bool': True}]
557         )