11# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
22# SPDX-License-Identifier: Unlicense OR CC0-1.0
3+ import json
4+ import logging
35import os .path
6+ import signal
47import time
8+ from telnetlib import Telnet
9+ from typing import Any
10+ from typing import Optional
511
12+ import pexpect
613import pytest
14+ from pytest_embedded .utils import to_bytes
15+ from pytest_embedded .utils import to_str
716from pytest_embedded_idf import IdfDut
817from pytest_embedded_idf .utils import idf_parametrize
9- from pytest_embedded_jtag import OpenOcd
1018
19+ MAX_RETRIES = 3
20+ RETRY_DELAY = 1
21+ TELNET_PORT = 4444
1122
12- @pytest .mark .jtag
13- @pytest .mark .parametrize (
14- 'embedded_services, no_gdb' ,
15- [
16- ('esp,idf,jtag' , 'y' ),
17- ],
18- indirect = True ,
19- )
20- @idf_parametrize ('target' , ['esp32' ], indirect = ['target' ])
21- def test_gcov (dut : IdfDut , openocd : OpenOcd ) -> None :
23+
24+ class OpenOCD :
25+ def __init__ (self , dut : 'IdfDut' ):
26+ self .dut = dut
27+ self .telnet : Optional [Telnet ] = None
28+ self .log_file = os .path .join (self .dut .logdir , 'ocd.txt' )
29+ self .proc : Optional [pexpect .spawn ] = None
30+
31+ def run (self ) -> Optional ['OpenOCD' ]:
32+ desc_path = os .path .join (self .dut .app .binary_path , 'project_description.json' )
33+
34+ try :
35+ with open (desc_path , 'r' ) as f :
36+ project_desc = json .load (f )
37+ except FileNotFoundError :
38+ logging .error ('Project description file not found at %s' , desc_path )
39+ return None
40+
41+ openocd_scripts = os .getenv ('OPENOCD_SCRIPTS' )
42+ if not openocd_scripts :
43+ logging .error ('OPENOCD_SCRIPTS environment variable is not set.' )
44+ return None
45+
46+ debug_args = project_desc .get ('debug_arguments_openocd' )
47+ if not debug_args :
48+ logging .error ("'debug_arguments_openocd' key is missing in project_description.json" )
49+ return None
50+
51+ # For debug purposes, make the value '4'
52+ ocd_env = os .environ .copy ()
53+ ocd_env ['LIBUSB_DEBUG' ] = '1'
54+
55+ for _ in range (1 , MAX_RETRIES + 1 ):
56+ try :
57+ self .proc = pexpect .spawn (
58+ command = 'openocd' ,
59+ args = ['-s' , openocd_scripts ] + debug_args .split (),
60+ timeout = 5 ,
61+ encoding = 'utf-8' ,
62+ codec_errors = 'ignore' ,
63+ env = ocd_env ,
64+ )
65+ if self .proc and self .proc .isalive ():
66+ self .proc .expect_exact ('Info : Listening on port 3333 for gdb connections' , timeout = 5 )
67+ return self
68+ except (pexpect .exceptions .EOF , pexpect .exceptions .TIMEOUT ) as e :
69+ logging .error ('Error running OpenOCD: %s' , str (e ))
70+ if self .proc and self .proc .isalive ():
71+ self .proc .terminate ()
72+ time .sleep (RETRY_DELAY )
73+
74+ logging .error ('Failed to run OpenOCD after %d attempts.' , MAX_RETRIES )
75+ return None
76+
77+ def connect_telnet (self ) -> None :
78+ for attempt in range (1 , MAX_RETRIES + 1 ):
79+ try :
80+ self .telnet = Telnet ('127.0.0.1' , TELNET_PORT , 5 )
81+ break
82+ except ConnectionRefusedError as e :
83+ logging .error ('Error telnet connection: %s in attempt:%d' , e , attempt )
84+ time .sleep (1 )
85+ else :
86+ raise ConnectionRefusedError
87+
88+ def write (self , s : str ) -> Any :
89+ if self .telnet is None :
90+ logging .error ('Telnet connection is not established.' )
91+ return ''
92+ resp = self .telnet .read_very_eager ()
93+ self .telnet .write (to_bytes (s , '\n ' ))
94+ resp += self .telnet .read_until (b'>' )
95+ return to_str (resp )
96+
97+ def apptrace_wait_stop (self , timeout : int = 30 ) -> None :
98+ stopped = False
99+ end_before = time .time () + timeout
100+ while not stopped :
101+ cmd_out = self .write ('esp apptrace status' )
102+ for line in cmd_out .splitlines ():
103+ if line .startswith ('Tracing is STOPPED.' ):
104+ stopped = True
105+ break
106+ if not stopped and time .time () > end_before :
107+ raise pexpect .TIMEOUT ('Failed to wait for apptrace stop!' )
108+ time .sleep (1 )
109+
110+ def kill (self ) -> None :
111+ # Check if the process is still running
112+ if self .proc and self .proc .isalive ():
113+ self .proc .terminate ()
114+ self .proc .kill (signal .SIGKILL )
115+
116+
117+ def _test_gcov (dut : IdfDut ) -> None :
22118 # create the generated .gcda folder, otherwise would have error: failed to open file.
23119 # normally this folder would be created via `idf.py build`. but in CI the non-related files would not be preserved
24120 os .makedirs (os .path .join (dut .app .binary_path , 'esp-idf' , 'main' , 'CMakeFiles' , '__idf_main.dir' ), exist_ok = True )
25121 os .makedirs (os .path .join (dut .app .binary_path , 'esp-idf' , 'sample' , 'CMakeFiles' , '__idf_sample.dir' ), exist_ok = True )
26122
123+ dut .expect_exact ('example: Ready for OpenOCD connection' , timeout = 5 )
124+ openocd = OpenOCD (dut ).run ()
125+ assert openocd
126+
27127 def expect_counter_output (loop : int , timeout : int = 10 ) -> None :
28128 dut .expect_exact (
29129 [f'blink_dummy_func: Counter = { loop } ' , f'some_dummy_func: Counter = { loop * 2 } ' ],
30130 expect_all = True ,
31131 timeout = timeout ,
32132 )
33133
34- expect_counter_output (0 )
35- dut .expect ('Ready to dump GCOV data...' , timeout = 5 )
36-
37134 def dump_coverage (cmd : str ) -> None :
38135 response = openocd .write (cmd )
39136
@@ -56,18 +153,41 @@ def dump_coverage(cmd: str) -> None:
56153
57154 assert len (expect_lines ) == 0
58155
59- # Test two hard-coded dumps
60- dump_coverage ('esp gcov dump' )
61- dut .expect ('GCOV data have been dumped.' , timeout = 5 )
62- expect_counter_output (1 )
63- dut .expect ('Ready to dump GCOV data...' , timeout = 5 )
64- dump_coverage ('esp gcov dump' )
65- dut .expect ('GCOV data have been dumped.' , timeout = 5 )
66-
67- for i in range (2 , 6 ):
68- expect_counter_output (i )
69-
70- for _ in range (3 ):
71- time .sleep (1 )
72- # Test instant run-time dump
73- dump_coverage ('esp gcov' )
156+ try :
157+ openocd .connect_telnet ()
158+ openocd .write ('log_output {}' .format (openocd .log_file ))
159+ openocd .write ('reset run' )
160+ dut .expect_exact ('example: Ready for OpenOCD connection' , timeout = 5 )
161+
162+ expect_counter_output (0 )
163+ dut .expect ('Ready to dump GCOV data...' , timeout = 5 )
164+
165+ # Test two hard-coded dumps
166+ dump_coverage ('esp gcov dump' )
167+ dut .expect ('GCOV data have been dumped.' , timeout = 5 )
168+ expect_counter_output (1 )
169+ dut .expect ('Ready to dump GCOV data...' , timeout = 5 )
170+ dump_coverage ('esp gcov dump' )
171+ dut .expect ('GCOV data have been dumped.' , timeout = 5 )
172+
173+ for i in range (2 , 6 ):
174+ expect_counter_output (i )
175+
176+ for _ in range (3 ):
177+ time .sleep (1 )
178+ # Test instant run-time dump
179+ dump_coverage ('esp gcov' )
180+ finally :
181+ openocd .kill ()
182+
183+
184+ @pytest .mark .jtag
185+ @idf_parametrize ('target' , ['esp32' , 'esp32c2' , 'esp32s2' ], indirect = ['target' ])
186+ def test_gcov (dut : IdfDut ) -> None :
187+ _test_gcov (dut )
188+
189+
190+ @pytest .mark .usb_serial_jtag
191+ @idf_parametrize ('target' , ['esp32c3' , 'esp32c5' , 'esp32c6' , 'esp32c61' , 'esp32h2' ], indirect = ['target' ])
192+ def test_gcov_usj (dut : IdfDut ) -> None :
193+ _test_gcov (dut )
0 commit comments