diff --git a/tools/otagui/target_lib.py b/tools/otagui/target_lib.py index 294ace2dd..db9ab9eea 100644 --- a/tools/otagui/target_lib.py +++ b/tools/otagui/target_lib.py @@ -59,11 +59,19 @@ class BuildInfo: pass def to_sql_form_dict(self): + """ + Because sqlite can only store text but self.partitions is a list + Turn the list into a string joined by ',', for example: + ['system', 'vendor'] => 'system,vendor' + """ sql_form_dict = asdict(self) sql_form_dict['partitions'] = ','.join(sql_form_dict['partitions']) return sql_form_dict def to_dict(self): + """ + Return as a normal dict. + """ return asdict(self) diff --git a/tools/otagui/test/test_ab_partitions.txt b/tools/otagui/test/test_ab_partitions.txt new file mode 100644 index 000000000..eb82b690e --- /dev/null +++ b/tools/otagui/test/test_ab_partitions.txt @@ -0,0 +1,2 @@ +system +vendor \ No newline at end of file diff --git a/tools/otagui/test/test_build.prop b/tools/otagui/test/test_build.prop new file mode 100644 index 000000000..9c5761e1d --- /dev/null +++ b/tools/otagui/test/test_build.prop @@ -0,0 +1,21 @@ +ro.build.id=AOSP.MASTER +ro.build.display.id=aosp_cf_x86_64_phone-userdebug S AOSP.MASTER 7392671 test-keys +ro.build.version.incremental=7392671 +ro.build.version.sdk=30 +ro.build.version.preview_sdk=1 +ro.build.version.preview_sdk_fingerprint=5f1ee022916302ff92d66186575d0b95 +ro.build.version.codename=S +ro.build.version.all_codenames=S +ro.build.version.release=11 +ro.build.version.release_or_codename=S +ro.build.version.security_patch=2021-05-05 +ro.build.version.base_os= +ro.build.version.min_supported_target_sdk=23 +ro.build.date=Mon May 24 08:56:05 UTC 2021 +ro.build.date.utc=1621846565 +ro.build.type=userdebug +ro.build.user=android-build +ro.build.host=abfarm069 +ro.build.tags=test-keys +ro.build.flavor=aosp_cf_x86_64_phone-userdebug +ro.build.system_root_image=false \ No newline at end of file diff --git a/tools/otagui/test_suite.py b/tools/otagui/test_suite.py new file mode 100644 index 000000000..f2c375f22 --- /dev/null +++ b/tools/otagui/test_suite.py @@ -0,0 +1,18 @@ +import os, unittest + +class Tests(): + def suite(self): + modules_to_test = [] + test_dir = os.listdir('.') + for test in test_dir: + if test.startswith('test') and test.endswith('.py'): + modules_to_test.append(test.rstrip('.py')) + + alltests = unittest.TestSuite() + for module in map(__import__, modules_to_test): + alltests.addTest(unittest.findTestCases(module)) + return alltests + +if __name__ == '__main__': + MyTests = Tests() + unittest.main(defaultTest='MyTests.suite', verbosity=2) \ No newline at end of file diff --git a/tools/otagui/test_target_lib.py b/tools/otagui/test_target_lib.py new file mode 100644 index 000000000..b2e883ca7 --- /dev/null +++ b/tools/otagui/test_target_lib.py @@ -0,0 +1,192 @@ +import unittest +from unittest.mock import patch, mock_open, Mock, MagicMock +from target_lib import BuildInfo, TargetLib +import zipfile +import os +import sqlite3 +from tempfile import NamedTemporaryFile + +class CreateTestBuild(): + def __init__(self, include_build_prop=True, include_ab_partitions=True): + self.file = NamedTemporaryFile(dir='test/') + self.test_path = self.file.name + with zipfile.ZipFile(self.file, mode='w') as package: + if include_build_prop: + package.write('test/test_build.prop', 'SYSTEM/build.prop') + if include_ab_partitions: + package.write('test/test_ab_partitions.txt', + 'META/ab_partitions.txt') + self.test_info = { + 'file_name': self.test_path.split('/')[-1], + 'path': self.test_path, + 'time': 1628698830, + 'build_id': 'AOSP.MASTER', + 'build_version': '7392671', + 'build_flavor': 'aosp_cf_x86_64_phone-userdebug', + 'partitions': ['system', 'vendor'], + } + if not include_build_prop: + self.test_info['build_id'] = '' + self.test_info['build_version'] = '' + self.test_info['build_flavor'] = '' + if not include_ab_partitions: + self.test_info['partitions'] = [] + + def clean(self): + self.file.close() + +class TestBuildInfo(unittest.TestCase): + def setUp(self): + """ + Create a virtual Android build, which only have build.prop + and ab_partitions.txt + """ + self.test_build = CreateTestBuild() + self.build_info = BuildInfo( + self.test_build.test_info['file_name'], + self.test_build.test_info['path'], + self.test_build.test_info['time'] + ) + self.build_info.analyse_buildprop() + + def tearDown(self): + self.test_build.clean() + + def test_analyse_buildprop(self): + # Test if the build.prop and ab_partitions are not empty + for key, value in self.test_build.test_info.items(): + self.assertEqual(value, self.build_info.__dict__[key], + 'The ' + key + ' is not parsed correctly.' + ) + # Test if the ab_partitions is empty + test_build_no_partitions = CreateTestBuild(include_ab_partitions=False) + build_info = BuildInfo( + test_build_no_partitions.test_info['file_name'], + test_build_no_partitions.test_info['path'], + test_build_no_partitions.test_info['time'] + ) + build_info.analyse_buildprop() + self.assertEqual(build_info.partitions, + test_build_no_partitions.test_info['partitions'], + 'The partition list is not empty if ab_partitions is not provided.' + ) + test_build_no_partitions.clean() + + def test_to_sql_form_dict(self): + sql_dict = self.build_info.to_sql_form_dict() + for key, value in self.test_build.test_info.items(): + if key != 'partitions': + self.assertEqual(value, sql_dict[key], + 'The ' + key + ' is not parsed correctly.' + ) + else: + self.assertEqual(','.join(value), sql_dict[key], + 'The partition list is not coverted to sql form properly.' + ) + + def test_to_dict(self): + ordinary_dict = self.build_info.to_dict() + for key, value in self.test_build.test_info.items(): + self.assertEqual(value, ordinary_dict[key], + 'The ' + key + ' is not parsed correctly.' + ) + + +class TestTargetLib(unittest.TestCase): + def setUp(self): + self.test_path = 'test/test_target_lib.db' + self.tearDown() + self.target_build = TargetLib(path=self.test_path) + + def tearDown(self): + if os.path.isfile(self.test_path): + os.remove(self.test_path) + + def test_init(self): + # Test the database is created successfully + self.assertTrue(os.path.isfile(self.test_path)) + test_columns = [ + {'name': 'FileName','type':'TEXT'}, + {'name': 'Path','type':'TEXT'}, + {'name': 'BuildID','type':'TEXT'}, + {'name': 'BuildVersion','type':'TEXT'}, + {'name': 'BuildFlavor','type':'TEXT'}, + {'name': 'Partitions','type':'TEXT'}, + {'name': 'UploadTime','type':'INTEGER'}, + ] + connect = sqlite3.connect(self.test_path) + cursor = connect.cursor() + cursor.execute("PRAGMA table_info(Builds)") + columns = cursor.fetchall() + for column in test_columns: + column_found = list(filter(lambda x: x[1]==column['name'], columns)) + self.assertEqual(len(column_found), 1, + 'The column ' + column['name'] + ' is not found in database' + ) + self.assertEqual(column_found[0][2], column['type'], + 'The column ' + column['name'] + ' has a wrong type' + ) + + def test_new_build(self): + test_build = CreateTestBuild() + self.target_build.new_build( + filename=test_build.test_info['file_name'], + path=test_build.test_path + ) + connect = sqlite3.connect(self.test_path) + cursor = connect.cursor() + cursor.execute("SELECT * FROM BUILDS") + entries = cursor.fetchall() + self.assertEqual(len(entries), 1, + 'The test build cannot be added into the database.' + ) + test_build.clean() + + def test_get_builds(self): + test_build = CreateTestBuild() + # time.time() has to be mocked, otherwise it will be the current time + mock_time = Mock(return_value=test_build.test_info['time']) + with patch('time.time', mock_time): + self.target_build.new_build( + filename=test_build.test_info['file_name'], + path=test_build.test_path + ) + # build_list is read and parsed from the database + # build_info is directly parsed from the package + # Test if the read/write database process changes the data entry + build_list = self.target_build.get_builds() + build_info = BuildInfo( + test_build.test_info['file_name'], + test_build.test_info['path'], + test_build.test_info['time'] + ) + build_info.analyse_buildprop() + self.assertEqual(build_list[0], build_info, + 'The list of build info cannot be extracted from database.' + ) + test_build.clean() + + def test_get_build_by_path(self): + test_build = CreateTestBuild() + # time.time() has to be mocked, otherwise it will be the current time + mock_time = Mock(return_value=test_build.test_info['time']) + with patch('time.time', mock_time): + self.target_build.new_build( + filename=test_build.test_info['file_name'], + path=test_build.test_path + ) + build = self.target_build.get_build_by_path(test_build.test_info['path']) + build_info = BuildInfo( + test_build.test_info['file_name'], + test_build.test_info['path'], + test_build.test_info['time'] + ) + build_info.analyse_buildprop() + self.assertEqual(build, build_info, + 'Build info cannot be extracted by path.' + ) + test_build.clean() + + +if __name__ == '__main__': + unittest.main()