1# Unit tests (really integration tests) for esptool.py using the pytest framework
2# Uses a device connected to the serial port.
3#
4# RUNNING THIS WILL MESS UP THE DEVICE'S SPI FLASH CONTENTS
5#
6# How to use:
7#
8# Run with a physical connection to a chip:
9#  - `pytest test_esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 115200`
10#
11# where  - --port       - a serial port for esptool.py operation
12#        - --chip       - ESP chip name
13#        - --baud       - baud rate
14#        - --with-trace - trace all interactions (True or False)
15
16import os
17import os.path
18import random
19import re
20import struct
21import subprocess
22import sys
23import tempfile
24import time
25from socket import AF_INET, SOCK_STREAM, socket
26from time import sleep
27from typing import List
28from unittest.mock import MagicMock
29
30# Link command line options --port, --chip, --baud, --with-trace, and --preload-port
31from conftest import (
32    arg_baud,
33    arg_chip,
34    arg_port,
35    arg_preload_port,
36    arg_trace,
37    need_to_install_package_err,
38)
39
40
41import pytest
42
43try:
44    import esptool
45    import espefuse
46except ImportError:
47    need_to_install_package_err()
48
49import serial
50
51
52TEST_DIR = os.path.abspath(os.path.dirname(__file__))
53
54# esptool.py skips strapping mode check in USB-CDC case if this is set
55os.environ["ESPTOOL_TESTING"] = "1"
56
57print("Running esptool.py tests...")
58
59
60class ESPRFC2217Server(object):
61    """Creates a virtual serial port accessible through rfc2217 port."""
62
63    def __init__(self, rfc2217_port=None):
64        self.port = rfc2217_port or self.get_free_port()
65        self.cmd = [
66            sys.executable,
67            os.path.join(TEST_DIR, "..", "esp_rfc2217_server.py"),
68            "-p",
69            str(self.port),
70            arg_port,
71        ]
72        self.server_output_file = open(f"{TEST_DIR}/{str(arg_chip)}_server.out", "a")
73        self.server_output_file.write("************************************")
74        self.p = None
75        self.wait_for_server_starts(attempts_count=5)
76
77    @staticmethod
78    def get_free_port():
79        s = socket(AF_INET, SOCK_STREAM)
80        s.bind(("", 0))
81        port = s.getsockname()[1]
82        s.close()
83        return port
84
85    def wait_for_server_starts(self, attempts_count):
86        for attempt in range(attempts_count):
87            try:
88                self.p = subprocess.Popen(
89                    self.cmd,
90                    cwd=TEST_DIR,
91                    stdout=self.server_output_file,
92                    stderr=subprocess.STDOUT,
93                    close_fds=True,
94                )
95                sleep(2)
96                s = socket(AF_INET, SOCK_STREAM)
97                result = s.connect_ex(("localhost", self.port))
98                s.close()
99                if result == 0:
100                    print("Server started successfully.")
101                    return
102            except Exception as e:
103                print(e)
104            print(
105                "Server start failed."
106                + (" Retrying . . ." if attempt < attempts_count - 1 else "")
107            )
108            self.p.terminate()
109        raise Exception("Server not started successfully!")
110
111    def __enter__(self):
112        return self
113
114    def __exit__(self, type, value, traceback):
115        self.server_output_file.close()
116        self.p.terminate()
117
118
119# Re-run all tests at least once if failure happens in USB-JTAG/Serial
120@pytest.mark.flaky(reruns=1, condition=arg_preload_port is not False)
121class EsptoolTestCase:
122    def run_espsecure(self, args):
123        cmd = [sys.executable, "-m", "espsecure"] + args.split(" ")
124        print("\nExecuting {}...".format(" ".join(cmd)))
125        try:
126            output = subprocess.check_output(
127                [str(s) for s in cmd], cwd=TEST_DIR, stderr=subprocess.STDOUT
128            )
129            output = output.decode("utf-8")
130            print(output)  # for more complete stdout logs on failure
131            return output
132        except subprocess.CalledProcessError as e:
133            print(e.output)
134            raise e
135
136    def run_esptool(self, args, baud=None, chip=None, port=None, preload=True):
137        """
138        Run esptool with the specified arguments. --chip, --port and --baud
139        are filled in automatically from the command line.
140        (These can be overridden with their respective params.)
141
142        Additional args passed in args parameter as a string.
143
144        Preloads a dummy binary if --preload_port is specified.
145        This is needed in USB-JTAG/Serial mode to disable the
146        RTC watchdog, which causes the port to periodically disappear.
147
148        Returns output from esptool.py as a string if there is any.
149        Raises an exception if esptool.py fails.
150        """
151
152        def run_esptool_process(cmd):
153            print("Executing {}...".format(" ".join(cmd)))
154            try:
155                output = subprocess.check_output(
156                    [str(s) for s in cmd],
157                    cwd=TEST_DIR,
158                    stderr=subprocess.STDOUT,
159                )
160                return output.decode("utf-8")
161            except subprocess.CalledProcessError as e:
162                print(e.output.decode("utf-8"))
163                raise e
164
165        try:
166            # Used for flasher_stub/run_tests_with_stub.sh
167            esptool = [os.environ["ESPTOOL_PY"]]
168        except KeyError:
169            # Run the installed esptool module
170            esptool = ["-m", "esptool"]
171        trace_arg = ["--trace"] if arg_trace else []
172        base_cmd = [sys.executable] + esptool + trace_arg
173        if chip or arg_chip is not None and chip != "auto":
174            base_cmd += ["--chip", chip or arg_chip]
175        if port or arg_port is not None:
176            base_cmd += ["--port", port or arg_port]
177        if baud or arg_baud is not None:
178            base_cmd += ["--baud", str(baud or arg_baud)]
179        usb_jtag_serial_reset = ["--before", "usb_reset"] if arg_preload_port else []
180        full_cmd = base_cmd + usb_jtag_serial_reset + args.split(" ")
181
182        # Preload a dummy binary to disable the RTC watchdog, needed in USB-JTAG/Serial
183        if (
184            preload
185            and arg_preload_port
186            and arg_chip
187            in [
188                "esp32c3",
189                "esp32s3",
190                "esp32c6",
191                "esp32h2",
192                "esp32p4",
193                "esp32c5",
194                "esp32c61",
195            ]  # With U-JS
196        ):
197            port_index = base_cmd.index("--port") + 1
198            base_cmd[port_index] = arg_preload_port  # Set the port to the preload one
199            preload_cmd = base_cmd + [
200                "--no-stub",
201                "load_ram",
202                f"{TEST_DIR}/images/ram_helloworld/helloworld-{arg_chip}.bin",
203            ]
204            print("\nPreloading dummy binary to disable RTC watchdog...")
205            run_esptool_process(preload_cmd)
206            print("Dummy binary preloaded successfully.")
207            time.sleep(0.3)  # Wait for the app to run and port to appear
208
209        # Run the command
210        print(f'\nRunning the "{args}" command...')
211        output = run_esptool_process(full_cmd)
212        print(output)  # for more complete stdout logs on failure
213        return output
214
215    def run_esptool_error(self, args, baud=None, chip=None):
216        """
217        Run esptool.py similar to run_esptool, but expect an error.
218
219        Verifies the error is an expected error not an unhandled exception,
220        and returns the output from esptool.py as a string.
221        """
222        with pytest.raises(subprocess.CalledProcessError) as fail:
223            self.run_esptool(args, baud, chip)
224        failure = fail.value
225        assert failure.returncode in [1, 2]  # UnsupportedCmdError and FatalError codes
226        return failure.output.decode("utf-8")
227
228    @classmethod
229    def setup_class(self):
230        print()
231        print(50 * "*")
232        # Save the current working directory to be restored later
233        self.stored_dir = os.getcwd()
234        os.chdir(TEST_DIR)
235
236    @classmethod
237    def teardown_class(self):
238        # Restore the stored working directory
239        os.chdir(self.stored_dir)
240
241    def readback(self, offset, length, spi_connection=None):
242        """Read contents of flash back, return to caller."""
243        dump_file = tempfile.NamedTemporaryFile(delete=False)  # a file we can read into
244        try:
245            cmd = (
246                f"--before default_reset read_flash {offset} {length} {dump_file.name}"
247            )
248            if spi_connection:
249                cmd += f" --spi-connection {spi_connection}"
250            self.run_esptool(cmd)
251            with open(dump_file.name, "rb") as f:
252                rb = f.read()
253
254            assert length == len(
255                rb
256            ), f"read_flash length {length} offset {offset:#x} yielded {len(rb)} bytes!"
257            return rb
258        finally:
259            dump_file.close()
260            os.unlink(dump_file.name)
261
262    def diff(self, readback, compare_to):
263        for rb_b, ct_b, offs in zip(readback, compare_to, range(len(readback))):
264            assert (
265                rb_b == ct_b
266            ), f"First difference at offset {offs:#x} Expected {ct_b} got {rb_b}"
267
268    def verify_readback(
269        self, offset, length, compare_to, is_bootloader=False, spi_connection=None
270    ):
271        rb = self.readback(offset, length, spi_connection)
272        with open(compare_to, "rb") as f:
273            ct = f.read()
274        if len(rb) != len(ct):
275            print(
276                f"WARNING: Expected length {len(ct)} doesn't match comparison {len(rb)}"
277            )
278        print(f"Readback {len(rb)} bytes")
279        if is_bootloader:
280            # writing a bootloader image to bootloader offset can set flash size/etc,
281            # so don't compare the 8 byte header
282            assert ct[0] == rb[0], "First bytes should be identical"
283            rb = rb[8:]
284            ct = ct[8:]
285        self.diff(rb, ct)
286
287
288@pytest.mark.skipif(arg_chip != "esp32", reason="ESP32 only")
289class TestFlashEncryption(EsptoolTestCase):
290    def valid_key_present(self):
291        try:
292            esp = esptool.ESP32ROM(arg_port)
293            esp.connect()
294            efuses, _ = espefuse.get_efuses(esp=esp)
295            blk1_rd_en = efuses["BLOCK1"].is_readable()
296            return not blk1_rd_en
297        finally:
298            esp._port.close()
299
300    def test_blank_efuse_encrypt_write_abort(self):
301        """
302        since flash crypt config is not set correctly, this test should abort write
303        """
304        if self.valid_key_present() is True:
305            pytest.skip("Valid encryption key already programmed, aborting the test")
306
307        self.run_esptool(
308            "write_flash 0x1000 images/bootloader_esp32.bin "
309            "0x8000 images/partitions_singleapp.bin "
310            "0x10000 images/ram_helloworld/helloworld-esp32.bin"
311        )
312        output = self.run_esptool_error(
313            "write_flash --encrypt 0x10000 images/ram_helloworld/helloworld-esp32.bin"
314        )
315        assert "Flash encryption key is not programmed".lower() in output.lower()
316
317    def test_blank_efuse_encrypt_write_continue1(self):
318        """
319        since ignore option is specified, write should happen even though flash crypt
320        config is 0
321        later encrypted flash contents should be read back & compared with
322        precomputed ciphertext
323        pass test
324        """
325        if self.valid_key_present() is True:
326            pytest.skip("Valid encryption key already programmed, aborting the test")
327
328        self.run_esptool(
329            "write_flash --encrypt --ignore-flash-encryption-efuse-setting "
330            "0x10000 images/ram_helloworld/helloworld-esp32.bin"
331        )
332        self.run_esptool("read_flash 0x10000 192 images/read_encrypted_flash.bin")
333        self.run_espsecure(
334            "encrypt_flash_data --address 0x10000 --keyfile images/aes_key.bin "
335            "--flash_crypt_conf 0 --output images/local_enc.bin "
336            "images/ram_helloworld/helloworld-esp32.bin"
337        )
338
339        try:
340            with open("images/read_encrypted_flash.bin", "rb") as file1:
341                read_file1 = file1.read()
342
343            with open("images/local_enc.bin", "rb") as file2:
344                read_file2 = file2.read()
345
346            for rf1, rf2, i in zip(read_file1, read_file2, range(len(read_file2))):
347                assert (
348                    rf1 == rf2
349                ), f"Encrypted write failed: file mismatch at byte position {i}"
350
351            print("Encrypted write success")
352        finally:
353            os.remove("images/read_encrypted_flash.bin")
354            os.remove("images/local_enc.bin")
355
356    def test_blank_efuse_encrypt_write_continue2(self):
357        """
358        since ignore option is specified, write should happen even though flash crypt
359        config is 0
360        later encrypted flash contents should be read back & compared with
361        precomputed ciphertext
362        fail test
363        """
364        if self.valid_key_present() is True:
365            pytest.skip("Valid encryption key already programmed, aborting the test")
366
367        self.run_esptool(
368            "write_flash --encrypt --ignore-flash-encryption-efuse-setting "
369            "0x10000 images/ram_helloworld/helloworld-esp32_edit.bin"
370        )
371        self.run_esptool("read_flash 0x10000 192 images/read_encrypted_flash.bin")
372        self.run_espsecure(
373            "encrypt_flash_data --address 0x10000 --keyfile images/aes_key.bin "
374            "--flash_crypt_conf 0 --output images/local_enc.bin "
375            "images/ram_helloworld/helloworld-esp32.bin"
376        )
377
378        try:
379            with open("images/read_encrypted_flash.bin", "rb") as file1:
380                read_file1 = file1.read()
381
382            with open("images/local_enc.bin", "rb") as file2:
383                read_file2 = file2.read()
384
385            mismatch = any(rf1 != rf2 for rf1, rf2 in zip(read_file1, read_file2))
386            assert mismatch, "Files should mismatch"
387
388        finally:
389            os.remove("images/read_encrypted_flash.bin")
390            os.remove("images/local_enc.bin")
391
392
393class TestFlashing(EsptoolTestCase):
394    @pytest.mark.quick_test
395    def test_short_flash(self):
396        self.run_esptool("write_flash 0x0 images/one_kb.bin")
397        self.verify_readback(0, 1024, "images/one_kb.bin")
398
399    @pytest.mark.quick_test
400    def test_highspeed_flash(self):
401        self.run_esptool("write_flash 0x0 images/fifty_kb.bin", baud=921600)
402        self.verify_readback(0, 50 * 1024, "images/fifty_kb.bin")
403
404    def test_adjacent_flash(self):
405        self.run_esptool("write_flash 0x0 images/sector.bin 0x1000 images/fifty_kb.bin")
406        self.verify_readback(0, 4096, "images/sector.bin")
407        self.verify_readback(4096, 50 * 1024, "images/fifty_kb.bin")
408
409    def test_short_flash_hex(self):
410        fd, f = tempfile.mkstemp(suffix=".hex")
411        try:
412            self.run_esptool(f"merge_bin --format hex 0x0 images/one_kb.bin -o {f}")
413            # make sure file is closed before running next command (mainly for Windows)
414            os.close(fd)
415            self.run_esptool(f"write_flash 0x0 {f}")
416            self.verify_readback(0, 1024, "images/one_kb.bin")
417        finally:
418            os.unlink(f)
419
420    def test_adjacent_flash_hex(self):
421        fd1, f1 = tempfile.mkstemp(suffix=".hex")
422        fd2, f2 = tempfile.mkstemp(suffix=".hex")
423        try:
424            self.run_esptool(f"merge_bin --format hex 0x0 images/sector.bin -o {f1}")
425            # make sure file is closed before running next command (mainly for Windows)
426            os.close(fd1)
427            self.run_esptool(
428                f"merge_bin --format hex 0x1000 images/fifty_kb.bin -o {f2}"
429            )
430            os.close(fd2)
431            self.run_esptool(f"write_flash 0x0 {f1} 0x1000 {f2}")
432            self.verify_readback(0, 4096, "images/sector.bin")
433            self.verify_readback(4096, 50 * 1024, "images/fifty_kb.bin")
434        finally:
435            os.unlink(f1)
436            os.unlink(f2)
437
438    def test_adjacent_flash_mixed(self):
439        fd, f = tempfile.mkstemp(suffix=".hex")
440        try:
441            self.run_esptool(
442                f"merge_bin --format hex 0x1000 images/fifty_kb.bin -o {f}"
443            )
444            # make sure file is closed before running next command (mainly for Windows)
445            os.close(fd)
446            self.run_esptool(f"write_flash 0x0 images/sector.bin 0x1000 {f}")
447            self.verify_readback(0, 4096, "images/sector.bin")
448            self.verify_readback(4096, 50 * 1024, "images/fifty_kb.bin")
449        finally:
450            os.unlink(f)
451
452    def test_adjacent_independent_flash(self):
453        self.run_esptool("write_flash 0x0 images/sector.bin")
454        self.verify_readback(0, 4096, "images/sector.bin")
455        self.run_esptool("write_flash 0x1000 images/fifty_kb.bin")
456        self.verify_readback(4096, 50 * 1024, "images/fifty_kb.bin")
457        # writing flash the second time shouldn't have corrupted the first time
458        self.verify_readback(0, 4096, "images/sector.bin")
459
460    @pytest.mark.skipif(
461        int(os.getenv("ESPTOOL_TEST_FLASH_SIZE", "0")) < 32, reason="needs 32MB flash"
462    )
463    def test_last_bytes_of_32M_flash(self):
464        flash_size = 32 * 1024 * 1024
465        image_size = 1024
466        offset = flash_size - image_size
467        self.run_esptool("write_flash {} images/one_kb.bin".format(hex(offset)))
468        # Some of the functions cannot handle 32-bit addresses - i.e. addresses accessing
469        # the higher 16MB will manipulate with the lower 16MB flash area.
470        offset2 = offset & 0xFFFFFF
471        self.run_esptool("write_flash {} images/one_kb_all_ef.bin".format(hex(offset2)))
472        self.verify_readback(offset, image_size, "images/one_kb.bin")
473
474    @pytest.mark.skipif(
475        int(os.getenv("ESPTOOL_TEST_FLASH_SIZE", "0")) < 32, reason="needs 32MB flash"
476    )
477    def test_write_larger_area_to_32M_flash(self):
478        offset = 18 * 1024 * 1024
479        self.run_esptool("write_flash {} images/one_mb.bin".format(hex(offset)))
480        # Some of the functions cannot handle 32-bit addresses - i.e. addresses accessing
481        # the higher 16MB will manipulate with the lower 16MB flash area.
482        offset2 = offset & 0xFFFFFF
483        self.run_esptool("write_flash {} images/one_kb_all_ef.bin".format(hex(offset2)))
484        self.verify_readback(offset, 1 * 1024 * 1024, "images/one_mb.bin")
485
486    def test_correct_offset(self):
487        """Verify writing at an offset actually writes to that offset."""
488        self.run_esptool("write_flash 0x2000 images/sector.bin")
489        time.sleep(0.1)
490        three_sectors = self.readback(0, 0x3000)
491        last_sector = three_sectors[0x2000:]
492        with open("images/sector.bin", "rb") as f:
493            ct = f.read()
494        assert last_sector == ct
495
496    @pytest.mark.quick_test
497    def test_no_compression_flash(self):
498        self.run_esptool(
499            "write_flash -u 0x0 images/sector.bin 0x1000 images/fifty_kb.bin"
500        )
501        self.verify_readback(0, 4096, "images/sector.bin")
502        self.verify_readback(4096, 50 * 1024, "images/fifty_kb.bin")
503
504    @pytest.mark.quick_test
505    @pytest.mark.skipif(arg_chip == "esp8266", reason="Added in ESP32")
506    def test_compressed_nostub_flash(self):
507        self.run_esptool(
508            "--no-stub write_flash -z 0x0 images/sector.bin 0x1000 images/fifty_kb.bin"
509        )
510        self.verify_readback(0, 4096, "images/sector.bin")
511        self.verify_readback(4096, 50 * 1024, "images/fifty_kb.bin")
512
513    def _test_partition_table_then_bootloader(self, args):
514        self.run_esptool(args + " 0x4000 images/partitions_singleapp.bin")
515        self.verify_readback(0x4000, 96, "images/partitions_singleapp.bin")
516        self.run_esptool(args + " 0x1000 images/bootloader_esp32.bin")
517        self.verify_readback(0x1000, 7888, "images/bootloader_esp32.bin", True)
518        self.verify_readback(0x4000, 96, "images/partitions_singleapp.bin")
519
520    def test_partition_table_then_bootloader(self):
521        self._test_partition_table_then_bootloader("write_flash --force")
522
523    def test_partition_table_then_bootloader_no_compression(self):
524        self._test_partition_table_then_bootloader("write_flash --force -u")
525
526    def test_partition_table_then_bootloader_nostub(self):
527        self._test_partition_table_then_bootloader("--no-stub write_flash --force")
528
529    # note: there is no "partition table then bootloader" test that
530    # uses --no-stub and -z, as the ESP32 ROM over-erases and can't
531    # flash this set of files in this order.  we do
532    # test_compressed_nostub_flash() instead.
533
534    def test_length_not_aligned_4bytes(self):
535        self.run_esptool("write_flash 0x0 images/not_4_byte_aligned.bin")
536
537    def test_length_not_aligned_4bytes_no_compression(self):
538        self.run_esptool("write_flash -u 0x0 images/not_4_byte_aligned.bin")
539
540    @pytest.mark.quick_test
541    @pytest.mark.host_test
542    def test_write_overlap(self):
543        output = self.run_esptool_error(
544            "write_flash 0x0 images/bootloader_esp32.bin 0x1000 images/one_kb.bin"
545        )
546        assert "Detected overlap at address: 0x1000 " in output
547
548    @pytest.mark.quick_test
549    @pytest.mark.host_test
550    def test_repeated_address(self):
551        output = self.run_esptool_error(
552            "write_flash 0x0 images/one_kb.bin 0x0 images/one_kb.bin"
553        )
554        assert "Detected overlap at address: 0x0 " in output
555
556    @pytest.mark.quick_test
557    @pytest.mark.host_test
558    def test_write_sector_overlap(self):
559        # These two 1KB files don't overlap,
560        # but they do both touch sector at 0x1000 so should fail
561        output = self.run_esptool_error(
562            "write_flash 0xd00 images/one_kb.bin 0x1d00 images/one_kb.bin"
563        )
564        assert "Detected overlap at address: 0x1d00" in output
565
566    def test_write_no_overlap(self):
567        output = self.run_esptool(
568            "write_flash 0x0 images/one_kb.bin 0x2000 images/one_kb.bin"
569        )
570        assert "Detected overlap at address" not in output
571
572    def test_compressible_file(self):
573        try:
574            input_file = tempfile.NamedTemporaryFile(delete=False)
575            file_size = 1024 * 1024
576            input_file.write(b"\x00" * file_size)
577            input_file.close()
578            self.run_esptool(f"write_flash 0x10000 {input_file.name}")
579        finally:
580            os.unlink(input_file.name)
581
582    def test_compressible_non_trivial_file(self):
583        try:
584            input_file = tempfile.NamedTemporaryFile(delete=False)
585            file_size = 1000 * 1000
586            same_bytes = 8000
587            for _ in range(file_size // same_bytes):
588                input_file.write(
589                    struct.pack("B", random.randrange(0, 1 << 8)) * same_bytes
590                )
591            input_file.close()
592            self.run_esptool(f"write_flash 0x10000 {input_file.name}")
593        finally:
594            os.unlink(input_file.name)
595
596    @pytest.mark.quick_test
597    def test_zero_length(self):
598        # Zero length files are skipped with a warning
599        output = self.run_esptool(
600            "write_flash 0x10000 images/one_kb.bin 0x11000 images/zerolength.bin"
601        )
602        self.verify_readback(0x10000, 1024, "images/one_kb.bin")
603        assert "zerolength.bin is empty" in output
604
605    @pytest.mark.quick_test
606    def test_single_byte(self):
607        self.run_esptool("write_flash 0x0 images/onebyte.bin")
608        self.verify_readback(0x0, 1, "images/onebyte.bin")
609
610    def test_erase_range_messages(self):
611        output = self.run_esptool(
612            "write_flash 0x1000 images/sector.bin 0x0FC00 images/one_kb.bin"
613        )
614        assert "Flash will be erased from 0x00001000 to 0x00001fff..." in output
615        assert (
616            "WARNING: Flash address 0x0000fc00 is not aligned to a 0x1000 "
617            "byte flash sector. 0xc00 bytes before this address will be erased."
618            in output
619        )
620        assert "Flash will be erased from 0x0000f000 to 0x0000ffff..." in output
621
622    @pytest.mark.skipif(
623        arg_chip == "esp8266", reason="chip_id field exist in ESP32 and later images"
624    )
625    @pytest.mark.skipif(
626        arg_chip == "esp32s3", reason="This is a valid ESP32-S3 image, would pass"
627    )
628    def test_write_image_for_another_target(self):
629        output = self.run_esptool_error(
630            "write_flash 0x0 images/esp32s3_header.bin 0x1000 images/one_kb.bin"
631        )
632        assert "Unexpected chip id in image." in output
633        assert "value was 9. Is this image for a different chip model?" in output
634        assert "images/esp32s3_header.bin is not an " in output
635        assert "image. Use --force to flash anyway." in output
636
637    @pytest.mark.skipif(
638        arg_chip == "esp8266", reason="chip_id field exist in ESP32 and later images"
639    )
640    @pytest.mark.skipif(
641        arg_chip != "esp32s3", reason="This check happens only on a valid image"
642    )
643    def test_write_image_for_another_revision(self):
644        output = self.run_esptool_error(
645            "write_flash 0x0 images/one_kb.bin 0x1000 images/esp32s3_header.bin"
646        )
647        assert "images/esp32s3_header.bin requires chip revision 10" in output
648        assert "or higher (this chip is revision" in output
649        assert "Use --force to flash anyway." in output
650
651    @pytest.mark.skipif(
652        arg_chip != "esp32c3", reason="This check happens only on a valid image"
653    )
654    def test_flash_with_min_max_rev(self):
655        """Use min/max_rev_full field to specify chip revision"""
656        output = self.run_esptool_error(
657            "write_flash 0x0 images/one_kb.bin 0x1000 images/esp32c3_header_min_rev.bin"
658        )
659        assert (
660            "images/esp32c3_header_min_rev.bin "
661            "requires chip revision in range [v2.55 - max rev not set]" in output
662        )
663        assert "Use --force to flash anyway." in output
664
665    @pytest.mark.quick_test
666    def test_erase_before_write(self):
667        output = self.run_esptool("write_flash --erase-all 0x0 images/one_kb.bin")
668        assert "Chip erase completed successfully" in output
669        assert "Hash of data verified" in output
670
671    @pytest.mark.quick_test
672    def test_flash_not_aligned_nostub(self):
673        output = self.run_esptool("--no-stub write_flash 0x1 images/one_kb.bin")
674        assert (
675            "WARNING: Flash address 0x00000001 is not aligned to a 0x1000 byte flash sector. 0x1 bytes before this address will be erased."
676            in output
677        )
678        assert "Hard resetting via RTS pin..." in output
679
680    @pytest.mark.skipif(arg_preload_port is False, reason="USB-JTAG/Serial only")
681    @pytest.mark.skipif(arg_chip != "esp32c3", reason="ESP32-C3 only")
682    def test_flash_overclocked(self):
683        SYSTEM_BASE_REG = 0x600C0000
684        SYSTEM_CPU_PER_CONF_REG = SYSTEM_BASE_REG + 0x008
685        SYSTEM_CPUPERIOD_SEL_S = 0
686        SYSTEM_CPUPERIOD_MAX = 1  # CPU_CLK frequency is 160 MHz
687
688        SYSTEM_SYSCLK_CONF_REG = SYSTEM_BASE_REG + 0x058
689        SYSTEM_SOC_CLK_SEL_S = 10
690        SYSTEM_SOC_CLK_MAX = 1
691
692        output = self.run_esptool(
693            "--after no_reset_stub write_flash 0x0 images/one_mb.bin", preload=False
694        )
695        faster = re.search(r"(\d+(\.\d+)?)\s+seconds", output)
696        assert faster, "Duration summary not found in the output"
697
698        with esptool.cmds.detect_chip(
699            port=arg_port, connect_mode="no_reset"
700        ) as reg_mod:
701            reg_mod.write_reg(
702                SYSTEM_SYSCLK_CONF_REG,
703                0,
704                mask=(SYSTEM_SOC_CLK_MAX << SYSTEM_SOC_CLK_SEL_S),
705            )
706            sleep(0.1)
707            reg_mod.write_reg(
708                SYSTEM_CPU_PER_CONF_REG,
709                0,
710                mask=(SYSTEM_CPUPERIOD_MAX << SYSTEM_CPUPERIOD_SEL_S),
711            )
712
713        output = self.run_esptool(
714            "--before no_reset write_flash 0x0 images/one_mb.bin", preload=False
715        )
716        slower = re.search(r"(\d+(\.\d+)?)\s+seconds", output)
717        assert slower, "Duration summary not found in the output"
718        assert (
719            float(slower.group(1)) - float(faster.group(1)) > 1
720        ), "Overclocking failed"
721
722    @pytest.mark.skipif(arg_preload_port is False, reason="USB-JTAG/Serial only")
723    @pytest.mark.skipif(arg_chip != "esp32c3", reason="ESP32-C3 only")
724    def test_flash_watchdogs(self):
725        RTC_WDT_ENABLE = 0xC927FA00  # Valid only for ESP32-C3
726
727        with esptool.cmds.detect_chip(port=arg_port) as reg_mod:
728            # Enable RTC WDT
729            reg_mod.write_reg(
730                reg_mod.RTC_CNTL_WDTWPROTECT_REG, reg_mod.RTC_CNTL_WDT_WKEY
731            )
732            reg_mod.write_reg(reg_mod.RTC_CNTL_WDTCONFIG0_REG, RTC_WDT_ENABLE)
733            reg_mod.write_reg(reg_mod.RTC_CNTL_WDTWPROTECT_REG, 0)
734
735            # Disable automatic feeding of SWD
736            reg_mod.write_reg(
737                reg_mod.RTC_CNTL_SWD_WPROTECT_REG, reg_mod.RTC_CNTL_SWD_WKEY
738            )
739            reg_mod.write_reg(
740                reg_mod.RTC_CNTL_SWD_CONF_REG, 0, mask=reg_mod.RTC_CNTL_SWD_AUTO_FEED_EN
741            )
742            reg_mod.write_reg(reg_mod.RTC_CNTL_SWD_WPROTECT_REG, 0)
743
744            reg_mod.sync_stub_detected = False
745            reg_mod.run_stub()
746
747        output = self.run_esptool(
748            "--before no_reset --after no_reset_stub flash_id", preload=False
749        )
750        assert "Stub is already running. No upload is necessary." in output
751
752        time.sleep(10)  # Wait if RTC WDT triggers
753
754        with esptool.cmds.detect_chip(
755            port=arg_port, connect_mode="no_reset"
756        ) as reg_mod:
757            output = reg_mod.read_reg(reg_mod.RTC_CNTL_WDTCONFIG0_REG)
758            assert output == 0, "RTC WDT is not disabled"
759
760            output = reg_mod.read_reg(reg_mod.RTC_CNTL_SWD_CONF_REG)
761            print(f"RTC_CNTL_SWD_CONF_REG: {output}")
762            assert output & 0x80000000, "SWD auto feeding is not disabled"
763
764
765@pytest.mark.skipif(
766    arg_chip in ["esp8266", "esp32"],
767    reason="get_security_info command is supported on ESP32S2 and later",
768)
769class TestSecurityInfo(EsptoolTestCase):
770    def test_show_security_info(self):
771        res = self.run_esptool("get_security_info")
772        assert "Flags" in res
773        assert "Crypt Count" in res
774        if arg_chip != "esp32c2":
775            assert "Key Purposes" in res
776        if arg_chip != "esp32s2":
777            try:
778                esp = esptool.get_default_connected_device(
779                    [arg_port], arg_port, 10, 115200, arg_chip
780                )
781                assert f"Chip ID: {esp.IMAGE_CHIP_ID}" in res
782                assert "API Version" in res
783            finally:
784                esp._port.close()
785        assert "Secure Boot" in res
786        assert "Flash Encryption" in res
787
788
789class TestFlashSizes(EsptoolTestCase):
790    def test_high_offset(self):
791        self.run_esptool("write_flash -fs 4MB 0x300000 images/one_kb.bin")
792        self.verify_readback(0x300000, 1024, "images/one_kb.bin")
793
794    def test_high_offset_no_compression(self):
795        self.run_esptool("write_flash -u -fs 4MB 0x300000 images/one_kb.bin")
796        self.verify_readback(0x300000, 1024, "images/one_kb.bin")
797
798    def test_large_image(self):
799        self.run_esptool("write_flash -fs 4MB 0x280000 images/one_mb.bin")
800        self.verify_readback(0x280000, 0x100000, "images/one_mb.bin")
801
802    def test_large_no_compression(self):
803        self.run_esptool("write_flash -u -fs 4MB 0x280000 images/one_mb.bin")
804        self.verify_readback(0x280000, 0x100000, "images/one_mb.bin")
805
806    @pytest.mark.quick_test
807    @pytest.mark.host_test
808    def test_invalid_size_arg(self):
809        self.run_esptool_error("write_flash -fs 10MB 0x6000 images/one_kb.bin")
810
811    def test_write_past_end_fails(self):
812        output = self.run_esptool_error(
813            "write_flash -fs 1MB 0x280000 images/one_kb.bin"
814        )
815        assert "File images/one_kb.bin" in output
816        assert "will not fit" in output
817
818    def test_write_no_compression_past_end_fails(self):
819        output = self.run_esptool_error(
820            "write_flash -u -fs 1MB 0x280000 images/one_kb.bin"
821        )
822        assert "File images/one_kb.bin" in output
823        assert "will not fit" in output
824
825    @pytest.mark.skipif(
826        arg_chip not in ["esp8266", "esp32", "esp32c3"],
827        reason="Don't run on every chip, so other bootloader images are not needed",
828    )
829    def test_flash_size_keep(self):
830        offset = 0x1000 if arg_chip in ["esp32", "esp32s2"] else 0x0
831
832        # this image is configured for 2MB (512KB on ESP8266) flash by default.
833        # assume this is not the flash size in use
834        image = f"images/bootloader_{arg_chip}.bin"
835
836        with open(image, "rb") as f:
837            f.seek(0, 2)
838            image_len = f.tell()
839        self.run_esptool(f"write_flash -fs keep {offset} {image}")
840        # header should be the same as in the .bin file
841        self.verify_readback(offset, image_len, image)
842
843    @pytest.mark.skipif(
844        arg_chip == "esp8266", reason="ESP8266 does not support read_flash_slow"
845    )
846    def test_read_nostub_high_offset(self):
847        offset = 0x300000
848        length = 1024
849        self.run_esptool(f"write_flash -fs detect {offset} images/one_kb.bin")
850        dump_file = tempfile.NamedTemporaryFile(delete=False)
851        # readback with no-stub and flash-size set
852        try:
853            self.run_esptool(
854                f"--no-stub read_flash -fs detect {offset} 1024 {dump_file.name}"
855            )
856            with open(dump_file.name, "rb") as f:
857                rb = f.read()
858            assert length == len(
859                rb
860            ), f"read_flash length {length} offset {offset:#x} yielded {len(rb)} bytes!"
861        finally:
862            dump_file.close()
863            os.unlink(dump_file.name)
864        # compare files
865        with open("images/one_kb.bin", "rb") as f:
866            ct = f.read()
867        self.diff(rb, ct)
868
869
870class TestFlashDetection(EsptoolTestCase):
871    @pytest.mark.quick_test
872    def test_flash_id(self):
873        """Test manufacturer and device response of flash detection."""
874        res = self.run_esptool("flash_id")
875        assert "Manufacturer:" in res
876        assert "Device:" in res
877
878    @pytest.mark.quick_test
879    def test_flash_id_expand_args(self):
880        """
881        Test manufacturer and device response of flash detection with expandable arg
882        """
883        try:
884            arg_file = tempfile.NamedTemporaryFile(delete=False)
885            arg_file.write(b"flash_id\n")
886            arg_file.close()
887            res = self.run_esptool(f"@{arg_file.name}")
888            assert "Manufacturer:" in res
889            assert "Device:" in res
890        finally:
891            os.unlink(arg_file.name)
892
893    @pytest.mark.quick_test
894    def test_flash_id_trace(self):
895        """Test trace functionality on flash detection, running without stub"""
896        res = self.run_esptool("--trace flash_id")
897        # read register command
898        assert re.search(r"TRACE \+\d.\d{3} command op=0x0a .*", res) is not None
899        # write register command
900        assert re.search(r"TRACE \+\d.\d{3} command op=0x09 .*", res) is not None
901        assert re.search(r"TRACE \+\d.\d{3} Read \d* bytes: .*", res) is not None
902        assert re.search(r"TRACE \+\d.\d{3} Write \d* bytes: .*", res) is not None
903        assert re.search(r"TRACE \+\d.\d{3} Received full packet: .*", res) is not None
904        # flasher stub handshake
905        assert (
906            re.search(r"TRACE \+\d.\d{3} Received full packet: 4f484149", res)
907            is not None
908        )
909        assert "Manufacturer:" in res
910        assert "Device:" in res
911
912    @pytest.mark.quick_test
913    @pytest.mark.skipif(
914        arg_chip not in ["esp32c2"],
915        reason="This test make sense only for EPS32-C2",
916    )
917    def test_flash_size(self):
918        """Test ESP32-C2 efuse block for flash size feature"""
919        # ESP32-C2 class inherits methods from ESP32-C3 class
920        # but it does not have the same amount of efuse blocks
921        # the methods are overwritten
922        # in case anything changes this test will fail to remind us
923        res = self.run_esptool("flash_id")
924        lines = res.splitlines()
925        for line in lines:
926            assert "embedded flash" not in line.lower()
927
928    @pytest.mark.quick_test
929    def test_flash_sfdp(self):
930        """Test manufacturer and device response of flash detection."""
931        res = self.run_esptool("read_flash_sfdp 0 4")
932        assert "SFDP[0..3]: 53 46 44 50" in res
933        res = self.run_esptool("read_flash_sfdp 1 3")
934        assert "SFDP[1..3]: 46 44 50 " in res
935
936
937@pytest.mark.skipif(
938    os.getenv("ESPTOOL_TEST_SPI_CONN") is None, reason="Needs external flash"
939)
940class TestExternalFlash(EsptoolTestCase):
941    conn = os.getenv("ESPTOOL_TEST_SPI_CONN")
942
943    def test_short_flash_to_external_stub(self):
944        # First flash internal flash, then external
945        self.run_esptool("write_flash 0x0 images/one_kb.bin")
946        self.run_esptool(
947            f"write_flash --spi-connection {self.conn} 0x0 images/sector.bin"
948        )
949
950        self.verify_readback(0, 1024, "images/one_kb.bin")
951        self.verify_readback(0, 1024, "images/sector.bin", spi_connection=self.conn)
952
953        # First flash external flash, then internal
954        self.run_esptool(
955            f"write_flash --spi-connection {self.conn} 0x0 images/one_kb.bin"
956        )
957        self.run_esptool("write_flash 0x0 images/sector.bin")
958
959        self.verify_readback(0, 1024, "images/sector.bin")
960        self.verify_readback(0, 1024, "images/one_kb.bin", spi_connection=self.conn)
961
962    def test_short_flash_to_external_ROM(self):
963        # First flash internal flash, then external
964        self.run_esptool("--no-stub write_flash 0x0 images/one_kb.bin")
965        self.run_esptool(
966            f"--no-stub write_flash --spi-connection {self.conn} 0x0 images/sector.bin"
967        )
968
969        self.verify_readback(0, 1024, "images/one_kb.bin")
970        self.verify_readback(0, 1024, "images/sector.bin", spi_connection=self.conn)
971
972        # First flash external flash, then internal
973        self.run_esptool(
974            f"--no-stub write_flash --spi-connection {self.conn} 0x0 images/one_kb.bin"
975        )
976        self.run_esptool("--no-stub write_flash 0x0 images/sector.bin")
977
978        self.verify_readback(0, 1024, "images/sector.bin")
979        self.verify_readback(0, 1024, "images/one_kb.bin", spi_connection=self.conn)
980
981
982class TestStubReuse(EsptoolTestCase):
983    def test_stub_reuse_with_synchronization(self):
984        """Keep the flasher stub running and reuse it the next time."""
985        res = self.run_esptool(
986            "--after no_reset_stub flash_id"
987        )  # flasher stub keeps running after this
988        assert "Manufacturer:" in res
989        res = self.run_esptool(
990            "--before no_reset flash_id",
991            preload=False,
992        )  # do sync before (without reset it talks to the flasher stub)
993        assert "Manufacturer:" in res
994
995    @pytest.mark.skipif(arg_chip != "esp8266", reason="ESP8266 only")
996    def test_stub_reuse_without_synchronization(self):
997        """
998        Keep the flasher stub running and reuse it the next time
999        without synchronization.
1000
1001        Synchronization is necessary for chips where the ROM bootloader has different
1002        status length in comparison to the flasher stub.
1003        Therefore, this is ESP8266 only test.
1004        """
1005        res = self.run_esptool("--after no_reset_stub flash_id")
1006        assert "Manufacturer:" in res
1007        res = self.run_esptool("--before no_reset_no_sync flash_id")
1008        assert "Manufacturer:" in res
1009
1010
1011class TestErase(EsptoolTestCase):
1012    @pytest.mark.quick_test
1013    def test_chip_erase(self):
1014        self.run_esptool("write_flash 0x10000 images/one_kb.bin")
1015        self.verify_readback(0x10000, 0x400, "images/one_kb.bin")
1016        self.run_esptool("erase_flash")
1017        empty = self.readback(0x10000, 0x400)
1018        assert empty == b"\xFF" * 0x400
1019
1020    def test_region_erase(self):
1021        self.run_esptool("write_flash 0x10000 images/one_kb.bin")
1022        self.run_esptool("write_flash 0x11000 images/sector.bin")
1023        self.verify_readback(0x10000, 0x400, "images/one_kb.bin")
1024        self.verify_readback(0x11000, 0x1000, "images/sector.bin")
1025        # erase only the flash sector containing one_kb.bin
1026        self.run_esptool("erase_region 0x10000 0x1000")
1027        self.verify_readback(0x11000, 0x1000, "images/sector.bin")
1028        empty = self.readback(0x10000, 0x1000)
1029        assert empty == b"\xFF" * 0x1000
1030
1031    def test_region_erase_all(self):
1032        res = self.run_esptool("erase_region 0x0 ALL")
1033        assert re.search(r"Detected flash size: \d+[KM]B", res) is not None
1034
1035    def test_large_region_erase(self):
1036        # verifies that erasing a large region doesn't time out
1037        self.run_esptool("erase_region 0x0 0x100000")
1038
1039
1040class TestSectorBoundaries(EsptoolTestCase):
1041    def test_end_sector(self):
1042        self.run_esptool("write_flash 0x10000 images/sector.bin")
1043        self.run_esptool("write_flash 0x0FC00 images/one_kb.bin")
1044        self.verify_readback(0x0FC00, 0x400, "images/one_kb.bin")
1045        self.verify_readback(0x10000, 0x1000, "images/sector.bin")
1046
1047    def test_end_sector_uncompressed(self):
1048        self.run_esptool("write_flash -u 0x10000 images/sector.bin")
1049        self.run_esptool("write_flash -u 0x0FC00 images/one_kb.bin")
1050        self.verify_readback(0x0FC00, 0x400, "images/one_kb.bin")
1051        self.verify_readback(0x10000, 0x1000, "images/sector.bin")
1052
1053    def test_overlap(self):
1054        self.run_esptool("write_flash 0x20800 images/sector.bin")
1055        self.verify_readback(0x20800, 0x1000, "images/sector.bin")
1056
1057
1058class TestVerifyCommand(EsptoolTestCase):
1059    @pytest.mark.quick_test
1060    def test_verify_success(self):
1061        self.run_esptool("write_flash 0x5000 images/one_kb.bin")
1062        self.run_esptool("verify_flash 0x5000 images/one_kb.bin")
1063
1064    def test_verify_failure(self):
1065        self.run_esptool("write_flash 0x6000 images/sector.bin")
1066        output = self.run_esptool_error(
1067            "verify_flash --diff=yes 0x6000 images/one_kb.bin"
1068        )
1069        assert "verify FAILED" in output
1070        assert "first @ 0x00006000" in output
1071
1072    def test_verify_unaligned_length(self):
1073        self.run_esptool("write_flash 0x0 images/not_4_byte_aligned.bin")
1074        self.run_esptool("verify_flash 0x0 images/not_4_byte_aligned.bin")
1075
1076
1077class TestReadIdentityValues(EsptoolTestCase):
1078    @pytest.mark.quick_test
1079    def test_read_mac(self):
1080        output = self.run_esptool("read_mac")
1081        mac = re.search(r"[0-9a-f:]{17}", output)
1082        assert mac is not None
1083        mac = mac.group(0)
1084        assert mac != "00:00:00:00:00:00"
1085        assert mac != "ff:ff:ff:ff:ff:ff"
1086
1087    @pytest.mark.skipif(arg_chip != "esp8266", reason="ESP8266 only")
1088    def test_read_chip_id(self):
1089        output = self.run_esptool("chip_id")
1090        idstr = re.search("Chip ID: 0x([0-9a-f]+)", output)
1091        assert idstr is not None
1092        idstr = idstr.group(1)
1093        assert idstr != "0" * 8
1094        assert idstr != "f" * 8
1095
1096
1097class TestMemoryOperations(EsptoolTestCase):
1098    @pytest.mark.quick_test
1099    def test_memory_dump(self):
1100        output = self.run_esptool("dump_mem 0x50000000 128 memout.bin")
1101        assert "Read 128 bytes" in output
1102        os.remove("memout.bin")
1103
1104    def test_memory_write(self):
1105        output = self.run_esptool("write_mem 0x400C0000 0xabad1dea 0x0000ffff")
1106        assert "Wrote abad1dea" in output
1107        assert "mask 0000ffff" in output
1108        assert "to 400c0000" in output
1109
1110    def test_memory_read(self):
1111        output = self.run_esptool("read_mem 0x400C0000")
1112        assert "0x400c0000 =" in output
1113
1114
1115class TestKeepImageSettings(EsptoolTestCase):
1116    """Tests for the -fm keep, -ff keep options for write_flash"""
1117
1118    @classmethod
1119    def setup_class(self):
1120        super(TestKeepImageSettings, self).setup_class()
1121        self.BL_IMAGE = f"images/bootloader_{arg_chip}.bin"
1122        self.flash_offset = esptool.CHIP_DEFS[arg_chip].BOOTLOADER_FLASH_OFFSET
1123        with open(self.BL_IMAGE, "rb") as f:
1124            self.header = f.read(8)
1125
1126    @pytest.mark.skipif(
1127        arg_chip not in ["esp8266", "esp32", "esp32c3"],
1128        reason="Don't run on every chip, so other bootloader images are not needed",
1129    )
1130    def test_keep_does_not_change_settings(self):
1131        # defaults should all be keep
1132        self.run_esptool(f"write_flash -fs keep {self.flash_offset:#x} {self.BL_IMAGE}")
1133        self.verify_readback(self.flash_offset, 8, self.BL_IMAGE, False)
1134        # can also explicitly set all options
1135        self.run_esptool(
1136            f"write_flash -fm keep -ff keep -fs keep "
1137            f"{self.flash_offset:#x} {self.BL_IMAGE}"
1138        )
1139        self.verify_readback(self.flash_offset, 8, self.BL_IMAGE, False)
1140        # verify_flash should also use 'keep'
1141        self.run_esptool(
1142            f"verify_flash -fs keep {self.flash_offset:#x} {self.BL_IMAGE}"
1143        )
1144
1145    @pytest.mark.skipif(
1146        arg_chip not in ["esp8266", "esp32", "esp32c3"],
1147        reason="Don't run for every chip, so other bootloader images are not needed",
1148    )
1149    @pytest.mark.quick_test
1150    def test_detect_size_changes_size(self):
1151        self.run_esptool(
1152            f"write_flash -fs detect {self.flash_offset:#x} {self.BL_IMAGE}"
1153        )
1154        readback = self.readback(self.flash_offset, 8)
1155        assert self.header[:3] == readback[:3]  # first 3 bytes unchanged
1156        assert self.header[3] != readback[3]  # size_freq byte changed
1157        assert self.header[4:] == readback[4:]  # rest unchanged
1158
1159    @pytest.mark.skipif(
1160        arg_chip not in ["esp8266", "esp32"],
1161        reason="Bootloader header needs to be modifiable - without sha256",
1162    )
1163    def test_explicit_set_size_freq_mode(self):
1164        self.run_esptool(
1165            f"write_flash -fs 2MB -fm dout -ff 80m "
1166            f"{self.flash_offset:#x} {self.BL_IMAGE}"
1167        )
1168
1169        readback = self.readback(self.flash_offset, 8)
1170        assert self.header[0] == readback[0]
1171        assert self.header[1] == readback[1]
1172        assert (0x3F if arg_chip == "esp8266" else 0x1F) == readback[3]  # size_freq
1173
1174        assert 3 != self.header[2]  # original image not dout mode
1175        assert 3 == readback[2]  # value in flash is dout mode
1176
1177        assert self.header[3] != readback[3]  # size/freq values have changed
1178        assert self.header[4:] == readback[4:]  # entrypoint address hasn't changed
1179
1180        # verify_flash should pass if we match params, fail otherwise
1181        self.run_esptool(
1182            f"verify_flash -fs 2MB -fm dout -ff 80m "
1183            f"{self.flash_offset:#x} {self.BL_IMAGE}"
1184        )
1185        self.run_esptool_error(f"verify_flash {self.flash_offset:#x} {self.BL_IMAGE}")
1186
1187
1188@pytest.mark.skipif(
1189    arg_chip in ["esp32s2", "esp32s3", "esp32p4"],
1190    reason="Not supported on targets with USB-CDC.",
1191)
1192class TestLoadRAM(EsptoolTestCase):
1193    # flashing an application not supporting USB-CDC will make
1194    # /dev/ttyACM0 disappear and USB-CDC tests will not work anymore
1195
1196    def verify_output(self, expected_out: List[bytes]):
1197        """Verify that at least one element of expected_out is in serial output"""
1198        # Setting rtscts to true enables hardware flow control.
1199        # This removes unwanted RTS logic level changes for some machines
1200        # (and, therefore, chip resets)
1201        # when the port is opened by the following function.
1202        # As a result, the app loaded to RAM has a chance to run and send
1203        # "Hello world" data without unwanted chip reset.
1204        with serial.serial_for_url(arg_port, arg_baud, rtscts=True) as p:
1205            p.timeout = 5
1206            output = p.read(100)
1207            print(f"Output: {output}")
1208            assert any(item in output for item in expected_out)
1209
1210    @pytest.mark.quick_test
1211    def test_load_ram(self):
1212        """Verify load_ram command
1213
1214        The "hello world" binary programs for each chip print
1215        "Hello world!\n" to the serial port.
1216        """
1217        self.run_esptool(f"load_ram images/ram_helloworld/helloworld-{arg_chip}.bin")
1218        self.verify_output(
1219            [b"Hello world!", b'\xce?\x13\x05\x04\xd0\x97A\x11"\xc4\x06\xc67\x04']
1220        )
1221
1222    def test_load_ram_hex(self):
1223        """Verify load_ram command with hex file as input
1224
1225        The "hello world" binary programs for each chip print
1226        "Hello world!\n" to the serial port.
1227        """
1228        fd, f = tempfile.mkstemp(suffix=".hex")
1229        try:
1230            self.run_esptool(
1231                f"merge_bin --format hex -o {f} 0x0 "
1232                f"images/ram_helloworld/helloworld-{arg_chip}.bin"
1233            )
1234            # make sure file is closed before running next command (mainly for Windows)
1235            os.close(fd)
1236            self.run_esptool(f"load_ram {f}")
1237            self.verify_output(
1238                [b"Hello world!", b'\xce?\x13\x05\x04\xd0\x97A\x11"\xc4\x06\xc67\x04']
1239            )
1240        finally:
1241            os.unlink(f)
1242
1243
1244class TestDeepSleepFlash(EsptoolTestCase):
1245    @pytest.mark.skipif(arg_chip != "esp8266", reason="ESP8266 only")
1246    def test_deep_sleep_flash(self):
1247        """Regression test for https://github.com/espressif/esptool/issues/351
1248
1249        ESP8266 deep sleep can disable SPI flash chip,
1250        stub loader (or ROM loader) needs to re-enable it.
1251
1252        NOTE: If this test fails, the ESP8266 may need a hard power cycle
1253        (probably with GPIO0 held LOW) to recover.
1254        """
1255        # not even necessary to wake successfully from sleep,
1256        # going into deep sleep is enough
1257        # (so GPIO16, etc, config is not important for this test)
1258        self.run_esptool("write_flash 0x0 images/esp8266_deepsleep.bin", baud=230400)
1259
1260        time.sleep(0.25)  # give ESP8266 time to enter deep sleep
1261
1262        self.run_esptool("write_flash 0x0 images/fifty_kb.bin", baud=230400)
1263        self.verify_readback(0, 50 * 1024, "images/fifty_kb.bin")
1264
1265
1266class TestBootloaderHeaderRewriteCases(EsptoolTestCase):
1267    @pytest.mark.skipif(
1268        arg_chip not in ["esp8266", "esp32", "esp32c3"],
1269        reason="Don't run on every chip, so other bootloader images are not needed",
1270    )
1271    @pytest.mark.quick_test
1272    def test_flash_header_rewrite(self):
1273        bl_offset = esptool.CHIP_DEFS[arg_chip].BOOTLOADER_FLASH_OFFSET
1274        bl_image = f"images/bootloader_{arg_chip}.bin"
1275
1276        output = self.run_esptool(
1277            f"write_flash -fm dout -ff 20m {bl_offset:#x} {bl_image}"
1278        )
1279        if arg_chip in ["esp8266", "esp32"]:
1280            # ESP8266 doesn't support this; The test image for ESP32 just doesn't have it.
1281            assert "Flash params set to" in output
1282        else:
1283            assert "Flash params set to" in output
1284            # Since SHA recalculation is supported for changed bootloader header
1285            assert "SHA digest in image updated" in output
1286
1287    def test_flash_header_no_magic_no_rewrite(self):
1288        # first image doesn't start with magic byte, second image does
1289        # but neither are valid bootloader binary images for either chip
1290        bl_offset = esptool.CHIP_DEFS[arg_chip].BOOTLOADER_FLASH_OFFSET
1291        for image in ["images/one_kb.bin", "images/one_kb_all_ef.bin"]:
1292            output = self.run_esptool(
1293                f"write_flash -fm dout -ff 20m {bl_offset:#x} {image}"
1294            )
1295            "not changing any flash settings" in output
1296            self.verify_readback(bl_offset, 1024, image)
1297
1298
1299class TestAutoDetect(EsptoolTestCase):
1300    def _check_output(self, output):
1301        expected_chip_name = esptool.util.expand_chip_name(arg_chip)
1302        if arg_chip not in ["esp8266", "esp32", "esp32s2"]:
1303            assert "Unsupported detection protocol" not in output
1304        assert f"Detecting chip type... {expected_chip_name}" in output
1305        assert f"Chip is {expected_chip_name}" in output
1306
1307    @pytest.mark.quick_test
1308    def test_auto_detect(self):
1309        output = self.run_esptool("chip_id", chip="auto")
1310        self._check_output(output)
1311
1312
1313@pytest.mark.flaky(reruns=5)
1314@pytest.mark.skipif(arg_preload_port is not False, reason="USB-to-UART bridge only")
1315@pytest.mark.skipif(os.name == "nt", reason="Linux/MacOS only")
1316class TestVirtualPort(TestAutoDetect):
1317    def test_auto_detect_virtual_port(self):
1318        with ESPRFC2217Server() as server:
1319            output = self.run_esptool(
1320                "chip_id",
1321                chip="auto",
1322                port=f"rfc2217://localhost:{str(server.port)}?ign_set_control",
1323            )
1324            self._check_output(output)
1325
1326    def test_highspeed_flash_virtual_port(self):
1327        with ESPRFC2217Server() as server:
1328            rfc2217_port = f"rfc2217://localhost:{str(server.port)}?ign_set_control"
1329            self.run_esptool(
1330                "write_flash 0x0 images/fifty_kb.bin",
1331                baud=921600,
1332                port=rfc2217_port,
1333            )
1334        self.verify_readback(0, 50 * 1024, "images/fifty_kb.bin")
1335
1336    @pytest.fixture
1337    def pty_port(self):
1338        import pty
1339
1340        master_fd, slave_fd = pty.openpty()
1341        yield os.ttyname(slave_fd)
1342        os.close(master_fd)
1343        os.close(slave_fd)
1344
1345    @pytest.mark.host_test
1346    def test_pty_port(self, pty_port):
1347        cmd = [sys.executable, "-m", "esptool", "--port", pty_port, "chip_id"]
1348        output = subprocess.run(
1349            cmd,
1350            cwd=TEST_DIR,
1351            stdout=subprocess.PIPE,
1352            stderr=subprocess.STDOUT,
1353        )
1354        # no chip connected so command should fail
1355        assert output.returncode != 0
1356        output = output.stdout.decode("utf-8")
1357        print(output)  # for logging
1358        assert "WARNING: Chip was NOT reset." in output
1359
1360
1361@pytest.mark.quick_test
1362class TestReadWriteMemory(EsptoolTestCase):
1363    def _test_read_write(self, esp):
1364        # find the start of one of these named memory regions
1365        test_addr = None
1366        for test_region in [
1367            "RTC_DRAM",
1368            "RTC_DATA",
1369            "DRAM",
1370        ]:  # find a probably-unused memory type
1371            region = esp.get_memory_region(test_region)
1372            if region:
1373                if arg_chip == "esp32c61":
1374                    # Write into the "BYTE_ACCESSIBLE" space and after the stub
1375                    region = esp.get_memory_region("DRAM")
1376                    test_addr = region[1] - 0x2FFFF
1377                elif arg_chip == "esp32c2":
1378                    # Write at the end of DRAM on ESP32-C2 to avoid overwriting the stub
1379                    test_addr = region[1] - 8
1380                else:
1381                    test_addr = region[0]
1382                break
1383
1384        print(f"Using test address {test_addr:#x}")
1385
1386        val = esp.read_reg(test_addr)  # verify we can read this word at all
1387
1388        try:
1389            esp.write_reg(test_addr, 0x1234567)
1390            assert esp.read_reg(test_addr) == 0x1234567
1391
1392            esp.write_reg(test_addr, 0, delay_us=100)
1393            assert esp.read_reg(test_addr) == 0
1394
1395            esp.write_reg(test_addr, 0x555, delay_after_us=100)
1396            assert esp.read_reg(test_addr) == 0x555
1397        finally:
1398            esp.write_reg(test_addr, val)  # write the original value, non-destructive
1399            esp._port.close()
1400
1401    def test_read_write_memory_rom(self):
1402        try:
1403            esp = esptool.get_default_connected_device(
1404                [arg_port], arg_port, 10, 115200, arg_chip
1405            )
1406            self._test_read_write(esp)
1407        finally:
1408            esp._port.close()
1409
1410    def test_read_write_memory_stub(self):
1411        try:
1412            esp = esptool.get_default_connected_device(
1413                [arg_port], arg_port, 10, 115200, arg_chip
1414            )
1415            esp = esp.run_stub()
1416            self._test_read_write(esp)
1417        finally:
1418            esp._port.close()
1419
1420    @pytest.mark.skipif(
1421        arg_chip != "esp32", reason="Could be unsupported by different flash"
1422    )
1423    def test_read_write_flash_status(self):
1424        """Read flash status and write back the same status"""
1425        res = self.run_esptool("read_flash_status")
1426        match = re.search(r"Status value: (0x[\d|a-f]*)", res)
1427        assert match is not None
1428        res = self.run_esptool(f"write_flash_status {match.group(1)}")
1429        assert f"Initial flash status: {match.group(1)}" in res
1430        assert f"Setting flash status: {match.group(1)}" in res
1431        assert f"After flash status:   {match.group(1)}" in res
1432
1433    def test_read_chip_description(self):
1434        try:
1435            esp = esptool.get_default_connected_device(
1436                [arg_port], arg_port, 10, 115200, arg_chip
1437            )
1438            chip = esp.get_chip_description()
1439            assert "unknown" not in chip.lower()
1440        finally:
1441            esp._port.close()
1442
1443    def test_read_get_chip_features(self):
1444        try:
1445            esp = esptool.get_default_connected_device(
1446                [arg_port], arg_port, 10, 115200, arg_chip
1447            )
1448
1449            if hasattr(esp, "get_flash_cap") and esp.get_flash_cap() == 0:
1450                esp.get_flash_cap = MagicMock(return_value=1)
1451            if hasattr(esp, "get_psram_cap") and esp.get_psram_cap() == 0:
1452                esp.get_psram_cap = MagicMock(return_value=1)
1453
1454            features = ", ".join(esp.get_chip_features())
1455            assert "Unknown Embedded Flash" not in features
1456            assert "Unknown Embedded PSRAM" not in features
1457        finally:
1458            esp._port.close()
1459
1460
1461@pytest.mark.skipif(
1462    arg_chip != "esp8266", reason="Make image option is supported only on ESP8266"
1463)
1464class TestMakeImage(EsptoolTestCase):
1465    def verify_image(self, offset, length, image, compare_to):
1466        with open(image, "rb") as f:
1467            f.seek(offset)
1468            rb = f.read(length)
1469        with open(compare_to, "rb") as f:
1470            ct = f.read()
1471        if len(rb) != len(ct):
1472            print(
1473                f"WARNING: Expected length {len(ct)} doesn't match comparison {len(rb)}"
1474            )
1475        print(f"Readback {len(rb)} bytes")
1476        self.diff(rb, ct)
1477
1478    def test_make_image(self):
1479        output = self.run_esptool(
1480            "make_image test"
1481            " -a 0x0 -f images/sector.bin -a 0x1000 -f images/fifty_kb.bin"
1482        )
1483        try:
1484            assert "Successfully created esp8266 image." in output
1485            assert os.path.exists("test0x00000.bin")
1486            self.verify_image(16, 4096, "test0x00000.bin", "images/sector.bin")
1487            self.verify_image(
1488                4096 + 24, 50 * 1024, "test0x00000.bin", "images/fifty_kb.bin"
1489            )
1490        finally:
1491            os.remove("test0x00000.bin")
1492
1493
1494@pytest.mark.skipif(arg_chip != "esp32", reason="Don't need to test multiple times")
1495@pytest.mark.quick_test
1496class TestConfigFile(EsptoolTestCase):
1497    class ConfigFile:
1498        """
1499        A class-based context manager to create
1500        a custom config file and delete it after usage.
1501        """
1502
1503        def __init__(self, file_path, file_content):
1504            self.file_path = file_path
1505            self.file_content = file_content
1506
1507        def __enter__(self):
1508            with open(self.file_path, "w") as cfg_file:
1509                cfg_file.write(self.file_content)
1510                return cfg_file
1511
1512        def __exit__(self, exc_type, exc_value, exc_tb):
1513            os.unlink(self.file_path)
1514            assert not os.path.exists(self.file_path)
1515
1516    dummy_config = (
1517        "[esptool]\n"
1518        "connect_attempts = 5\n"
1519        "reset_delay = 1\n"
1520        "serial_write_timeout = 12"
1521    )
1522
1523    @pytest.mark.host_test
1524    def test_load_config_file(self):
1525        # Test a valid file is loaded
1526        config_file_path = os.path.join(os.getcwd(), "esptool.cfg")
1527        with self.ConfigFile(config_file_path, self.dummy_config):
1528            output = self.run_esptool("version")
1529            assert f"Loaded custom configuration from {config_file_path}" in output
1530            assert "Ignoring unknown config file option" not in output
1531            assert "Ignoring invalid config file" not in output
1532
1533        # Test invalid files are ignored
1534        # Wrong section header, no config gets loaded
1535        with self.ConfigFile(config_file_path, "[wrong section name]"):
1536            output = self.run_esptool("version")
1537            assert f"Loaded custom configuration from {config_file_path}" not in output
1538
1539        # Correct header, but options are unparsable
1540        faulty_config = "[esptool]\n" "connect_attempts = 5\n" "connect_attempts = 9\n"
1541        with self.ConfigFile(config_file_path, faulty_config):
1542            output = self.run_esptool("version")
1543            assert f"Ignoring invalid config file {config_file_path}" in output
1544            assert (
1545                "option 'connect_attempts' in section 'esptool' already exists"
1546                in output
1547            )
1548
1549        # Correct header, unknown option (or a typo)
1550        faulty_config = (
1551            "[esptool]\n" "connect_attempts = 9\n" "timoout = 2\n" "bits = 2"
1552        )
1553        with self.ConfigFile(config_file_path, faulty_config):
1554            output = self.run_esptool("version")
1555            assert "Ignoring unknown config file options: bits, timoout" in output
1556
1557        # Test other config files (setup.cfg, tox.ini) are loaded
1558        config_file_path = os.path.join(os.getcwd(), "tox.ini")
1559        with self.ConfigFile(config_file_path, self.dummy_config):
1560            output = self.run_esptool("version")
1561            assert f"Loaded custom configuration from {config_file_path}" in output
1562
1563    @pytest.mark.host_test
1564    def test_load_config_file_with_env_var(self):
1565        config_file_path = os.path.join(TEST_DIR, "custom_file.ini")
1566        with self.ConfigFile(config_file_path, self.dummy_config):
1567            # Try first without setting the env var, check that no config gets loaded
1568            output = self.run_esptool("version")
1569            assert f"Loaded custom configuration from {config_file_path}" not in output
1570
1571            # Set the env var and try again, check that config was loaded
1572            tmp = os.environ.get("ESPTOOL_CFGFILE")  # Save the env var if it is set
1573
1574            os.environ["ESPTOOL_CFGFILE"] = config_file_path
1575            output = self.run_esptool("version")
1576            assert f"Loaded custom configuration from {config_file_path}" in output
1577            assert "(set with ESPTOOL_CFGFILE)" in output
1578
1579            if tmp is not None:  # Restore the env var or unset it
1580                os.environ["ESPTOOL_CFGFILE"] = tmp
1581            else:
1582                os.environ.pop("ESPTOOL_CFGFILE", None)
1583
1584    def test_custom_reset_sequence(self):
1585        # This reset sequence will fail to reset the chip to bootloader,
1586        # the flash_id operation should therefore fail.
1587        # Also tests the number of connection attempts.
1588        reset_seq_config = (
1589            "[esptool]\n"
1590            "custom_reset_sequence = D0|W0.1|R1|R0|W0.1|R1|R0\n"
1591            "connect_attempts = 1\n"
1592        )
1593        config_file_path = os.path.join(os.getcwd(), "esptool.cfg")
1594        with self.ConfigFile(config_file_path, reset_seq_config):
1595            output = self.run_esptool_error("flash_id")
1596            assert f"Loaded custom configuration from {config_file_path}" in output
1597            assert "A fatal error occurred: Failed to connect to" in output
1598            # Connection attempts are represented with dots,
1599            # there are enough dots for two attempts here, but only one is executed
1600            assert "Connecting............." not in output
1601
1602        # Test invalid custom_reset_sequence format is not accepted
1603        invalid_reset_seq_config = "[esptool]\n" "custom_reset_sequence = F0|R1|C0|A5\n"
1604        with self.ConfigFile(config_file_path, invalid_reset_seq_config):
1605            output = self.run_esptool_error("flash_id")
1606            assert f"Loaded custom configuration from {config_file_path}" in output
1607            assert 'Invalid "custom_reset_sequence" option format:' in output
1608
1609    def test_open_port_attempts(self):
1610        # Test that the open_port_attempts option is loaded correctly
1611        connect_attempts = 5
1612        config = (
1613            "[esptool]\n"
1614            f"open_port_attempts = {connect_attempts}\n"
1615            "connect_attempts = 1\n"
1616            "custom_reset_sequence = D0\n"  # Invalid reset sequence to make sure connection fails
1617        )
1618        config_file_path = os.path.join(os.getcwd(), "esptool.cfg")
1619        with self.ConfigFile(config_file_path, config):
1620            output = self.run_esptool_error("flash_id")
1621            assert f"Loaded custom configuration from {config_file_path}" in output
1622            assert "Retrying failed connection" in output
1623            for _ in range(connect_attempts):
1624                assert "Connecting........" in output
1625