From b286105d5fd2ce71b028eb0a5ac8d494cd78a024 Mon Sep 17 00:00:00 2001 From: Jan Beran Date: Mon, 17 Jun 2024 11:25:39 +0200 Subject: [PATCH] feat: Add unit tests for new wrapper commands --- .codespellrc | 2 +- tools/idf_py_actions/serial_ext.py | 40 ++++--- tools/test_idf_py/test_idf_py.py | 177 +++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 15 deletions(-) diff --git a/.codespellrc b/.codespellrc index b79ebe6b0a..211f5a2272 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] skip = build,*.yuv,components/fatfs/src/*,alice.txt,*.rgb,components/wpa_supplicant/*,components/esp_wifi/* -ignore-words-list = ser,dout,rsource,fram,inout,shs,ans,aci,unstall,unstalling,hart,wheight,wel,ot,fane +ignore-words-list = ser,dout,rsource,fram,inout,shs,ans,aci,unstall,unstalling,hart,wheight,wel,ot,fane,assertIn write-changes = true diff --git a/tools/idf_py_actions/serial_ext.py b/tools/idf_py_actions/serial_ext.py index 71b649b7c9..96f1025898 100644 --- a/tools/idf_py_actions/serial_ext.py +++ b/tools/idf_py_actions/serial_ext.py @@ -9,7 +9,6 @@ from typing import Any from typing import Dict from typing import List from typing import Optional -from typing import Tuple import click from idf_py_actions.global_options import global_options @@ -405,7 +404,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: if version: generate_signing_key_args += ['--version', version] if scheme: - generate_signing_key_args += ['--scheme', '2'] + generate_signing_key_args += ['--scheme', scheme] if extra_args['keyfile']: generate_signing_key_args += [extra_args['keyfile']] RunTool('espsecure', generate_signing_key_args, args.build_dir)() @@ -440,10 +439,13 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: def _parse_efuse_args(ctx: click.core.Context, args: PropertyDict, extra_args: Dict) -> List: efuse_args = [] - efuse_args += ['-p', args.port or get_default_serial_port()] - if args.baud: - efuse_args += ['-b', str(args.baud)] + if args.port: + efuse_args += ['-p', args.port] + elif not args.port and not extra_args['virt']: # if --virt, no port will be found and it would cause error + efuse_args += ['-p', get_default_serial_port()] efuse_args += ['--chip', _get_project_desc(ctx, args)['target']] + if extra_args['virt']: + efuse_args += ['--virt'] if extra_args['before']: efuse_args += ['--before', extra_args['before'].replace('-', '_')] if extra_args['debug']: @@ -470,8 +472,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: burn_key_args += ['--force-write-always'] if extra_args['show_sensitive_info']: burn_key_args += ['--show-sensitive-info'] - if extra_args['image']: - burn_key_args.append(extra_args['image']) + if extra_args['efuse_positional_args']: + burn_key_args += extra_args['efuse_positional_args'] RunTool('espefuse.py', burn_key_args, args.project_dir, build_dir=args.build_dir)() def efuse_dump(action: str, ctx: click.core.Context, args: PropertyDict, file_name: str, **extra_args: Dict) -> None: @@ -490,14 +492,14 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: read_protect_args += list(extra_args['efuse_positional_args']) RunTool('espefuse', read_protect_args, args.build_dir)() - def efuse_summary(action: str, ctx: click.core.Context, args: PropertyDict, format: str, **extra_args: Tuple) -> None: + def efuse_summary(action: str, ctx: click.core.Context, args: PropertyDict, format: str, **extra_args: Dict) -> None: ensure_build_directory(args, ctx.info_name) summary_args = [PYTHON, '-m' 'espefuse', 'summary'] summary_args += _parse_efuse_args(ctx, args, extra_args) if format: - summary_args += ['--format', format.replace('-', '_')] - if extra_args['efuses']: - summary_args += extra_args['efuse_name'] + summary_args += [f'--format={format.replace("-", "_")}'] + if extra_args['efuse_name']: + summary_args += [str(extra_args['efuse_name'])] RunTool('espefuse', summary_args, args.build_dir)() def efuse_write_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: Dict) -> None: @@ -524,7 +526,13 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: } ] - EFUSE_OPTS = BAUD_AND_PORT + [ + EFUSE_OPTS = [PORT] + [ + { + 'names': ['--virt'], + 'is_flag': True, + 'hidden': True, + 'help': 'For host tests, the tool will work in the virtual mode (without connecting to a chip).', + }, { 'names': ['--before'], 'help': 'What to do before connecting to the chip.', @@ -802,6 +810,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: 'options': EFUSE_OPTS + [ { 'names': ['--no-protect-key'], + 'is_flag': True, 'help': ( 'Disable default read- and write-protecting of the key.' 'If this option is not set, once the key is flashed it cannot be read back or changed.' @@ -809,6 +818,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: }, { 'names': ['--force-write-always'], + 'is_flag': True, 'help': ( "Write the eFuse even if it looks like it's already been written, or is write protected." "Note that this option can't disable write protection, or clear any bit which has already been set." @@ -816,6 +826,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: }, { 'names': ['--show-sensitive-info'], + 'is_flag': True, 'help': ( 'Show data to be burned (may expose sensitive data). Enabled if --debug is used.' ), @@ -823,8 +834,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: ], 'arguments': [ { - 'names': ['image'], - 'nargs': 1, + 'names': ['efuse-positional-args'], + 'nargs': -1, }, ], }, @@ -866,6 +877,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict: { 'names': ['efuse-name'], 'nargs': 1, + 'required': False, }, ], }, diff --git a/tools/test_idf_py/test_idf_py.py b/tools/test_idf_py/test_idf_py.py index 0a77a0ab97..e104651748 100755 --- a/tools/test_idf_py/test_idf_py.py +++ b/tools/test_idf_py/test_idf_py.py @@ -360,5 +360,182 @@ class TestFileArgumentExpansion(TestCase): self.assertIn('(expansion of @args_non_existent) could not be opened', cm.exception.output.decode('utf-8', 'ignore')) +class TestWrapperCommands(TestCase): + @classmethod + def setUpClass(cls): + cls.sample_project_dir = os.path.join(current_dir, '..', 'test_build_system', 'build_test_app') + os.chdir(cls.sample_project_dir) + super().setUpClass() + + def call_command(self, command: List[str]) -> str: + try: + output = subprocess.check_output( + command, + env=os.environ, + stderr=subprocess.STDOUT).decode('utf-8', 'ignore') + return output + except subprocess.CalledProcessError as e: + self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}') + + @classmethod + def tearDownClass(cls): + subprocess.run([sys.executable, idf_py_path, 'fullclean'], stdout=subprocess.DEVNULL) + os.chdir(current_dir) + super().tearDownClass() + + +class TestEFuseCommands(TestWrapperCommands): + """ + Test if wrapper commands for espefuse.py are working as expected. + The goal is NOT to test the functionality of espefuse.py, but to test if the wrapper commands are working as expected. + """ + + def test_efuse_summary(self): + summary_command = [sys.executable, idf_py_path, 'efuse-summary', '--virt'] + output = self.call_command(summary_command) + self.assertIn('EFUSE_NAME (Block) Description = [Meaningful Value] [Readable/Writeable] (Hex Value)', output) + + output = self.call_command(summary_command + ['--format','summary']) + self.assertIn('00:00:00:00:00:00', output) + self.assertIn('MAC address', output) + + output = self.call_command(summary_command + ['--format','value-only', 'WR_DIS']) + self.assertIn('0', output) + + def test_efuse_burn(self): + burn_command = [sys.executable, idf_py_path, 'efuse-burn', '--virt', '--do-not-confirm'] + output = self.call_command(burn_command + ['WR_DIS', '1']) + self.assertIn('\'WR_DIS\' (Efuse write disable mask) 0x0000 -> 0x0001', output) + self.assertIn('Successful', output) + + output = self.call_command(burn_command + ['WR_DIS', '1', 'RD_DIS', '1']) + self.assertIn('WR_DIS', output) + self.assertIn('RD_DIS', output) + self.assertIn('Successful', output) + + def test_efuse_burn_key(self): + key_name = 'efuse_test_key.bin' + subprocess.run([sys.executable, idf_py_path, 'secure-generate-flash-encryption-key', os.path.join(current_dir, key_name)], stdout=subprocess.DEVNULL) + burn_key_command = [sys.executable, idf_py_path, 'efuse-burn-key', '--virt', '--do-not-confirm'] + output = self.call_command(burn_key_command + ['--show-sensitive-info', 'secure_boot_v1', os.path.join(current_dir, key_name)]) + self.assertIn('Burn keys to blocks:', output) + self.assertIn('Successful', output) + + def test_efuse_dump(self): + dump_command = [sys.executable, idf_py_path, 'efuse-dump', '--virt'] + output = self.call_command(dump_command) + self.assertIn('BLOCK0', output) + self.assertIn('BLOCK1', output) + self.assertIn('BLOCK2', output) + self.assertIn('BLOCK3', output) + self.assertIn('read_regs', output) + + def test_efuse_read_protect(self): + read_protect_command = [sys.executable, idf_py_path, 'efuse-read-protect', '--virt', '--do-not-confirm'] + output = self.call_command(read_protect_command + ['MAC_VERSION']) + self.assertIn('MAC_VERSION', output) + self.assertIn('Successful', output) + + def test_efuse_write_protect(self): + write_protect_command = [sys.executable, idf_py_path, 'efuse-write-protect', '--virt', '--do-not-confirm'] + output = self.call_command(write_protect_command + ['WR_DIS']) + self.assertIn('WR_DIS', output) + self.assertIn('Successful', output) + + +class TestSecureCommands(TestWrapperCommands): + """ + Test if wrapper commands for espsecure.py are working as expected. + The goal is NOT to test the functionality of espsecure.py, but to test if the wrapper commands are working as expected. + """ + @classmethod + def setUpClass(cls): + super().setUpClass() + subprocess.run([sys.executable, idf_py_path, 'build'], stdout=subprocess.DEVNULL) + cls.flash_encryption_key = 'test_key.bin' + cls.signing_key = 'test_signing_key.pem' + + def secure_generate_flash_encryption_key(self): + generate_key_command = [sys.executable, idf_py_path, 'secure-generate-flash-encryption-key', self.flash_encryption_key] + output = self.call_command(generate_key_command) + self.assertIn(f'Writing 256 random bits to key file {self.flash_encryption_key}', output) + + def secure_encrypt_flash_data(self): + self.secure_generate_flash_encryption_key() + encrypt_command = [sys.executable, + idf_py_path, + 'secure-encrypt-flash-data', + '--aes-xts', + '--keyfile', + f'{self.flash_encryption_key}', + '--address', + '0x1000', + '--output', + 'bootloader-enc.bin', + 'bootloader/bootloader.bin'] + output = self.call_command(encrypt_command) + self.assertIn('Using 256-bit key', output) + self.assertIn('Done', output) + + def test_secure_decrypt_flash_data(self): + self.secure_encrypt_flash_data() + decrypt_command = [sys.executable, + idf_py_path, + 'secure-decrypt-flash-data', + '--aes-xts', + '--keyfile', + f'{self.flash_encryption_key}', + '--address', + '0x1000', + '--output', + 'bootloader-dec.bin', + 'bootloader-enc.bin'] + output = self.call_command(decrypt_command) + self.assertIn('Using 256-bit key', output) + self.assertIn('Done', output) + + def secure_generate_signing_key(self): + generate_key_command = [sys.executable, + idf_py_path, + 'secure-generate-signing-key', + '--version', + '2', + '--scheme', + 'rsa3072', + self.signing_key] + output = self.call_command(generate_key_command) + self.assertIn(f'RSA 3072 private key in PEM format written to {self.signing_key}', output) + self.assertIn('Done', output) + + def secure_sign_data(self): + self.secure_generate_signing_key() + sign_command = [sys.executable, + idf_py_path, + 'secure-sign-data', + '--version', + '2', + '--keyfile', + self.signing_key, + '--output', + 'bootloader-signed.bin', + 'bootloader/bootloader.bin'] + output = self.call_command(sign_command) + self.assertIn('Signed', output) + + +class TestMergeBinCommands(TestWrapperCommands): + """ + Test if merge-bin command is invoked as expected. + This test is not testing the functionality of esptool.py merge_bin command, but the invocation of the command from idf.py. + """ + + def test_merge_bin(self): + merge_bin_command = [sys.executable, idf_py_path, 'merge-bin'] + merged_binary_name = 'test-merge-binary.bin' + output = self.call_command(merge_bin_command + ['--output', merged_binary_name]) + self.assertIn(f'file {merged_binary_name}, ready to flash to offset 0x0', output) + self.assertIn(f'Merged binary {merged_binary_name} will be created in the build directory...', output) + + if __name__ == '__main__': main()