1import os
2import os.path
3import subprocess
4import sys
5import tempfile
6
7from conftest import need_to_install_package_err
8
9import pytest
10
11try:
12    import esptool  # noqa: F401
13except ImportError:
14    need_to_install_package_err()
15
16IMAGES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "images")
17
18ESP8266_BIN = "not_4_byte_aligned.bin"
19
20
21def read_image(filename):
22    with open(os.path.join(IMAGES_DIR, filename), "rb") as f:
23        return f.read()
24
25
26@pytest.mark.host_test
27class TestImageInfo:
28    def run_image_info(self, chip, file, version=None):
29        """Runs image_info on a binary file.
30        Returns the command output.
31        Filenames are relative to the 'test/images' directory.
32        """
33
34        cmd = [
35            sys.executable,
36            "-m",
37            "esptool",
38            "--chip",
39            chip,
40            "image_info",
41        ]
42        if version is not None:
43            cmd += ["--version", str(version)]
44        # if path was passed use the whole path
45        # if file does not exists try to use file from IMAGES_DIR directory
46        cmd += [file] if os.path.isfile(file) else ["".join([IMAGES_DIR, os.sep, file])]
47        print("\nExecuting {}".format(" ".join(cmd)))
48
49        try:
50            output = subprocess.check_output(cmd)
51            output = output.decode("utf-8")
52            print(output)  # for more complete stdout logs on failure
53            assert (
54                "warning" not in output.lower()
55            ), "image_info should not output warnings"
56            return output
57        except subprocess.CalledProcessError as e:
58            print(e.output)
59            raise
60
61    def test_v1_esp32(self):
62        out = self.run_image_info("esp32", "bootloader_esp32.bin")
63        assert "Entry point: 4009816c" in out, "Wrong entry point"
64        assert "Checksum: 83 (valid)" in out, "Invalid checksum"
65        assert "4 segments" in out, "Wrong number of segments"
66        assert (
67            "Segment 3: len 0x01068 load 0x40078000 file_offs 0x00000b64 [CACHE_APP]"
68            in out
69        ), "Wrong segment info"
70
71    def test_v1_esp8266(self):
72        out = self.run_image_info("esp8266", ESP8266_BIN)
73        assert "Image version: 1" in out, "Wrong image version"
74        assert "Entry point: 40101844" in out, "Wrong entry point"
75        assert "Checksum: 6b (valid)" in out, "Invalid checksum"
76        assert "1 segments" in out, "Wrong number of segments"
77        assert (
78            "Segment 1: len 0x00014 load 0x40100000 file_offs 0x00000008 [IRAM]" in out
79        ), "Wrong segment info"
80
81    def test_v2_esp32c3(self):
82        out = self.run_image_info("esp32c3", "bootloader_esp32c3.bin", "2")
83
84        # Header
85        assert "Entry point: 0x403c0000" in out, "Wrong entry point"
86        assert "Segments: 4" in out, "Wrong num of segments"
87        assert "Flash size: 2MB" in out, "Wrong flash size"
88        assert "Flash freq: 40m" in out, "Wrong flash frequency"
89        assert "Flash mode: DIO" in out, "Wrong flash mode"
90
91        # Extended header
92        assert "WP pin: 0xee (disabled)" in out, "Wrong WP pin"
93        assert "Chip ID: 5 (ESP32-C3)" in out, "Wrong chip ID"
94        assert (
95            "clk_drv: 0x0, q_drv: 0x0, d_drv: 0x0, "
96            "cs0_drv: 0x0, hd_drv: 0x0, wp_drv: 0x0" in out
97        ), "Wrong flash pins drive settings"
98
99        assert "Minimal chip revision: v0.0" in out, "Wrong min revision"
100        assert "Maximal chip revision: v0.0" in out, "Wrong min revision"
101
102        # Segments
103        assert (
104            "1  0x01864  0x3fcd6114  0x00000034  DRAM, BYTE_ACCESSIBLE" in out
105        ), "Wrong segment info"
106
107        # Footer
108        assert "Checksum: 0x77 (valid)" in out, "Invalid checksum"
109        assert "c0a9d6d882b65580da2e5e6347 (valid)" in out, "Invalid hash"
110
111        # Check output against individual bytes in the headers
112        hdr = read_image("bootloader_esp32c3.bin")[:8]
113        ex_hdr = read_image("bootloader_esp32c3.bin")[8:24]
114        assert f"Segments: {hdr[1]}" in out, "Wrong num of segments"
115        assert f"WP pin: {ex_hdr[0]:#02x}" in out, "Wrong WP pin"
116        assert f"Chip ID: {ex_hdr[4]}" in out, "Wrong chip ID"
117        if ex_hdr[15] == 1:  # Hash appended
118            assert "Validation hash: 4faeab1bd3fd" in out, "Invalid hash"
119
120    def test_v2_esp8266(self):
121        out = self.run_image_info("esp8266", ESP8266_BIN, "2")
122        assert "Image version: 1" in out, "Wrong image version"
123        assert "Entry point: 0x40101844" in out, "Wrong entry point"
124        assert "Flash size: 512KB" in out, "Wrong flash size"
125        assert "Flash freq: 40m" in out, "Wrong flash frequency"
126        assert "Flash mode: QIO" in out, "Wrong flash mode"
127        assert "Checksum: 0x6b (valid)" in out, "Invalid checksum"
128        assert "Segments: 1" in out, "Wrong number of segments"
129        assert "0  0x00014  0x40100000  0x00000008  IRAM" in out, "Wrong segment info"
130
131    def test_image_type_detection(self):
132        # ESP8266, version 1 and 2
133        out = self.run_image_info("auto", ESP8266_BIN, "1")
134        assert "Detected image type: ESP8266" in out
135        assert "Segment 1: len 0x00014" in out
136        out = self.run_image_info("auto", ESP8266_BIN, "2")
137        assert "Detected image type: ESP8266" in out
138        assert "Flash freq: 40m" in out
139        out = self.run_image_info("auto", "esp8266_deepsleep.bin", "2")
140        assert "Detected image type: ESP8266" in out
141
142        # ESP32, with and without detection
143        out = self.run_image_info("auto", "bootloader_esp32.bin", "2")
144        assert "Detected image type: ESP32" in out
145        out = self.run_image_info(
146            "auto", "ram_helloworld/helloworld-esp32_edit.bin", "2"
147        )
148        assert "Detected image type: ESP32" in out
149        out = self.run_image_info("esp32", "bootloader_esp32.bin", "2")
150        assert "Detected image type: ESP32" not in out
151
152        # ESP32-C3
153        out = self.run_image_info("auto", "bootloader_esp32c3.bin", "2")
154        assert "Detected image type: ESP32-C3" in out
155
156        # ESP32-S3
157        out = self.run_image_info("auto", "esp32s3_header.bin", "2")
158        assert "Detected image type: ESP32-S3" in out
159
160    def test_invalid_image_type_detection(self, capsys):
161        with pytest.raises(subprocess.CalledProcessError):
162            # Invalid image
163            self.run_image_info("auto", "one_kb.bin", "2")
164        assert (
165            "This is not a valid image (invalid magic number: 0xed)"
166            in capsys.readouterr().out
167        )
168
169    def test_application_info(self):
170        out = self.run_image_info("auto", "esp_idf_blink_esp32s2.bin", "2")
171        assert "Application information" in out
172        assert "Project name: blink" in out
173        assert "App version: qa-test-v5.0-20220830-4-g4532e6" in out
174        assert "Secure version: 0" in out
175        assert "Compile time: Sep 13 2022" in out
176        assert "19:46:07" in out
177        assert "3059e6b55a965865febd28fa9f6028ad5" in out
178        assert "cd0dab311febb0a3ea79eaa223ac2b0" in out
179        assert "ESP-IDF: v5.0-beta1-427-g4532e6e0b2-dirt" in out
180        # No application info in image
181        out = self.run_image_info("auto", "bootloader_esp32.bin", "2")
182        assert "Application information" not in out
183        out = self.run_image_info("auto", ESP8266_BIN, "2")
184        assert "Application information" not in out
185
186    def test_bootloader_info(self):
187        # This bootloader binary is built from "hello_world" project
188        # with default settings, IDF version is v5.2.
189        out = self.run_image_info("esp32", "bootloader_esp32_v5_2.bin", "2")
190        assert "File size: 26768 (bytes)" in out
191        assert "Bootloader information" in out
192        assert "Bootloader version: 1" in out
193        assert "ESP-IDF: v5.2-dev-254-g1950b15" in out
194        assert "Compile time: Apr 25 2023 00:13:32" in out
195
196    def test_intel_hex(self):
197        # This bootloader binary is built from "hello_world" project
198        # with default settings, IDF version is v5.2.
199        # File is converted to Intel Hex using merge_bin
200
201        def convert_bin2hex(file):
202            subprocess.check_output(
203                [
204                    sys.executable,
205                    "-m",
206                    "esptool",
207                    "--chip",
208                    "esp32",
209                    "merge_bin",
210                    "--format",
211                    "hex",
212                    "0x0",
213                    "".join([IMAGES_DIR, os.sep, "bootloader_esp32_v5_2.bin"]),
214                    "-o",
215                    file,
216                ]
217            )
218
219        fd, file = tempfile.mkstemp(suffix=".hex")
220        try:
221            convert_bin2hex(file)
222            out = self.run_image_info("esp32", file, "2")
223            assert "File size: 26768 (bytes)" in out
224            assert "Bootloader information" in out
225            assert "Bootloader version: 1" in out
226            assert "ESP-IDF: v5.2-dev-254-g1950b15" in out
227            assert "Compile time: Apr 25 2023 00:13:32" in out
228        finally:
229            try:
230                # make sure that file was closed before removing it
231                os.close(fd)
232            except OSError:
233                pass
234            os.unlink(file)
235