aboutsummaryrefslogtreecommitdiff
path: root/tests/scripts/mbedtls_test.py
blob: f9e88cf99900b6467bd089605798f38e21ae2864 (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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# Greentea host test script for Mbed TLS on-target test suite testing.
#
# Copyright (C) 2018, Arm Limited, All Rights Reserved
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This file is part of Mbed TLS (https://tls.mbed.org)


"""
Mbed TLS on-target test suite tests are implemented as Greentea
tests. Greentea tests are implemented in two parts: target test and
host test. Target test is a C application that is built for the
target platform and executes on the target. Host test is a Python
class derived from mbed_host_tests.BaseHostTest. Target communicates
with the host over serial for the test data and sends back the result.

Python tool mbedgt (Greentea) is responsible for flashing the test
binary on to the target and dynamically loading this host test module.

Greentea documentation can be found here:
https://github.com/ARMmbed/greentea
"""


import re
import os
import binascii
from mbed_host_tests import BaseHostTest, event_callback


class TestDataParserError(Exception):
    """Indicates error in test data, read from .data file."""
    pass


class TestDataParser(object):
    """
    Parses test name, dependencies, test function name and test parameters
    from the data file.
    """

    def __init__(self):
        """
        Constructor
        """
        self.tests = []

    def parse(self, data_file):
        """
        Data file parser.

        :param data_file: Data file path
        """
        with open(data_file, 'r') as data_f:
            self.__parse(data_f)

    @staticmethod
    def __escaped_split(inp_str, split_char):
        """
        Splits inp_str on split_char except when escaped.

        :param inp_str: String to split
        :param split_char: Split character
        :return: List of splits
        """
        if len(split_char) > 1:
            raise ValueError('Expected split character. Found string!')
        out = re.sub(r'(\\.)|' + split_char,
                     lambda m: m.group(1) or '\n', inp_str,
                     len(inp_str)).split('\n')
        out = [x for x in out if x]
        return out

    def __parse(self, data_f):
        """
        Parses data file using supplied file object.

        :param data_f: Data file object
        :return:
        """
        for line in data_f:
            line = line.strip()
            if not line:
                continue
            # Read test name
            name = line

            # Check dependencies
            dependencies = []
            line = data_f.next().strip()
            match = re.search('depends_on:(.*)', line)
            if match:
                dependencies = [int(x) for x in match.group(1).split(':')]
                line = data_f.next().strip()

            # Read test vectors
            line = line.replace('\\n', '\n')
            parts = self.__escaped_split(line, ':')
            function_name = int(parts[0])
            args = parts[1:]
            args_count = len(args)
            if args_count % 2 != 0:
                raise TestDataParserError("Number of test arguments should "
                                          "be even: %s" % line)
            grouped_args = [(args[i * 2], args[(i * 2) + 1])
                            for i in range(len(args)/2)]
            self.tests.append((name, function_name, dependencies,
                               grouped_args))

    def get_test_data(self):
        """
        Returns test data.
        """
        return self.tests


class MbedTlsTest(BaseHostTest):
    """
    Host test for Mbed TLS unit tests. This script is loaded at
    run time by Greentea for executing Mbed TLS test suites. Each
    communication from the target is received in this object as
    an event, which is then handled by the event handler method
    decorated by the associated event. Ex: @event_callback('GO').

    Target test sends requests for dispatching next test. It reads
    tests from the intermediate data file and sends test function
    identifier, dependency identifiers, expression identifiers and
    the test data in binary form. Target test checks dependencies
    , evaluate integer constant expressions and dispatches the test
    function with received test parameters. After test function is
    finished, target sends the result. This class handles the result
    event and prints verdict in the form that Greentea understands.

    """
    # status/error codes from suites/helpers.function
    DEPENDENCY_SUPPORTED = 0
    KEY_VALUE_MAPPING_FOUND = DEPENDENCY_SUPPORTED
    DISPATCH_TEST_SUCCESS = DEPENDENCY_SUPPORTED

    KEY_VALUE_MAPPING_NOT_FOUND = -1    # Expression Id not found.
    DEPENDENCY_NOT_SUPPORTED = -2       # Dependency not supported.
    DISPATCH_TEST_FN_NOT_FOUND = -3     # Test function not found.
    DISPATCH_INVALID_TEST_DATA = -4     # Invalid parameter type.
    DISPATCH_UNSUPPORTED_SUITE = -5     # Test suite not supported/enabled.

    def __init__(self):
        """
        Constructor initialises test index to 0.
        """
        super(MbedTlsTest, self).__init__()
        self.tests = []
        self.test_index = -1
        self.dep_index = 0
        self.error_str = dict()
        self.error_str[self.DEPENDENCY_SUPPORTED] = \
            'DEPENDENCY_SUPPORTED'
        self.error_str[self.KEY_VALUE_MAPPING_NOT_FOUND] = \
            'KEY_VALUE_MAPPING_NOT_FOUND'
        self.error_str[self.DEPENDENCY_NOT_SUPPORTED] = \
            'DEPENDENCY_NOT_SUPPORTED'
        self.error_str[self.DISPATCH_TEST_FN_NOT_FOUND] = \
            'DISPATCH_TEST_FN_NOT_FOUND'
        self.error_str[self.DISPATCH_INVALID_TEST_DATA] = \
            'DISPATCH_INVALID_TEST_DATA'
        self.error_str[self.DISPATCH_UNSUPPORTED_SUITE] = \
            'DISPATCH_UNSUPPORTED_SUITE'

    def setup(self):
        """
        Setup hook implementation. Reads test suite data file and parses out
        tests.
        """
        binary_path = self.get_config_item('image_path')
        script_dir = os.path.split(os.path.abspath(__file__))[0]
        suite_name = os.path.splitext(os.path.basename(binary_path))[0]
        data_file = ".".join((suite_name, 'datax'))
        data_file = os.path.join(script_dir, '..', 'mbedtls',
                                 suite_name, data_file)
        if os.path.exists(data_file):
            self.log("Running tests from %s" % data_file)
            parser = TestDataParser()
            parser.parse(data_file)
            self.tests = parser.get_test_data()
            self.print_test_info()
        else:
            self.log("Data file not found: %s" % data_file)
            self.notify_complete(False)

    def print_test_info(self):
        """
        Prints test summary read by Greentea to detect test cases.
        """
        self.log('{{__testcase_count;%d}}' % len(self.tests))
        for name, _, _, _ in self.tests:
            self.log('{{__testcase_name;%s}}' % name)

    @staticmethod
    def align_32bit(data_bytes):
        """
        4 byte aligns input byte array.

        :return:
        """
        data_bytes += bytearray((4 - (len(data_bytes))) % 4)

    @staticmethod
    def hex_str_bytes(hex_str):
        """
        Converts Hex string representation to byte array

        :param hex_str: Hex in string format.
        :return: Output Byte array
        """
        if hex_str[0] != '"' or hex_str[len(hex_str) - 1] != '"':
            raise TestDataParserError("HEX test parameter missing '\"':"
                                      " %s" % hex_str)
        hex_str = hex_str.strip('"')
        if len(hex_str) % 2 != 0:
            raise TestDataParserError("HEX parameter len should be mod of "
                                      "2: %s" % hex_str)

        data_bytes = binascii.unhexlify(hex_str)
        return data_bytes

    @staticmethod
    def int32_to_big_endian_bytes(i):
        """
        Coverts i to byte array in big endian format.

        :param i: Input integer
        :return: Output bytes array in big endian or network order
        """
        data_bytes = bytearray([((i >> x) & 0xff) for x in [24, 16, 8, 0]])
        return data_bytes

    def test_vector_to_bytes(self, function_id, dependencies, parameters):
        """
        Converts test vector into a byte array that can be sent to the target.

        :param function_id: Test Function Identifier
        :param dependencies: Dependency list
        :param parameters: Test function input parameters
        :return: Byte array and its length
        """
        data_bytes = bytearray([len(dependencies)])
        if dependencies:
            data_bytes += bytearray(dependencies)
        data_bytes += bytearray([function_id, len(parameters)])
        for typ, param in parameters:
            if typ == 'int' or typ == 'exp':
                i = int(param)
                data_bytes += 'I' if typ == 'int' else 'E'
                self.align_32bit(data_bytes)
                data_bytes += self.int32_to_big_endian_bytes(i)
            elif typ == 'char*':
                param = param.strip('"')
                i = len(param) + 1  # + 1 for null termination
                data_bytes += 'S'
                self.align_32bit(data_bytes)
                data_bytes += self.int32_to_big_endian_bytes(i)
                data_bytes += bytearray(list(param))
                data_bytes += '\0'   # Null terminate
            elif typ == 'hex':
                binary_data = self.hex_str_bytes(param)
                data_bytes += 'H'
                self.align_32bit(data_bytes)
                i = len(binary_data)
                data_bytes += self.int32_to_big_endian_bytes(i)
                data_bytes += binary_data
        length = self.int32_to_big_endian_bytes(len(data_bytes))
        return data_bytes, length

    def run_next_test(self):
        """
        Fetch next test information and execute the test.

        """
        self.test_index += 1
        self.dep_index = 0
        if self.test_index < len(self.tests):
            name, function_id, dependencies, args = self.tests[self.test_index]
            self.run_test(name, function_id, dependencies, args)
        else:
            self.notify_complete(True)

    def run_test(self, name, function_id, dependencies, args):
        """
        Execute the test on target by sending next test information.

        :param name: Test name
        :param function_id: function identifier
        :param dependencies: Dependencies list
        :param args: test parameters
        :return:
        """
        self.log("Running: %s" % name)

        param_bytes, length = self.test_vector_to_bytes(function_id,
                                                        dependencies, args)
        self.send_kv(length, param_bytes)

    @staticmethod
    def get_result(value):
        """
        Converts result from string type to integer
        :param value: Result code in string
        :return: Integer result code. Value is from the test status
                 constants defined under the MbedTlsTest class.
        """
        try:
            return int(value)
        except ValueError:
            ValueError("Result should return error number. "
                       "Instead received %s" % value)

    @event_callback('GO')
    def on_go(self, _key, _value, _timestamp):
        """
        Sent by the target to start first test.

        :param _key: Event key
        :param _value: Value. ignored
        :param _timestamp: Timestamp ignored.
        :return:
        """
        self.run_next_test()

    @event_callback("R")
    def on_result(self, _key, value, _timestamp):
        """
        Handle result. Prints test start, finish required by Greentea
        to detect test execution.

        :param _key: Event key
        :param value: Value. ignored
        :param _timestamp: Timestamp ignored.
        :return:
        """
        int_val = self.get_result(value)
        name, _, _, _ = self.tests[self.test_index]
        self.log('{{__testcase_start;%s}}' % name)
        self.log('{{__testcase_finish;%s;%d;%d}}' % (name, int_val == 0,
                                                     int_val != 0))
        self.run_next_test()

    @event_callback("F")
    def on_failure(self, _key, value, _timestamp):
        """
        Handles test execution failure. That means dependency not supported or
        Test function not supported. Hence marking test as skipped.

        :param _key: Event key
        :param value: Value. ignored
        :param _timestamp: Timestamp ignored.
        :return:
        """
        int_val = self.get_result(value)
        if int_val in self.error_str:
            err = self.error_str[int_val]
        else:
            err = 'Unknown error'
        # For skip status, do not write {{__testcase_finish;...}}
        self.log("Error: %s" % err)
        self.run_next_test()