import filecmp import hashlib import itertools import os import os.path import random import struct import subprocess import sys import tempfile from functools import partial IMAGES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "images") from conftest import need_to_install_package_err import pytest try: from esptool.util import byte from esptool.uf2_writer import UF2Writer from esptool.targets import CHIP_DEFS except ImportError: need_to_install_package_err() def read_image(filename): with open(os.path.join(IMAGES_DIR, filename), "rb") as f: return f.read() @pytest.mark.host_test class TestMergeBin: def run_merge_bin(self, chip, offsets_names, options=[], allow_warnings=False): """Run merge-bin on a list of (offset, filename) tuples with output to a named temporary file. Filenames are relative to the 'test/images' directory. Returns the contents of the merged file if successful. """ output_file = tempfile.NamedTemporaryFile(delete=False) try: output_file.close() cmd = [ sys.executable, "-m", "esptool", "--chip", chip, "merge-bin", "-o", output_file.name, ] + options for offset, name in offsets_names: cmd += [hex(offset), name] print("\nExecuting {}".format(" ".join(cmd))) output = subprocess.check_output( cmd, cwd=IMAGES_DIR, stderr=subprocess.STDOUT ) output = output.decode("utf-8") print(output) if not allow_warnings: assert "warning" not in output.lower(), ( "merge-bin should not output warnings" ) with open(output_file.name, "rb") as f: return f.read() except subprocess.CalledProcessError as e: print(e.output) raise finally: os.unlink(output_file.name) def assertAllFF(self, some_bytes): # this may need some improving as the failed assert messages may be # very long and/or useless! assert b"\xff" * len(some_bytes) == some_bytes def test_simple_merge(self): merged = self.run_merge_bin( "esp8266", [(0x0, "one_kb.bin"), (0x1000, "one_kb.bin"), (0x10000, "one_kb.bin")], ) one_kb = read_image("one_kb.bin") assert len(one_kb) == 0x400 assert len(merged) == 0x10400 assert merged[:0x400] == one_kb assert merged[0x1000:0x1400] == one_kb assert merged[0x10000:] == one_kb self.assertAllFF(merged[0x400:0x1000]) self.assertAllFF(merged[0x1400:0x10000]) def test_args_out_of_order(self): # no matter which order we supply arguments, the output should be the same args = [(0x0, "one_kb.bin"), (0x1000, "one_kb.bin"), (0x10000, "one_kb.bin")] merged_orders = [ self.run_merge_bin("esp8266", perm_args) for perm_args in itertools.permutations(args) ] for m in merged_orders: assert m == merged_orders[0] def test_error_overlap(self, capsys): args = [(0x1000, "one_mb.bin"), (0x20000, "one_kb.bin")] for perm_args in itertools.permutations(args): with pytest.raises(subprocess.CalledProcessError): self.run_merge_bin("esp32", perm_args) output = capsys.readouterr().out assert "overlap" in output def test_leading_padding(self): merged = self.run_merge_bin("esp32c3", [(0x100000, "one_mb.bin")]) self.assertAllFF(merged[:0x100000]) assert read_image("one_mb.bin") == merged[0x100000:] def test_update_bootloader_params(self): merged = self.run_merge_bin( "esp32", [ (0x1000, "bootloader_esp32.bin"), (0x10000, "ram_helloworld/helloworld-esp32.bin"), ], ["--flash-size", "2MB", "--flash-mode", "dout"], ) self.assertAllFF(merged[:0x1000]) bootloader = read_image("bootloader_esp32.bin") helloworld = read_image("ram_helloworld/helloworld-esp32.bin") # test the bootloader is unchanged apart from the header # (updating the header doesn't change CRC, # and doesn't update the SHA although it will invalidate it!) assert merged[0x1010 : 0x1000 + len(bootloader)] == bootloader[0x10:] # check the individual bytes in the header are as expected merged_hdr = merged[0x1000:0x1010] bootloader_hdr = bootloader[:0x10] assert bootloader_hdr[:2] == merged_hdr[:2] assert byte(merged_hdr, 2) == 3 # flash mode dout assert byte(merged_hdr, 3) & 0xF0 == 0x10 # flash size 2MB (ESP32) # flash freq is unchanged assert byte(bootloader_hdr, 3) & 0x0F == byte(merged_hdr, 3) & 0x0F assert bootloader_hdr[4:] == merged_hdr[4:] # remaining field are unchanged # check all the padding is as expected self.assertAllFF(merged[0x1000 + len(bootloader) : 0x10000]) assert merged[0x10000 : 0x10000 + len(helloworld)], helloworld def test_target_offset(self): merged = self.run_merge_bin( "esp32", [ (0x1000, "bootloader_esp32.bin"), (0x10000, "ram_helloworld/helloworld-esp32.bin"), ], ["--target-offset", "0x1000"], ) bootloader = read_image("bootloader_esp32.bin") helloworld = read_image("ram_helloworld/helloworld-esp32.bin") assert bootloader == merged[: len(bootloader)] assert helloworld == merged[0xF000 : 0xF000 + len(helloworld)] self.assertAllFF(merged[0x1000 + len(bootloader) : 0xF000]) def test_pad_to_size(self): merged = self.run_merge_bin( "esp32c3", [(0x0, "bootloader_esp32c3.bin")], ["--pad-to-size", "4MB"] ) bootloader = read_image("bootloader_esp32c3.bin") assert len(merged) == 0x400000 assert bootloader == merged[: len(bootloader)] self.assertAllFF(merged[len(bootloader) :]) def test_pad_to_size_w_target_offset(self): merged = self.run_merge_bin( "esp32", [ (0x1000, "bootloader_esp32.bin"), (0x10000, "ram_helloworld/helloworld-esp32.bin"), ], ["--target-offset", "0x1000", "--pad-to-size", "2MB"], ) # full length is without target-offset arg assert len(merged) == 0x200000 - 0x1000 bootloader = read_image("bootloader_esp32.bin") helloworld = read_image("ram_helloworld/helloworld-esp32.bin") assert bootloader == merged[: len(bootloader)] assert helloworld == merged[0xF000 : 0xF000 + len(helloworld)] self.assertAllFF(merged[0xF000 + len(helloworld) :]) def test_merge_mixed(self): # convert bootloader to hex hex = self.run_merge_bin( "esp32", [(0x1000, "bootloader_esp32.bin")], options=["--format", "hex"], allow_warnings=True, ) # create a temp file with hex content with tempfile.NamedTemporaryFile(suffix=".hex", delete=False) as f: f.write(hex) # merge hex file with bin file # output to bin file should be the same as in merge bin + bin try: merged = self.run_merge_bin( "esp32", [(0x1000, f.name), (0x10000, "ram_helloworld/helloworld-esp32.bin")], ["--target-offset", "0x1000", "--pad-to-size", "2MB"], ) finally: os.unlink(f.name) # full length is without target-offset arg assert len(merged) == 0x200000 - 0x1000 bootloader = read_image("bootloader_esp32.bin") helloworld = read_image("ram_helloworld/helloworld-esp32.bin") assert bootloader == merged[: len(bootloader)] assert helloworld == merged[0xF000 : 0xF000 + len(helloworld)] self.assertAllFF(merged[0xF000 + len(helloworld) :]) def test_merge_bin2hex(self): merged = self.run_merge_bin( "esp32", [ (0x1000, "bootloader_esp32.bin"), ], options=["--format", "hex"], allow_warnings=True, ) lines = merged.splitlines() # hex format - :0300300002337A1E # :03 0030 00 02337A 1E # ^data_cnt/2 ^addr ^type ^data ^checksum # check for starting address - 0x1000 passed from arg assert lines[0][3:7] == b"1000" # pick a random line for testing the format line = lines[random.randrange(0, len(lines))] assert line[0] == ord(":") data_len = int(b"0x" + line[1:3], 16) # : + len + addr + type + data + checksum assert len(line) == 1 + 2 + 4 + 2 + data_len * 2 + 2 # last line is always :00000001FF assert lines[-1] == b":00000001FF" # convert back and verify the result against the source bin file with tempfile.NamedTemporaryFile(suffix=".hex", delete=False) as hex: hex.write(merged) merged_bin = self.run_merge_bin( "esp32", [(0x1000, hex.name)], options=["--format", "raw"], ) source = read_image("bootloader_esp32.bin") # verify that padding was done correctly assert b"\xff" * 0x1000 == merged_bin[:0x1000] # verify the file itself assert source == merged_bin[0x1000:] def test_hex_header_raw_file(self): # use raw binary file starting with colon with tempfile.NamedTemporaryFile(delete=False) as f: f.write(b":") try: merged = self.run_merge_bin("esp32", [(0x0, f.name)]) assert merged == b":" finally: os.unlink(f.name) class UF2Block: def __init__(self, bs): self.length = len(bs) # See https://github.com/microsoft/uf2 for the format first_part = "<" + "I" * 8 # payload is between last_part = "