# 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)