Skip to content

Commit e39dcc1

Browse files
run_python_test: Start tcpdump during test case execution (#43490)
* run_python_test: Start tcpdump during test case execution Signed-off-by: Maciej Grela <m.grela@samsung.com> * Some improvements, changes suggested by automated review, try to fix missing tcpdump: - bump image version - don't break when pcap is already deleted - distinguish pcaps from different runs of the same TC - use non-interactive sudo - make exit_code handling more robust in case of exceptions - make tcpdump start/stop conditions simpler Signed-off-by: Maciej Grela <m.grela@samsung.com> * Restyled by autopep8 * Make sure tcpdump's Subprocess object is None before start() is called Signed-off-by: Maciej Grela <m.grela@samsung.com> * Remove stray dash, harmonize upload action version Signed-off-by: Maciej Grela <m.grela@samsung.com> * Fixes suggested by automated review - reverse `keep_dumpfile` logic to ensure dumps are kept in case of any exceptions - move pcap upload action to the proper job in tests.yaml --------- Signed-off-by: Maciej Grela <m.grela@samsung.com> Co-authored-by: Restyled.io <commits@restyled.io>
1 parent 2c98999 commit e39dcc1

File tree

2 files changed

+75
-6
lines changed

2 files changed

+75
-6
lines changed

.github/workflows/tests.yaml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
runs-on: ubuntu-latest
6262

6363
container:
64-
image: ghcr.io/project-chip/chip-build:184
64+
image: ghcr.io/project-chip/chip-build:190
6565
options: >-
6666
--privileged --sysctl "net.ipv6.conf.all.disable_ipv6=0
6767
net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1"
@@ -796,7 +796,7 @@ jobs:
796796
runs-on: namespace-profile-16x32
797797

798798
container:
799-
image: ghcr.io/project-chip/chip-build:181
799+
image: ghcr.io/project-chip/chip-build:190
800800
volumes:
801801
- /var/run/nsc/:/var/run/nsc/
802802
env:
@@ -1054,12 +1054,13 @@ jobs:
10541054
BUILD_VARIANT: ipv6only-no-ble-no-wifi
10551055
DISABLE_CCACHE: ${{ (github.event_name == 'workflow_dispatch' && inputs.disable_ccache == 'true') && 'true' || (contains(github.event.head_commit.message, '[no-ccache]') && 'true') || 'false' }}
10561056
CCACHE_KEY_SUFFIX: ${{ github.event_name == 'workflow_dispatch' && inputs.cache_suffix || '' }}
1057+
CHIP_IP_PACKET_CAPTURE: 1
10571058

10581059
# Tests are I/O bound, so run on a smaller machine
10591060
runs-on: namespace-profile-1x2
10601061

10611062
container:
1062-
image: ghcr.io/project-chip/chip-build:182
1063+
image: ghcr.io/project-chip/chip-build:190
10631064
volumes:
10641065
- /var/run/nsc/:/var/run/nsc/
10651066
env:
@@ -1191,6 +1192,21 @@ jobs:
11911192
docs/development_controllers/matter-repl/Matter_Multi_Fabric_Commissioning.ipynb \
11921193
"
11931194
1195+
- name: Uploading core files
1196+
uses: actions/upload-artifact@v7
1197+
if: ${{ failure() && !env.ACT }}
1198+
with:
1199+
name: crash-core-linux-repl-${{ matrix.filter }}
1200+
path: /tmp/cores/
1201+
# Cores are big; don't hold on to them too long.
1202+
retention-days: 5
1203+
- name: Uploading packet captures for debugging
1204+
uses: actions/upload-artifact@v7
1205+
if: ${{ failure() && !env.ACT }}
1206+
with:
1207+
name: pcaps-linux-repl-${{ matrix.filter }}
1208+
path: out/ip_packet_captures
1209+
11941210
# Example debug breakpoint:
11951211
#
11961212
# - name: Breakpoint if tests failed

scripts/tests/run_python_test.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import dataclasses
1919
import datetime
2020
import enum
21+
import getpass
2122
import glob
2223
import io
2324
import logging
@@ -47,6 +48,7 @@
4748

4849
MATTER_DEVELOPMENT_PAA_ROOT_CERTS = "credentials/development/paa-root-certs"
4950

51+
TAG_PROCESS_MON = f"[{Fore.GREEN}MON {Style.RESET_ALL}]".encode()
5052
TAG_PROCESS_APP = f"[{Fore.GREEN}APP {Style.RESET_ALL}]".encode()
5153
TAG_PROCESS_TEST = f"[{Fore.GREEN}TEST{Style.RESET_ALL}]".encode()
5254
TAG_STDOUT = f"[{Fore.YELLOW}STDOUT{Style.RESET_ALL}]".encode()
@@ -70,6 +72,10 @@ def process_chip_output(line: bytes, is_stderr: bool, process_tag: bytes = b"")
7072
return f"[{timestamp}]".encode() + process_tag + (TAG_STDERR if is_stderr else TAG_STDOUT) + line
7173

7274

75+
def process_mon_output(line, is_stderr):
76+
return process_chip_output(line, is_stderr, TAG_PROCESS_MON)
77+
78+
7379
def process_chip_app_output(line, is_stderr):
7480
return process_chip_output(line, is_stderr, TAG_PROCESS_APP)
7581

@@ -150,6 +156,33 @@ def get_process(self):
150156
return self.app_process
151157

152158

159+
class IpPacketCaptureManager():
160+
def __init__(self, dump_filename: pathlib.Path):
161+
self.tcpdump_process = None
162+
self.dump_filename = dump_filename
163+
self.interface = 'any'
164+
self.keep_dumpfile = True
165+
166+
def start(self):
167+
# Create directory for dump files
168+
self.dump_filename.parent.mkdir(parents=True, exist_ok=True)
169+
170+
cmd = ['tcpdump', '-qn', '-i', self.interface, '-w', str(self.dump_filename), '-Z', getpass.getuser()]
171+
if os.getuid() != 0:
172+
cmd = ['sudo', '-n'] + cmd
173+
self.tcpdump_process = Subprocess(cmd[0], *cmd[1:], output_cb=process_mon_output)
174+
175+
self.tcpdump_process.start()
176+
177+
def stop(self):
178+
if self.tcpdump_process:
179+
self.tcpdump_process.terminate()
180+
self.tcpdump_process = None
181+
if not self.keep_dumpfile:
182+
log.info("Deleting capture file '%s'", self.dump_filename)
183+
self.dump_filename.unlink(missing_ok=True)
184+
185+
153186
@click.command()
154187
@click.option("--app", type=click.Path(exists=True), default=None,
155188
help='Path to local application to use, omit to use external apps.')
@@ -178,10 +211,14 @@ def get_process(self):
178211
help="Do not print output from passing tests. Use this flag in CI to keep GitHub log size manageable.")
179212
@click.option("--load-from-env", default=None, help="YAML file that contains values for environment variables.")
180213
@click.option("--run", type=str, multiple=True, help="Run only the specified test run(s).")
214+
@click.option("--ip-packet-capture/--no-ip-packet-capture", is_flag=True, default=False, help="Enable IP packet capture.")
215+
@click.option("--ip-packet-capture-dir", type=click.Path(file_okay=False, writable=True, path_type=pathlib.Path),
216+
default=pathlib.Path.cwd() / "out/ip_packet_captures", help="Storage for capture files.")
181217
@click.option("--app-filter", type=str, default=None, help="Run only for the specified app(s). Comma separated.")
182218
def main(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args: str,
183219
app_ready_pattern: str, app_stdin_pipe: str, script: str, script_args: str,
184-
script_gdb: bool, quiet: bool, load_from_env, run, app_filter):
220+
script_gdb: bool, quiet: bool, load_from_env, run, ip_packet_capture: bool, ip_packet_capture_dir: pathlib.Path,
221+
app_filter):
185222
if load_from_env:
186223
reader = MetadataReader(load_from_env)
187224
runs = reader.parse_script(script)
@@ -228,12 +265,14 @@ def main(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args:
228265
for run in runs:
229266
log.info("Executing '%s' '%s'", run.py_script_path.split('/')[-1], run.run)
230267
main_impl(run.app, run.factory_reset, run.factory_reset_app_only, run.app_args or "", run.app_ready_pattern,
231-
run.app_stdin_pipe, run.py_script_path, run.script_args or "", run.script_gdb, run.quiet)
268+
run.app_stdin_pipe, run.py_script_path, run.script_args or "", run.script_gdb, ip_packet_capture,
269+
ip_packet_capture_dir, run.quiet, run.run)
232270

233271

234272
def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args: str,
235273
app_ready_pattern: str, app_stdin_pipe: str, script: str, script_args: str,
236-
script_gdb: bool, quiet: bool):
274+
script_gdb: bool, ip_packet_capture: bool, ip_packet_capture_dir: pathlib.Path,
275+
quiet: bool, run_name: str):
237276

238277
app_args = app_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0])
239278
script_args = script_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0])
@@ -242,6 +281,14 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
242281
test_run_id = str(uuid.uuid4())[:8] # Use first 8 characters for shorter paths
243282
restart_flag_file = f"/tmp/chip_test_restart_app_{test_run_id}"
244283

284+
script_name = pathlib.Path(script).name.removesuffix('.py')
285+
tcpdump_capture_filename = ip_packet_capture_dir / f"tcpdump_{script_name}-{os.getpid()}-{run_name}.pcap"
286+
287+
tcpdump = IpPacketCaptureManager(pathlib.Path(tcpdump_capture_filename))
288+
289+
if ip_packet_capture:
290+
tcpdump.start()
291+
245292
# Remove app config and storage if factory reset is requested
246293
if factory_reset or factory_reset_app_only:
247294
reset_type = FactoryResetType.AppAndController if factory_reset else FactoryResetType.AppOnly
@@ -328,6 +375,10 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
328375
# We expect both app and test script should exit with 0
329376
exit_code = test_script_exit_code or app_exit_code
330377

378+
if tcpdump and exit_code == 0:
379+
# Delete packet captures from successful runs
380+
tcpdump.keep_dumpfile = False
381+
331382
if quiet:
332383
if exit_code:
333384
sys.stdout.write(stream_output.getvalue().decode('utf-8', errors='replace'))
@@ -346,6 +397,8 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
346397
log.info("Stopping app restart monitor thread")
347398
restart_monitor_thread.join(2.0)
348399

400+
tcpdump.stop()
401+
349402
# Clean up any leftover flag files if they exist - ensure this always executes
350403
log.info("Cleaning up flag files")
351404
if os.path.exists(restart_flag_file):

0 commit comments

Comments
 (0)