| # Copyright 2016 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import logging |
| import os |
| import tempfile |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.server import test |
| from autotest_lib.server import utils |
| |
| |
| class brillo_HWRandom(test.test): |
| """Tests that /dev/hw_random is present and passes basic tests.""" |
| version = 1 |
| |
| # Basic info for a dieharder test. |
| TestInfo = collections.namedtuple('TestInfo', 'number custom_args') |
| |
| # Basic results of a dieharder test. |
| TestResult = collections.namedtuple('TestResult', 'test_name assessment') |
| |
| # Results of a test suite run. |
| TestSuiteResult = collections.namedtuple('TestSuiteResult', |
| 'num_weak num_failed full_output') |
| |
| # A list of dieharder tests that can be reasonably constrained to run within |
| # a sample space of <= 10MB, and the arguments to constrain them. These have |
| # been applied somewhat naively and over time these can be tweaked if a test |
| # has a problematic failure rate. In general, since there is only so much |
| # that can be done within the constraints these tests should be viewed as a |
| # sanity check and not as a measure of entropy quality. If a hardware RNG |
| # repeatedly fails this test, it has a big problem and should not be used. |
| _TEST_LIST = [ |
| TestInfo(number=0, custom_args=['-p', '50']), |
| TestInfo(number=1, custom_args=['-p', '50', '-t', '50000']), |
| TestInfo(number=2, custom_args=['-p', '50', '-t', '1000']), |
| TestInfo(number=3, custom_args=['-p', '50', '-t', '5000']), |
| TestInfo(number=8, custom_args=['-p', '40']), |
| TestInfo(number=10, custom_args=[]), |
| TestInfo(number=11, custom_args=[]), |
| TestInfo(number=12, custom_args=[]), |
| TestInfo(number=15, custom_args=['-p', '50', '-t', '50000']), |
| TestInfo(number=16, custom_args=['-p', '50', '-t', '7000']), |
| TestInfo(number=17, custom_args=['-p', '50', '-t', '20000']), |
| TestInfo(number=100, custom_args=['-p', '50', '-t', '50000']), |
| TestInfo(number=101, custom_args=['-p', '50', '-t', '50000']), |
| TestInfo(number=102, custom_args=['-p', '50', '-t', '50000']), |
| TestInfo(number=200, custom_args=['-p', '20', '-t', '20000', |
| '-n', '3']), |
| TestInfo(number=202, custom_args=['-p', '20', '-t', '20000']), |
| TestInfo(number=203, custom_args=['-p', '50', '-t', '50000']), |
| TestInfo(number=204, custom_args=['-p', '200']), |
| TestInfo(number=205, custom_args=['-t', '512000']), |
| TestInfo(number=206, custom_args=['-t', '40000', '-n', '64']), |
| TestInfo(number=207, custom_args=['-t', '300000']), |
| TestInfo(number=208, custom_args=['-t', '400000']), |
| TestInfo(number=209, custom_args=['-t', '2000000']), |
| ] |
| |
| def _run_dieharder_test(self, input_file, test_number, custom_args=None): |
| """Runs a specific dieharder test (locally) and returns the assessment. |
| |
| @param input_file: The name of the file containing the data to be tested |
| @param test_number: A dieharder test number specifying which test to run |
| @param custom_args: Optional additional arguments for the test |
| |
| @returns A list of TestResult |
| |
| @raise TestError: An error occurred running the test. |
| """ |
| command = ['dieharder', |
| '-g', '201', |
| '-D', 'test_name', |
| '-D', 'ntuple', |
| '-D', 'assessment', |
| '-D', '32768', # no_whitespace |
| '-c', ',', |
| '-d', str(test_number), |
| '-f', input_file] |
| if custom_args: |
| command.extend(custom_args) |
| command_result = utils.run(command) |
| if command_result.stderr != '': |
| raise error.TestError('Error running dieharder: %s' % |
| command_result.stderr.rstrip()) |
| output = command_result.stdout.splitlines() |
| results = [] |
| for line in output: |
| fields = line.split(',') |
| if len(fields) != 3: |
| raise error.TestError( |
| 'dieharder: unexpected output: %s' % line) |
| results.append(self.TestResult( |
| test_name='%s[%s]' % (fields[0], fields[1]), |
| assessment=fields[2])) |
| return results |
| |
| |
| def _run_all_dieharder_tests(self, input_file): |
| """Runs all the dieharder tests in _TEST_LIST, continuing on failure. |
| |
| @param input_file: The name of the file containing the data to be tested |
| |
| @returns TestSuiteResult |
| |
| @raise TestError: An error occurred running the test. |
| """ |
| weak = 0 |
| failed = 0 |
| full_output = 'Test Results:\n' |
| for test_info in self._TEST_LIST: |
| results = self._run_dieharder_test(input_file, |
| test_info.number, |
| test_info.custom_args) |
| for test_result in results: |
| logging.info('%s: %s', test_result.test_name, |
| test_result.assessment) |
| full_output += ' %s: %s\n' % test_result |
| if test_result.assessment == 'WEAK': |
| weak += 1 |
| elif test_result.assessment == 'FAILED': |
| failed += 1 |
| elif test_result.assessment != 'PASSED': |
| raise error.TestError( |
| 'Unexpected output: %s' % full_output) |
| logging.info('Total: %d, Weak: %d, Failed: %d', |
| len(self._TEST_LIST), weak, failed) |
| return self.TestSuiteResult(weak, failed, full_output) |
| |
| def run_once(self, host=None): |
| """Runs the test. |
| |
| @param host: A host object representing the DUT. |
| |
| @raise TestError: An error occurred running the test. |
| @raise TestFail: The test ran without error but failed. |
| """ |
| # Grab 10MB of data from /dev/hw_random. |
| dut_file = '/data/local/tmp/hw_random_output' |
| host.run('dd count=20480 if=/dev/hw_random of=%s' % dut_file) |
| with tempfile.NamedTemporaryFile() as local_file: |
| host.get_file(dut_file, local_file.name) |
| output_size = os.stat(local_file.name).st_size |
| if output_size != 0xA00000: |
| raise error.TestError( |
| 'Unexpected output length: %d (expecting %d)', |
| output_size, 0xA00000) |
| # Run the data through each test (even if one fails). |
| result = self._run_all_dieharder_tests(local_file.name) |
| if result.num_failed > 0 or result.num_weak > 5: |
| raise error.TestFail(result.full_output) |