diff --git a/.github/workflows/test_esptool.yml b/.github/workflows/test_esptool.yml index e4de82ef0..a242a81ff 100644 --- a/.github/workflows/test_esptool.yml +++ b/.github/workflows/test_esptool.yml @@ -23,11 +23,11 @@ jobs: run: | python setup.py build pip install --extra-index-url https://dl.espressif.com/pypi -e .[dev] - python test/test_imagegen.py - python test/test_espsecure.py - python test/test_merge_bin.py - python test/test_image_info.py - python test/test_modules.py + pytest test/test_imagegen.py + pytest test/test_espsecure.py + pytest test/test_merge_bin.py + pytest test/test_image_info.py + pytest test/test_modules.py - name: Check the installed versions can run run: | diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 695d1919c..4115179e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,12 +49,13 @@ host_tests: variables: PYTHONPATH: "$PYTHONPATH:${CI_PROJECT_DIR}/test" COVERAGE_PROCESS_START: "${CI_PROJECT_DIR}/test/.covconf" + PYTEST_ADDOPTS: "-sv --junitxml=test/report.xml --color=yes" script: - - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_imagegen.py - - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_espsecure.py - - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_merge_bin.py - - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_image_info.py - - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_modules.py + - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_imagegen.py + - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_espsecure.py + - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_merge_bin.py + - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_image_info.py + - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_modules.py - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_espefuse_host.py esp32 - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_espefuse_host.py esp32c2 - coverage run --parallel-mode ${CI_PROJECT_DIR}/test/test_espefuse_host.py esp32c3 @@ -98,6 +99,8 @@ check_python_style: check_install_coverage: <<: *test_template artifacts: + reports: + junit: test/report.xml when: always paths: - "**/.coverage*" @@ -271,7 +274,9 @@ combine_reports: - host_test artifacts: reports: - cobertura: cobertura_report.xml + coverage_report: + coverage_format: cobertura + path: cobertura_report.xml when: always paths: - ".coverage*" diff --git a/setup.py b/setup.py index 6a0fd985c..3f881f53a 100644 --- a/setup.py +++ b/setup.py @@ -114,6 +114,7 @@ def find_version(*file_paths): "coverage~=6.0", "black", "pre-commit", + "pytest", ], }, install_requires=[ diff --git a/test/test_espsecure.py b/test/test_espsecure.py index 995cd64d5..6ba6ce994 100755 --- a/test/test_espsecure.py +++ b/test/test_espsecure.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python -# -# Tests for espsecure.py +# Tests for espsecure.py using the pytest framework # # Assumes openssl binary is in the PATH @@ -11,12 +9,13 @@ import subprocess import sys import tempfile -import unittest from collections import namedtuple TEST_DIR = os.path.abspath(os.path.dirname(__file__)) os.chdir(TEST_DIR) +ESPSECURE_PY = os.path.join(TEST_DIR, "..", "espsecure/__init__.py") + try: import espsecure except ImportError: @@ -25,10 +24,10 @@ import esptool -from test_esptool import ESPSECURE_PY +import pytest -class EspSecureTestCase(unittest.TestCase): +class EspSecureTestCase: def run_espsecure(self, args): """ Run espsecure.py with the specified arguments @@ -37,32 +36,34 @@ def run_espsecure(self, args): raises an exception if espsecure.py fails """ cmd = [sys.executable, ESPSECURE_PY] + args.split(" ") - print("Running %s..." % (" ".join(cmd))) + print("\nExecuting {}...".format(" ".join(cmd))) try: output = subprocess.check_output( [str(s) for s in cmd], cwd=TEST_DIR, stderr=subprocess.STDOUT ) - print(output) + print(output.decode("utf-8")) return output.decode("utf-8") except subprocess.CalledProcessError as e: - print(e.output) + print(e.output.decode("utf-8")) raise e - def setUp(self): + @classmethod + def setup_class(self): self.cleanup_files = [] # keep a list of files _open()ed by each test case - def tearDown(self): + @classmethod + def teardown_class(self): for f in self.cleanup_files: f.close() def _open(self, image_file): - f = open(os.path.join("secure_images", image_file), "rb") + f = open(os.path.join(TEST_DIR, "secure_images", image_file), "rb") self.cleanup_files.append(f) return f -class ESP32SecureBootloaderTests(EspSecureTestCase): +class TestESP32SecureBootloader(EspSecureTestCase): def test_digest_bootloader(self): DBArgs = namedtuple( "digest_bootloader_args", ["keyfile", "output", "iv", "image"] @@ -82,7 +83,7 @@ def test_digest_bootloader(self): with open(output_file.name, "rb") as of: with self._open("bootloader_digested.bin") as ef: - self.assertEqual(ef.read(), of.read()) + assert ef.read() == of.read() finally: os.unlink(output_file.name) @@ -100,12 +101,12 @@ def test_digest_rsa_public_key(self): with open(output_file.name, "rb") as of: with self._open("rsa_public_key_digest.bin") as ef: - self.assertEqual(ef.read(), of.read()) + assert ef.read() == of.read() finally: os.unlink(output_file.name) -class SigningTests(EspSecureTestCase): +class TestSigning(EspSecureTestCase): VerifyArgs = namedtuple("verify_signature_args", ["version", "keyfile", "datafile"]) @@ -121,29 +122,18 @@ class SigningTests(EspSecureTestCase): GenerateKeyArgs = namedtuple("generate_key_args", ["version", "scheme", "keyfile"]) def test_key_generation_v1(self): - # tempfile.TemporaryDirectory() would be better - # but we need compatibility with old Pythons - keydir = tempfile.mkdtemp() - self.addCleanup(os.rmdir, keydir) - - # keyfile cannot exist before generation -> tempfile.NamedTemporaryFile() - # cannot be used for keyfile - keyfile_name = os.path.join(keydir, "key.pem") - self.addCleanup(os.remove, keyfile_name) - self.run_espsecure("generate_signing_key --version 1 {}".format(keyfile_name)) + with tempfile.TemporaryDirectory() as keydir: + # keyfile cannot exist before generation -> tempfile.NamedTemporaryFile() + # cannot be used for keyfile + keyfile_name = os.path.join(keydir, "key.pem") + self.run_espsecure(f"generate_signing_key --version 1 {keyfile_name}") def test_key_generation_v2(self): - # tempfile.TemporaryDirectory() would be better - # but we need compatibility with old Pythons - keydir = tempfile.mkdtemp() - self.addCleanup(os.rmdir, keydir) - - # keyfile cannot exist before generation -> tempfile.NamedTemporaryFile() - # cannot be used for keyfile - keyfile_name = os.path.join(keydir, "key.pem") - self.addCleanup(os.remove, keyfile_name) - - self.run_espsecure("generate_signing_key --version 2 {}".format(keyfile_name)) + with tempfile.TemporaryDirectory() as keydir: + # keyfile cannot exist before generation -> tempfile.NamedTemporaryFile() + # cannot be used for keyfile + keyfile_name = os.path.join(keydir, "key.pem") + self.run_espsecure(f"generate_signing_key --version 2 {keyfile_name}") def _test_sign_v1_data(self, key_name): try: @@ -163,7 +153,7 @@ def _test_sign_v1_data(self, key_name): with open(output_file.name, "rb") as of: with self._open("bootloader_signed.bin") as ef: - self.assertEqual(ef.read(), of.read()) + assert ef.read() == of.read() finally: os.unlink(output_file.name) @@ -337,9 +327,9 @@ def test_verify_signature_signing_key(self): self._open("ecdsa_secure_boot_signing_key2.pem"), self._open("bootloader_signed.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn("Signature is not valid", str(cm.exception)) + assert "Signature is not valid" in str(cm.value) # wrong key v2 args = self.VerifyArgs( @@ -347,11 +337,9 @@ def test_verify_signature_signing_key(self): self._open("rsa_secure_boot_signing_key2.pem"), self._open("bootloader_signed_v2.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) # right key, wrong scheme (ecdsa256, v2) args = self.VerifyArgs( @@ -359,9 +347,9 @@ def test_verify_signature_signing_key(self): self._open("ecdsa_secure_boot_signing_key.pem"), self._open("bootloader_signed.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn("Invalid datafile", str(cm.exception)) + assert "Invalid datafile" in str(cm.value) # wrong key v2 (ecdsa256) args = self.VerifyArgs( @@ -369,11 +357,9 @@ def test_verify_signature_signing_key(self): self._open("ecdsa_secure_boot_signing_key2.pem"), self._open("bootloader_signed_v2_ecdsa256.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) # wrong key v2 (ecdsa192) args = self.VerifyArgs( @@ -381,11 +367,9 @@ def test_verify_signature_signing_key(self): self._open("ecdsa192_secure_boot_signing_key2.pem"), self._open("bootloader_signed_v2_ecdsa192.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) # multi-signed wrong key v2 args = self.VerifyArgs( @@ -393,11 +377,9 @@ def test_verify_signature_signing_key(self): self._open("rsa_secure_boot_signing_key4.pem"), self._open("bootloader_multi_signed_v2.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) def test_verify_signature_public_key(self): # correct key v1 @@ -438,9 +420,9 @@ def test_verify_signature_public_key(self): self._open("ecdsa_secure_boot_signing_pubkey2.pem"), self._open("bootloader_signed.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn("Signature is not valid", str(cm.exception)) + assert "Signature is not valid" in str(cm.value) # wrong key v2 args = self.VerifyArgs( @@ -448,11 +430,9 @@ def test_verify_signature_public_key(self): self._open("rsa_secure_boot_signing_pubkey2.pem"), self._open("bootloader_signed_v2.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) # wrong key v2 (ecdsa256) args = self.VerifyArgs( @@ -460,11 +440,9 @@ def test_verify_signature_public_key(self): self._open("ecdsa_secure_boot_signing_pubkey2.pem"), self._open("bootloader_signed_v2_ecdsa256.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) # wrong key v2 (ecdsa192) args = self.VerifyArgs( @@ -472,11 +450,9 @@ def test_verify_signature_public_key(self): self._open("ecdsa192_secure_boot_signing_pubkey2.pem"), self._open("bootloader_signed_v2_ecdsa192.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) # multi-signed wrong key v2 args = self.VerifyArgs( @@ -484,11 +460,9 @@ def test_verify_signature_public_key(self): self._open("rsa_secure_boot_signing_pubkey4.pem"), self._open("bootloader_multi_signed_v2.bin"), ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn( - "Signature could not be verified with the provided key.", str(cm.exception) - ) + assert "Signature could not be verified with the provided key." in str(cm.value) def test_extract_binary_public_key(self): @@ -516,36 +490,32 @@ def test_extract_binary_public_key(self): args = self.VerifyArgs( "1", pub_keyfile2, self._open("bootloader_signed.bin") ) - with self.assertRaises(esptool.FatalError) as cm: + with pytest.raises(esptool.FatalError) as cm: espsecure.verify_signature(args) - self.assertIn("Signature is not valid", str(cm.exception)) + assert "Signature is not valid" in str(cm.value) def test_generate_and_extract_key_v2(self): - # tempfile.TemporaryDirectory() would be better - # but we need compatibility with old Pythons - keydir = tempfile.mkdtemp() - self.addCleanup(os.rmdir, keydir) - - # keyfile cannot exist before generation -> tempfile.NamedTemporaryFile() - # cannot be used for keyfile - keyfile_name = os.path.join(keydir, "key.pem") + with tempfile.TemporaryDirectory() as keydir: + # keyfile cannot exist before generation -> tempfile.NamedTemporaryFile() + # cannot be used for keyfile + keyfile_name = os.path.join(keydir, "key.pem") - # We need to manually delete the keyfile as we are iterating over - # different schemes with the same keyfile so instead of using addCleanup, - # we remove it using os.remove at the end of each pass - for scheme in ["rsa3072", "ecdsa192", "ecdsa256"]: - args = self.GenerateKeyArgs("2", scheme, keyfile_name) - espsecure.generate_signing_key(args) + # We need to manually delete the keyfile as we are iterating over + # different schemes with the same keyfile so instead of using addCleanup, + # we remove it using os.remove at the end of each pass + for scheme in ["rsa3072", "ecdsa192", "ecdsa256"]: + args = self.GenerateKeyArgs("2", scheme, keyfile_name) + espsecure.generate_signing_key(args) - with tempfile.NamedTemporaryFile() as pub_keyfile, open( - keyfile_name, "rb" - ) as keyfile: - args = self.ExtractKeyArgs("2", keyfile, pub_keyfile) - espsecure.extract_public_key(args) - os.remove(keyfile_name) + with tempfile.NamedTemporaryFile() as pub_keyfile, open( + keyfile_name, "rb" + ) as keyfile: + args = self.ExtractKeyArgs("2", keyfile, pub_keyfile) + espsecure.extract_public_key(args) + os.remove(keyfile_name) -class FlashEncryptionTests(EspSecureTestCase): +class TestFlashEncryption(EspSecureTestCase): EncryptArgs = namedtuple( "encrypt_flash_data_args", @@ -591,9 +561,9 @@ def _test_encrypt_decrypt( espsecure.encrypt_flash_data(args) original_plaintext.seek(0) - self.assertNotEqual(original_plaintext.read(), ciphertext.getvalue()) + assert original_plaintext.read() != ciphertext.getvalue() with self._open(expected_ciphertext) as f: - self.assertEqual(f.read(), ciphertext.getvalue()) + assert f.read() == ciphertext.getvalue() ciphertext.seek(0) keyfile.seek(0) @@ -604,10 +574,10 @@ def _test_encrypt_decrypt( espsecure.decrypt_flash_data(args) original_plaintext.seek(0) - self.assertEqual(original_plaintext.read(), plaintext.getvalue()) + assert original_plaintext.read() == plaintext.getvalue() -class ESP32FlashEncryptionTests(FlashEncryptionTests): +class TestESP32FlashEncryption(TestFlashEncryption): def test_encrypt_decrypt_bootloader(self): self._test_encrypt_decrypt( "bootloader.bin", "bootloader-encrypted.bin", "256bit_key.bin", 0x1000, 0xF @@ -627,14 +597,14 @@ def test_encrypt_decrypt_non_default_conf(self): for conf in [0x0, 0x3, 0x9, 0xC]: self._test_encrypt_decrypt( "bootloader.bin", - "bootloader-encrypted-conf%x.bin" % conf, + f"bootloader-encrypted-conf{conf:x}.bin", "256bit_key.bin", 0x1000, conf, ) -class AesXtsFlashEncryptionTests(FlashEncryptionTests): +class TestAesXtsFlashEncryption(TestFlashEncryption): def test_encrypt_decrypt_bootloader(self): self._test_encrypt_decrypt( "bootloader.bin", @@ -721,42 +691,29 @@ def test_padding(self): espsecure.encrypt_flash_data(encrypt_args) - self.assertEqual(ciphertext_full_block.getvalue(), ciphertext.getvalue()) + assert ciphertext_full_block.getvalue() == ciphertext.getvalue() -class DigestTests(EspSecureTestCase): +class TestDigest(EspSecureTestCase): def test_digest_private_key(self): - with tempfile.NamedTemporaryFile(delete=False) as f: - self.addCleanup(os.remove, f.name) + with tempfile.NamedTemporaryFile() as f: outfile_name = f.name self.run_espsecure( "digest_private_key " "--keyfile secure_images/ecdsa_secure_boot_signing_key.pem " - "{}".format(outfile_name) + f"{outfile_name}" ) with open(outfile_name, "rb") as f: - self.assertEqual( - f.read(), - binascii.unhexlify( - "7b7b53708fc89d5e0b2df2571fb8f9d778f61a422ff1101a22159c4b34aad0aa" - ), + assert f.read() == binascii.unhexlify( + "7b7b53708fc89d5e0b2df2571fb8f9d778f61a422ff1101a22159c4b34aad0aa" ) - def test_digest_private_key_with_invalid_output(self): + def test_digest_private_key_with_invalid_output(self, capsys): fname = "secure_images/ecdsa_secure_boot_signing_key.pem" - with self.assertRaises(subprocess.CalledProcessError): - self.run_espsecure( - "digest_private_key --keyfile {} {}".format(fname, fname) - ) - - -if __name__ == "__main__": - print("Running espsecure tests...") - print( - "Using espsecure %s at %s" - % (esptool.__version__, os.path.abspath(espsecure.__file__)) - ) - unittest.main(buffer=True) + with pytest.raises(subprocess.CalledProcessError): + self.run_espsecure(f"digest_private_key --keyfile {fname} {fname}") + output = capsys.readouterr().out + assert "should not be the same!" in output diff --git a/test/test_image_info.py b/test/test_image_info.py index 5235ae0f1..0f2ab24af 100755 --- a/test/test_image_info.py +++ b/test/test_image_info.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python - import os import os.path import subprocess import sys -import unittest -IMAGES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "images") +import pytest + +IMAGES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "images/") os.chdir(IMAGES_DIR) try: ESPTOOL_PY = os.environ["ESPTOOL_PY"] @@ -24,14 +23,11 @@ def read_image(filename): return f.read() -class ImageInfoTests(unittest.TestCase): +class TestImageInfo: def run_image_info(self, chip, file, version=None): """Runs image_info on a binary file. - Returns the command output. - Filenames are relative to the 'test/images' directory. - """ cmd = [ @@ -43,15 +39,15 @@ def run_image_info(self, chip, file, version=None): ] if version is not None: cmd += ["--version", str(version)] - cmd += [file] - print("Executing {}".format(" ".join(cmd))) + cmd += ["".join([IMAGES_DIR, file])] + print("\nExecuting {}".format(" ".join(cmd))) try: output = str(subprocess.check_output(cmd)) print(output) - self.assertFalse( - "warning" in output.lower(), "image_info should not output warnings" - ) + assert ( + "warning" not in output.lower() + ), "image_info should not output warnings" return output except subprocess.CalledProcessError as e: print(e.output) @@ -59,131 +55,125 @@ def run_image_info(self, chip, file, version=None): def test_v1_esp32(self): out = self.run_image_info("esp32", "bootloader_esp32.bin") - self.assertTrue("Entry point: 4009816c" in out, "Wrong entry point") - self.assertTrue("Checksum: 83 (valid)" in out, "Invalid checksum") - self.assertTrue("4 segments" in out, "Wrong number of segments") - self.assertTrue( + assert "Entry point: 4009816c" in out, "Wrong entry point" + assert "Checksum: 83 (valid)" in out, "Invalid checksum" + assert "4 segments" in out, "Wrong number of segments" + assert ( "Segment 3: len 0x01068 load 0x40078000 file_offs 0x00000b64 [CACHE_APP]" - in out, - "Wrong segment info", - ) + in out + ), "Wrong segment info" def test_v1_esp8266(self): out = self.run_image_info("esp8266", NODEMCU_FILE) - self.assertTrue("Image version: 1" in out, "Wrong image version") - self.assertTrue("Entry point: 40101844" in out, "Wrong entry point") - self.assertTrue("Checksum: 2f (valid)" in out, "Invalid checksum") - self.assertTrue("3 segments" in out, "Wrong number of segments") - self.assertTrue( - "Segment 2: len 0x00894 load 0x3ffe8000 file_offs 0x00005ee4 [DRAM]" in out, - "Wrong segment info", - ) + assert "Image version: 1" in out, "Wrong image version" + assert "Entry point: 40101844" in out, "Wrong entry point" + assert "Checksum: 2f (valid)" in out, "Invalid checksum" + assert "3 segments" in out, "Wrong number of segments" + assert ( + "Segment 2: len 0x00894 load 0x3ffe8000 file_offs 0x00005ee4 [DRAM]" in out + ), "Wrong segment info" def test_v2_esp32c3(self): out = self.run_image_info("esp32c3", "bootloader_esp32c3.bin", "2") # Header - self.assertTrue("Entry point: 0x403c0000" in out, "Wrong entry point") - self.assertTrue("Segments: 4" in out, "Wrong num of segments") - self.assertTrue("Flash size: 2MB" in out, "Wrong flash size") - self.assertTrue("Flash freq: 40m" in out, "Wrong flash frequency") - self.assertTrue("Flash mode: DIO" in out, "Wrong flash mode") + assert "Entry point: 0x403c0000" in out, "Wrong entry point" + assert "Segments: 4" in out, "Wrong num of segments" + assert "Flash size: 2MB" in out, "Wrong flash size" + assert "Flash freq: 40m" in out, "Wrong flash frequency" + assert "Flash mode: DIO" in out, "Wrong flash mode" # Extended header - self.assertTrue("WP pin: 0xee" in out, "Wrong WP pin") - self.assertTrue("Chip ID: 5" in out, "Wrong chip ID") - self.assertTrue( + assert "WP pin: 0xee" in out, "Wrong WP pin" + assert "Chip ID: 5" in out, "Wrong chip ID" + assert ( "clk_drv: 0x0, q_drv: 0x0, d_drv: 0x0, " - "cs0_drv: 0x0, hd_drv: 0x0, wp_drv: 0x0" in out, - "Wrong flash pins drive settings", - ) - self.assertTrue("Minimal chip revision: v0.0" in out, "Wrong min revision") - self.assertTrue("Maximal chip revision: v0.0" in out, "Wrong min revision") + "cs0_drv: 0x0, hd_drv: 0x0, wp_drv: 0x0" in out + ), "Wrong flash pins drive settings" + + assert "Minimal chip revision: v0.0" in out, "Wrong min revision" + assert "Maximal chip revision: v0.0" in out, "Wrong min revision" # Segments - self.assertTrue( - "2 0x01864 0x3fcd6114 0x00000034 DRAM, BYTE_ACCESSIBLE" in out, - "Wrong segment info", - ) + assert ( + "2 0x01864 0x3fcd6114 0x00000034 DRAM, BYTE_ACCESSIBLE" in out + ), "Wrong segment info" # Footer - self.assertTrue("Checksum: 0x77 (valid)" in out, "Invalid checksum") - self.assertTrue("c0a9d6d882b65580da2e5e6347 (valid)" in out, "Invalid hash") + assert "Checksum: 0x77 (valid)" in out, "Invalid checksum" + assert "c0a9d6d882b65580da2e5e6347 (valid)" in out, "Invalid hash" # Check output against individual bytes in the headers hdr = read_image("bootloader_esp32c3.bin")[:8] ex_hdr = read_image("bootloader_esp32c3.bin")[8:24] - self.assertTrue("Segments: {}".format(hdr[1]) in out, "Wrong num of segments") - self.assertTrue("WP pin: {:#02x}".format(ex_hdr[0]) in out, "Wrong WP pin") - self.assertTrue("Chip ID: {}".format(ex_hdr[4]) in out, "Wrong chip ID") + assert f"Segments: {hdr[1]}" in out, "Wrong num of segments" + assert f"WP pin: {ex_hdr[0]:#02x}" in out, "Wrong WP pin" + assert f"Chip ID: {ex_hdr[4]}" in out, "Wrong chip ID" if ex_hdr[15] == 1: # Hash appended - self.assertTrue("Validation hash: 4faeab1bd3fd" in out, "Invalid hash") + assert "Validation hash: 4faeab1bd3fd" in out, "Invalid hash" def test_v2_esp8266(self): out = self.run_image_info("esp8266", NODEMCU_FILE, "2") - self.assertTrue("Image version: 1" in out, "Wrong image version") - self.assertTrue("Entry point: 0x40101844" in out, "Wrong entry point") - self.assertTrue("Flash size: 512KB" in out, "Wrong flash size") - self.assertTrue("Flash freq: 40m" in out, "Wrong flash frequency") - self.assertTrue("Flash mode: QIO" in out, "Wrong flash mode") - self.assertTrue("Checksum: 0x2f (valid)" in out, "Invalid checksum") - self.assertTrue("Segments: 3" in out, "Wrong number of segments") - self.assertTrue( - "2 0x00894 0x3ffe8000 0x00005ee4 DRAM" in out, - "Wrong segment info", - ) + assert "Image version: 1" in out, "Wrong image version" + assert "Entry point: 0x40101844" in out, "Wrong entry point" + assert "Flash size: 512KB" in out, "Wrong flash size" + assert "Flash freq: 40m" in out, "Wrong flash frequency" + assert "Flash mode: QIO" in out, "Wrong flash mode" + assert "Checksum: 0x2f (valid)" in out, "Invalid checksum" + assert "Segments: 3" in out, "Wrong number of segments" + assert "2 0x00894 0x3ffe8000 0x00005ee4 DRAM" in out, "Wrong segment info" def test_image_type_detection(self): # ESP8266, version 1 and 2 out = self.run_image_info("auto", NODEMCU_FILE, "1") - self.assertTrue("Detected image type: ESP8266" in out) - self.assertTrue("Segment 1: len 0x05ed4" in out) + assert "Detected image type: ESP8266" in out + assert "Segment 1: len 0x05ed4" in out out = self.run_image_info("auto", NODEMCU_FILE, "2") - self.assertTrue("Detected image type: ESP8266" in out) - self.assertTrue("Flash freq: 40m" in out) + assert "Detected image type: ESP8266" in out + assert "Flash freq: 40m" in out out = self.run_image_info("auto", "esp8266_deepsleep.bin", "2") - self.assertTrue("Detected image type: ESP8266" in out) + assert "Detected image type: ESP8266" in out # ESP32, with and without detection out = self.run_image_info("auto", "bootloader_esp32.bin", "2") - self.assertTrue("Detected image type: ESP32" in out) + assert "Detected image type: ESP32" in out out = self.run_image_info( "auto", "ram_helloworld/helloworld-esp32_edit.bin", "2" ) - self.assertTrue("Detected image type: ESP32" in out) + assert "Detected image type: ESP32" in out out = self.run_image_info("esp32", "bootloader_esp32.bin", "2") - self.assertFalse("Detected image type: ESP32" in out) + assert "Detected image type: ESP32" not in out # ESP32-C3 out = self.run_image_info("auto", "bootloader_esp32c3.bin", "2") - self.assertTrue("Detected image type: ESP32-C3" in out) + assert "Detected image type: ESP32-C3" in out # ESP32-S3 out = self.run_image_info("auto", "bootloader_esp32s3.bin", "2") - self.assertTrue("Detected image type: ESP32-S3" in out) - - @unittest.expectedFailure - def test_invalid_image_type_detection(self): - # Invalid image - self.run_image_info("auto", "one_kb.bin", "2") + assert "Detected image type: ESP32-S3" in out + + def test_invalid_image_type_detection(self, capsys): + with pytest.raises(subprocess.CalledProcessError): + # Invalid image + self.run_image_info("auto", "one_kb.bin", "2") + assert ( + "This is not a valid image (invalid magic number: 0xed)" + in capsys.readouterr().out + ) def test_application_info(self): out = self.run_image_info("auto", "esp_idf_blink_esp32s2.bin", "2") - self.assertTrue("Application information" in out) - self.assertTrue("Project name: blink" in out) - self.assertTrue("App version: qa-test-v5.0-20220830-4-g4532e6" in out) - self.assertTrue("Secure version: 0" in out) - self.assertTrue("Compile time: Sep 13 2022" in out) - self.assertTrue("19:46:07" in out) - self.assertTrue("3059e6b55a965865febd28fa9f6028ad5" in out) - self.assertTrue("cd0dab311febb0a3ea79eaa223ac2b0" in out) - self.assertTrue("ESP-IDF: v5.0-beta1-427-g4532e6e0b2-dirt" in out) + assert "Application information" in out + assert "Project name: blink" in out + assert "App version: qa-test-v5.0-20220830-4-g4532e6" in out + assert "Secure version: 0" in out + assert "Compile time: Sep 13 2022" in out + assert "19:46:07" in out + assert "3059e6b55a965865febd28fa9f6028ad5" in out + assert "cd0dab311febb0a3ea79eaa223ac2b0" in out + assert "ESP-IDF: v5.0-beta1-427-g4532e6e0b2-dirt" in out # No application info in image out = self.run_image_info("auto", "bootloader_esp32.bin", "2") - self.assertFalse("Application information" in out) + assert "Application information" not in out out = self.run_image_info("auto", NODEMCU_FILE, "2") - self.assertFalse("Application information" in out) - - -if __name__ == "__main__": - unittest.main(buffer=True) + assert "Application information" not in out diff --git a/test/test_imagegen.py b/test/test_imagegen.py index 5388d790b..2b8517ee4 100755 --- a/test/test_imagegen.py +++ b/test/test_imagegen.py @@ -1,12 +1,9 @@ -#!/usr/bin/env python - import hashlib import os import os.path import struct import subprocess import sys -import unittest from elftools.elf.elffile import ELFFile @@ -21,6 +18,8 @@ sys.path.append(os.path.dirname(ESPTOOL_PY)) import esptool +import pytest + def try_delete(path): try: @@ -39,7 +38,7 @@ def segment_matches_section(segment, section): return section.header.sh_addr == segment.addr and sh_size == len(segment.data) -class BaseTestCase(unittest.TestCase): +class BaseTestCase: def assertEqualHex(self, expected, actual, message=None): try: expected = hex(expected) @@ -49,7 +48,7 @@ def assertEqualHex(self, expected, actual, message=None): actual = hex(actual) except TypeError: # if actual is character actual = hex(ord(actual)) - self.assertEqual(expected, actual, message) + assert expected == actual, message def assertImageDoesNotContainSection(self, image, elf, section_name): """ @@ -59,19 +58,17 @@ def assertImageDoesNotContainSection(self, image, elf, section_name): with open(elf, "rb") as f: e = ELFFile(f) section = e.get_section_by_name(section_name) - self.assertTrue(section, "%s should be in the ELF" % section_name) + assert section, f"{section_name} should be in the ELF" sh_addr = section.header.sh_addr data = section.data() # no section should start at the same address as the ELF section. for seg in sorted(image.segments, key=lambda s: s.addr): print( - "comparing seg 0x%x sec 0x%x len 0x%x" - % (seg.addr, sh_addr, len(data)) - ) - self.assertFalse( - seg.addr == sh_addr, - "%s should not be in the binary image" % section_name, + f"comparing seg {seg.addr:#x} sec {sh_addr:#x} len {len(data):#x}" ) + assert ( + seg.addr != sh_addr + ), f"{section_name} should not be in the binary image" def assertImageContainsSection(self, image, elf, section_name): """ @@ -81,7 +78,7 @@ def assertImageContainsSection(self, image, elf, section_name): with open(elf, "rb") as f: e = ELFFile(f) section = e.get_section_by_name(section_name) - self.assertTrue(section, "%s should be in the ELF" % section_name) + assert section, f"{section_name} should be in the ELF" sh_addr = section.header.sh_addr data = section.data() # section contents may be smeared across multiple image segments, @@ -90,26 +87,20 @@ def assertImageContainsSection(self, image, elf, section_name): # all be accounted for for seg in sorted(image.segments, key=lambda s: s.addr): print( - "comparing seg 0x%x sec 0x%x len 0x%x" - % (seg.addr, sh_addr, len(data)) + f"comparing seg {seg.addr:#x} sec {sh_addr:#x} len {len(data):#x}" ) if seg.addr == sh_addr: overlap_len = min(len(seg.data), len(data)) - self.assertEqual( - data[:overlap_len], - seg.data[:overlap_len], - "ELF '%s' section has mis-matching binary image data" - % section_name, - ) + assert ( + data[:overlap_len] == seg.data[:overlap_len] + ), f"ELF '{section_name}' section has mis-matching bin image data" sh_addr += overlap_len data = data[overlap_len:] # no bytes in 'data' should be left unmatched - self.assertEqual( - 0, - len(data), - "ELF %s section '%s' has no encompassing segment(s) in binary image " - "(image segments: %s)" % (elf, section_name, image.segments), + assert len(data) == 0, ( + f"ELF {elf} section '{section_name}' has no encompassing" + f" segment(s) in bin image (image segments: {image.segments})" ) def assertImageInfo(self, binpath, chip="esp8266"): @@ -124,10 +115,10 @@ def assertImageInfo(self, binpath, chip="esp8266"): except subprocess.CalledProcessError as e: print(e.output) raise - self.assertFalse("invalid" in output, "Checksum calculation should be valid") - self.assertFalse( - "warning" in output.lower(), "Should be no warnings in image_info output" - ) + assert "invalid" not in output, "Checksum calculation should be valid" + assert ( + "warning" not in output.lower() + ), "Should be no warnings in image_info output" def run_elf2image(self, chip, elf_path, version=None, extra_args=[]): """Run elf2image on elf_path""" @@ -135,27 +126,29 @@ def run_elf2image(self, chip, elf_path, version=None, extra_args=[]): if version is not None: cmd += ["--version", str(version)] cmd += [elf_path] + extra_args - print("Executing %s" % (" ".join(cmd))) + print("\nExecuting {}".format(" ".join(cmd))) try: output = str(subprocess.check_output(cmd)) print(output) - self.assertFalse( - "warning" in output.lower(), "elf2image should not output warnings" - ) + assert ( + "warning" not in output.lower() + ), "elf2image should not output warnings" except subprocess.CalledProcessError as e: print(e.output) raise -class ESP8266V1ImageTests(BaseTestCase): +class TestESP8266V1Image(BaseTestCase): ELF = "esp8266-nonosssdk20-iotdemo.elf" BIN_LOAD = "esp8266-nonosssdk20-iotdemo.elf-0x00000.bin" BIN_IROM = "esp8266-nonosssdk20-iotdemo.elf-0x10000.bin" - def setUp(self): - self.run_elf2image("esp8266", self.ELF, 1) + @classmethod + def setup_class(self): + self.run_elf2image(self, "esp8266", self.ELF, 1) - def tearDown(self): + @classmethod + def teardown_class(self): try_delete(self.BIN_LOAD) try_delete(self.BIN_IROM) @@ -163,17 +156,15 @@ def test_irom_bin(self): with open(self.ELF, "rb") as f: e = ELFFile(f) irom_section = e.get_section_by_name(".irom0.text") - self.assertEqual( - irom_section.header.sh_size, - os.stat(self.BIN_IROM).st_size, - "IROM raw binary file should be same length as .irom0.text section", - ) + assert ( + irom_section.header.sh_size == os.stat(self.BIN_IROM).st_size + ), "IROM raw binary file should be same length as .irom0.text section" def test_loaded_sections(self): image = esptool.bin_image.LoadFirmwareImage("esp8266", self.BIN_LOAD) # Adjacent sections are now merged, len(image.segments) should # equal 2 (instead of 3). - self.assertEqual(2, len(image.segments)) + assert len(image.segments) == 2 self.assertImageContainsSection(image, self.ELF, ".data") self.assertImageContainsSection(image, self.ELF, ".text") # Section .rodata is merged in the binary with the previous one, @@ -181,7 +172,7 @@ def test_loaded_sections(self): self.assertImageDoesNotContainSection(image, self.ELF, ".rodata") -class ESP8266V12SectionHeaderNotAtEnd(BaseTestCase): +class TestESP8266V12SectionHeaderNotAtEnd(BaseTestCase): """Ref https://github.com/espressif/esptool/issues/197 - this ELF image has the section header not at the end of the file""" @@ -189,26 +180,27 @@ class ESP8266V12SectionHeaderNotAtEnd(BaseTestCase): BIN_LOAD = ELF + "-0x00000.bin" BIN_IROM = ELF + "-0x40000.bin" + @classmethod + def teardown_class(self): + try_delete(self.BIN_LOAD) + try_delete(self.BIN_IROM) + def test_elf_section_header_not_at_end(self): self.run_elf2image("esp8266", self.ELF) image = esptool.bin_image.LoadFirmwareImage("esp8266", self.BIN_LOAD) - self.assertEqual(3, len(image.segments)) + assert len(image.segments) == 3 self.assertImageContainsSection(image, self.ELF, ".data") self.assertImageContainsSection(image, self.ELF, ".text") self.assertImageContainsSection(image, self.ELF, ".rodata") - def tearDown(self): - try_delete(self.BIN_LOAD) - try_delete(self.BIN_IROM) - -class ESP8266V2ImageTests(BaseTestCase): +class TestESP8266V2Image(BaseTestCase): def _test_elf2image(self, elfpath, binpath, mergedsections=[]): try: self.run_elf2image("esp8266", elfpath, 2) image = esptool.bin_image.LoadFirmwareImage("esp8266", binpath) print("In test_elf2image", len(image.segments)) - self.assertEqual(4 - len(mergedsections), len(image.segments)) + assert 4 - len(mergedsections) == len(image.segments) sections = [".data", ".text", ".rodata"] # Remove the merged sections from the `sections` list sections = [sec for sec in sections if sec not in mergedsections] @@ -218,19 +210,15 @@ def _test_elf2image(self, elfpath, binpath, mergedsections=[]): self.assertImageDoesNotContainSection(image, elfpath, sec) irom_segment = image.segments[0] - self.assertEqual( - 0, irom_segment.addr, "IROM segment 'load address' should be zero" - ) + assert irom_segment.addr == 0, "IROM segment 'load address' should be zero" with open(elfpath, "rb") as f: e = ELFFile(f) sh_size = ( e.get_section_by_name(".irom0.text").header.sh_size + 15 ) & ~15 - self.assertEqual( - len(irom_segment.data), - sh_size, - "irom segment (0x%x) should be same size (16 padded) " - "as .irom0.text section (0x%x)" % (len(irom_segment.data), sh_size), + assert len(irom_segment.data) == sh_size, ( + f"irom segment ({len(irom_segment.data):#x}) should be same size " + f"(16 padded) as .irom0.text section ({sh_size:#x})" ) # check V2 CRC (for ESP8266 SDK bootloader) @@ -240,7 +228,7 @@ def _test_elf2image(self, elfpath, binpath, mergedsections=[]): crc_stored = struct.unpack("