| #!/usr/bin/env python2 |
| # Copyright 2018 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 os |
| import unittest |
| |
| import mock |
| from six import itervalues |
| from six.moves import xrange |
| |
| import factory_common # pylint: disable=unused-import |
| from cros.factory.hwid.v3 import builder |
| from cros.factory.hwid.v3 import common |
| from cros.factory.hwid.v3.database import Database |
| from cros.factory.hwid.v3 import probe |
| from cros.factory.utils import file_utils |
| |
| |
| _TEST_DATA_PATH = os.path.join(os.path.dirname(__file__), 'testdata') |
| _TEST_DATABASE_PATH = os.path.join(_TEST_DATA_PATH, 'test_builder_db.yaml') |
| |
| |
| class DetermineComponentNameTest(unittest.TestCase): |
| |
| def testMainboard(self): |
| comp_cls = 'mainboard' |
| value = { |
| 'version': 'rev2'} |
| expected = 'rev2' |
| self.assertEqual(expected, builder.DetermineComponentName(comp_cls, value)) |
| |
| def testFirmwareKeys(self): |
| comp_cls = 'firmware_keys' |
| value = { |
| 'key_recovery': |
| 'c14bd720b70d97394257e3e826bd8f43de48d4ed#devkeys/recovery', |
| 'key_root': 'b11d74edd286c144e1135b49e7f0bc20cf041f10#devkeys/rootkey'} |
| expected = 'firmware_keys_dev' |
| self.assertEqual(expected, builder.DetermineComponentName(comp_cls, value)) |
| |
| def testDRAM(self): |
| comp_cls = 'dram' |
| value = { |
| 'part': 'ABCD', |
| 'size': '2048', |
| 'slot': '0', |
| 'timing': 'DDR3-800,DDR3-1066,DDR3-1333,DDR3-1600'} |
| expected = 'ABCD_2048mb_0' |
| self.assertEqual(expected, builder.DetermineComponentName(comp_cls, value)) |
| |
| |
| class BuilderMethodTest(unittest.TestCase): |
| |
| def testFilterSpecialCharacter(self): |
| function = builder.FilterSpecialCharacter |
| self.assertEqual(function(''), 'unknown') |
| self.assertEqual(function('foo bar'), 'foo_bar') |
| self.assertEqual(function('aaa::bbb-ccc'), 'aaa_bbb_ccc') |
| self.assertEqual(function(' aaa::bbb-ccc___'), 'aaa_bbb_ccc') |
| |
| def testPromptAndAsk(self): |
| function = builder.PromptAndAsk |
| with mock.patch('six.moves.input', return_value='') as mock_input: |
| self.assertTrue(function('This is the question.', default_answer=True)) |
| mock_input.assert_called_once_with('This is the question. [Y/n] ') |
| |
| with mock.patch('six.moves.input', return_value='') as mock_input: |
| self.assertFalse(function('This is the question.', default_answer=False)) |
| mock_input.assert_called_once_with('This is the question. [y/N] ') |
| |
| with mock.patch('six.moves.input', return_value='y'): |
| self.assertTrue(function('This is the question.', default_answer=True)) |
| self.assertTrue(function('This is the question.', default_answer=False)) |
| |
| with mock.patch('six.moves.input', return_value='n'): |
| self.assertFalse(function('This is the question.', default_answer=True)) |
| self.assertFalse(function('This is the question.', default_answer=False)) |
| |
| def testChecksumUpdater(self): |
| checksum_updater = builder.ChecksumUpdater() |
| self.assertIsNotNone(checksum_updater) |
| with open(os.path.join(_TEST_DATA_PATH, 'CHECKSUM_TEST'), 'r') as f: |
| checksum_test = f.read() |
| updated = checksum_updater.ReplaceChecksum(checksum_test) |
| with open(os.path.join(_TEST_DATA_PATH, 'CHECKSUM_TEST.golden'), 'r') as f: |
| checksum_test_golden = f.read() |
| self.assertEqual(updated, checksum_test_golden) |
| |
| |
| class DatabaseBuilderTest(unittest.TestCase): |
| |
| def testInit(self): |
| self.assertRaises(ValueError, builder.DatabaseBuilder) |
| |
| # From file. |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| self.assertEqual(db.database, |
| Database.LoadFile(_TEST_DATABASE_PATH, |
| verify_checksum=False)) |
| |
| # From stratch. |
| self.assertRaises(ValueError, builder.DatabaseBuilder, project='PROJ') |
| |
| db = builder.DatabaseBuilder(project='PROJ', image_name='PROTO') |
| self.assertEqual(db.database.project, 'PROJ') |
| self.assertEqual(db.database.GetImageName(0), 'PROTO') |
| |
| def testAddDefaultComponent(self): |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| |
| db.AddDefaultComponent('comp_cls_1') |
| |
| # If the probed results don't contain the component value, the default |
| # component should be returned. |
| bom = probe.GenerateBOMFromProbedResults( |
| db.database, {}, {}, {}, 'normal', False)[0] |
| self.assertEqual(bom.components['comp_cls_1'], ['comp_cls_1_default']) |
| |
| # If the probed results contain a real component value, the default |
| # component shouldn't be returned. |
| bom = probe.GenerateBOMFromProbedResults( |
| db.database, |
| {'comp_cls_1': [{'name': 'comp1', 'values': {'value': "1"}}]}, |
| {}, {}, 'normal', False)[0] |
| self.assertEqual(bom.components['comp_cls_1'], ['comp_1_1']) |
| |
| # One component class can have at most one default component. |
| self.assertRaises(ValueError, db.AddDefaultComponent, 'comp_cls_1') |
| |
| def testAddNullComponent(self): |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| |
| db.AddNullComponent('comp_cls_1') |
| self.assertEqual({0: {'comp_cls_1': ['comp_1_1']}, |
| 1: {'comp_cls_1': ['comp_1_2']}, |
| 2: {'comp_cls_1': []}}, |
| db.database.GetEncodedField('comp_cls_1_field')) |
| |
| # The database already accepts a device without a cpu component. |
| db.AddNullComponent('cpu') |
| self.assertEqual( |
| {0: {'cpu': []}}, db.database.GetEncodedField('cpu_field')) |
| |
| # The given component class was not recorded in the database. |
| db.AddNullComponent('new_component') |
| self.assertEqual({0: {'new_component': []}}, |
| db.database.GetEncodedField('new_component_field')) |
| |
| # Should fail if the encoded field of the specified component class encodes |
| # more than one class of components. |
| self.assertRaises(ValueError, db.AddNullComponent, 'comp_cls_2') |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk', |
| return_value=False) |
| def testUpdateByProbedResultsAddFirmware(self, unused_prompt_and_ask_mock): |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| db.UpdateByProbedResults( |
| {'ro_main_firmware': [{'name': 'generic', 'values': {'hash': '1'}}]}, |
| {}, {}) |
| |
| # Should deprecated the legacy firmwares. |
| self.assertEqual( |
| db.database.GetComponents('ro_main_firmware')['firmware0'].status, |
| common.COMPONENT_STATUS.deprecated) |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk') |
| def testUpdateByProbedResultsWithExtraComponentClasses(self, |
| prompt_and_ask_mock): |
| for add_null_comp in [False, True]: |
| prompt_and_ask_mock.return_value = add_null_comp |
| |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| db.UpdateByProbedResults( |
| {'comp_cls_100': [{'name': 'generic', 'values': {'key1': 'value1'}}, |
| {'name': 'generic', 'values': {'key1': 'value1', |
| 'key2': 'value2'}}, |
| {'name': 'generic', 'values': {'key1': 'value1', |
| 'key3': 'value3'}}, |
| {'name': 'special', 'values': {'key4': 'value4'}}, |
| {'name': 'special', 'values': {'key4': 'value5'}}]}, |
| {}, {}, image_name='NEW_IMAGE') |
| self.assertEqual( |
| sorted([attr.values for attr in itervalues(db.database.GetComponents( |
| 'comp_cls_100'))]), |
| sorted([{'key1': 'value1'}, {'key4': 'value4'}, {'key4': 'value5'}])) |
| |
| self.assertEqual( |
| add_null_comp, |
| {'comp_cls_100': []} in db.database.GetEncodedField( |
| 'comp_cls_100_field').values()) |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk', return_value=False) |
| def testUpdateByProbedResultsWithExtraComponents( |
| self, unused_prompt_and_ask_mock): |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| |
| # {'value': '3'} is the extra component. |
| db.UpdateByProbedResults( |
| {'comp_cls_1': [{'name': 'generic', 'values': {'value': '1'}}, |
| {'name': 'generic', 'values': {'value': '3'}}]}, {}, {}, |
| image_name='NEW_IMAGE') |
| self.assertEqual( |
| sorted([attr.values for attr in itervalues(db.database.GetComponents( |
| 'comp_cls_1'))]), |
| sorted([{'value': '1'}, {'value': '2'}, {'value': '3'}])) |
| |
| self.assertIn({'comp_cls_1': sorted(['comp_1_1', '3'])}, |
| list(db.database.GetEncodedField('comp_cls_1_field') |
| .values())) |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk') |
| def testUpdateByProbedResultsMissingEssentialComponents(self, |
| prompt_and_ask_mock): |
| # If the user answer "N", the null component will be added. |
| prompt_and_ask_mock.return_value = False |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| db.UpdateByProbedResults({}, {}, {}, image_name='NEW_IMAGE') |
| for comp_cls in builder.ESSENTIAL_COMPS: |
| self.assertIn({comp_cls: []}, |
| list(db.database.GetEncodedField(comp_cls + '_field') |
| .values())) |
| |
| # If the user answer "Y", the default component will be added if no null |
| # component is recorded. |
| prompt_and_ask_mock.return_value = True |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| db.UpdateByProbedResults({}, {}, {}, image_name='NEW_IMAGE') |
| for comp_cls in builder.ESSENTIAL_COMPS: |
| if {comp_cls: []} in db.database.GetEncodedField( |
| comp_cls + '_field').values(): |
| continue |
| self.assertIn(comp_cls + '_default', db.database.GetComponents(comp_cls)) |
| self.assertIn({comp_cls: [comp_cls + '_default']}, |
| list(db.database.GetEncodedField(comp_cls + '_field') |
| .values())) |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk', return_value=False) |
| def testUpdateByProbedResultsUpdateEncodedFieldsAndPatternCorrectly( |
| self, unused_prompt_and_ask_mock): |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| |
| # Add a lot of mainboard so that the field need more bits. |
| for i in xrange(10): |
| db.UpdateByProbedResults( |
| {'mainboard': [{'name': 'generic', 'values': {'rev': str(i)}}]}, |
| {}, {}) |
| |
| # Add a lot of cpu so that the field need more bits. |
| for i in xrange(50): |
| db.UpdateByProbedResults( |
| {'cpu': [{'name': 'generic', 'values': {'vendor': str(i)}}]}, {}, {}) |
| |
| # Add more component combination of comp_cls_1, comp_cls_2 and comp_cls_3. |
| # Also add an extran component class to trigger adding a new pattern. |
| db.UpdateByProbedResults( |
| {'comp_cls_1': [{'name': 'generic', 'values': {'value': '1'}}, |
| {'name': 'generic', 'values': {'value': '3'}}], |
| 'comp_cls_2': [{'name': 'generic', 'values': {'value': '2'}}], |
| 'comp_cls_3': [{'name': 'generic', 'values': {'value': '1'}}], |
| 'comp_cls_100': [{'name': 'generic', 'values': {'value': '100'}}]}, |
| {}, {}, image_name='NEW_IMAGE') |
| |
| self.assertEqual( |
| db.database.GetEncodedField('comp_cls_23_field'), |
| {0: {'comp_cls_2': ['comp_2_1'], 'comp_cls_3': ['comp_3_1']}, |
| 1: {'comp_cls_2': ['comp_2_2'], 'comp_cls_3': ['comp_3_2']}, |
| 2: {'comp_cls_2': [], 'comp_cls_3': []}, |
| 3: {'comp_cls_2': ['comp_2_2'], 'comp_cls_3': ['comp_3_1']}}) |
| |
| # Check the pattern by checking if the fields bit length are all correct. |
| self.assertEqual(db.database.GetEncodedFieldsBitLength(), |
| {'mainboard_field': 8, |
| 'region_field': 5, |
| 'dram_field': 3, |
| 'cpu_field': 10, |
| 'storage_field': 3, |
| 'chassis_field': 0, |
| 'firmware_keys_field': 1, |
| 'ro_main_firmware_field': 5, |
| 'comp_cls_1_field': 2, |
| 'comp_cls_23_field': 2, |
| 'comp_cls_100_field': 0}) |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk', return_value=False) |
| def testUpdateByProbedResultsNoNeedNewPattern( |
| self, unused_prompt_and_ask_mock): |
| # No matter if new image name is specified, the pattern will always use |
| # the same one if no new encoded fields are added. |
| for image_name in [None, 'EVT', 'NEW_IMAGE_NAME']: |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| db.UpdateByProbedResults( |
| {'comp_cls_2': [{'name': 'generic', 'values': {str(x): str(x)}} |
| for x in xrange(10)]}, |
| {}, {}, image_name=image_name) |
| self.assertEqual(db.database.GetBitMapping(0), |
| db.database.GetBitMapping(db.database.max_image_id)) |
| |
| @mock.patch('cros.factory.hwid.v3.builder.PromptAndAsk', return_value=False) |
| def testUpdateByProbedResultsNeedNewPattern(self, unused_prompt_and_ask_mock): |
| # New pattern is required if new encoded fields are added. |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| |
| db.UpdateByProbedResults( |
| {'comp_cls_200': [{'name': 'generic', 'values': {str(x): str(x)}} |
| for x in xrange(10)]}, |
| {}, {}, image_name='NEW_IMAGE_NAME') |
| self.assertNotIn('comp_cls_200_field', |
| db.database.GetEncodedFieldsBitLength(0)) |
| self.assertIn('comp_cls_200_field', |
| db.database.GetEncodedFieldsBitLength()) |
| self.assertIn('NEW_IMAGE_NAME', |
| db.database.GetImageName(db.database.max_image_id)) |
| |
| # Should raise error if new image is needed but no image name. |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| self.assertRaises(ValueError, db.UpdateByProbedResults, |
| {'comp_cls_200': [{'name': 'x', 'values': {'a': 'b'}}]}, |
| {}, {}) |
| |
| def testRender(self): |
| db = builder.DatabaseBuilder(database_path=_TEST_DATABASE_PATH) |
| path = file_utils.CreateTemporaryFile() |
| db.Render(path) |
| |
| # Should be able to load successfully and pass the checksum check. |
| Database.LoadFile(path) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |