diff --git a/.github/scripts/setup_environment.sh b/.github/scripts/setup_environment.sh index 1b3321f38..8f585e2cb 100755 --- a/.github/scripts/setup_environment.sh +++ b/.github/scripts/setup_environment.sh @@ -75,7 +75,10 @@ function setup_ubuntu_install_dependencies() { clang \ libsdl2-dev \ libsdl2-ttf-dev \ - cmake + cmake \ + linuxptp \ + ethtool \ + netsniff-ng # CiCd only if [ "${CICD_BUILD}" == "1" ]; then diff --git a/.github/workflows/custom-pytest.yml b/.github/workflows/custom-pytest.yml index 5a38e5ea9..e79bfd171 100644 --- a/.github/workflows/custom-pytest.yml +++ b/.github/workflows/custom-pytest.yml @@ -64,11 +64,20 @@ jobs: --pci_device "${{ env.PCI_DEVICE }}" \ --ip_address 127.0.0.1 \ --username "${{ secrets.RUNNER_USERNAME }}" \ - --key_path "${{ secrets.RUNNER_KEY_PATH }}" + --key_path ${{ secrets.RUNNER_KEY_PATH }} \ + --ebu_ip ${{ secrets.RUNNER_EBU_LIST_IP }} \ + --ebu_user ${{ secrets.RUNNER_EBU_LIST_USER }} \ + --ebu_password ${{ secrets.RUNNER_EBU_LIST_PASSWORD }} - name: Kill active MtlManager and pytest processes run: | - sudo killall -SIGINT MtlManager || true + sudo killall -SIGINT pipenv || true sudo killall -SIGINT pytest || true + sudo killall -SIGINT MtlManager || true + sudo killall -SIGINT phc2sys || true + - name: Export workflow tag for PCAP naming + shell: bash + run: | + echo "MTL_GITHUB_WORKFLOW=${{ github.workflow }}:${{ inputs.nic }}" >> "$GITHUB_ENV" - name: Start MtlManager run: | sudo MtlManager & diff --git a/.github/workflows/nightly-pytest.yml b/.github/workflows/nightly-pytest.yml index 1610a5dec..752e2edc3 100644 --- a/.github/workflows/nightly-pytest.yml +++ b/.github/workflows/nightly-pytest.yml @@ -73,12 +73,20 @@ jobs: --pci_device ${{ env.PCI_DEVICE }} \ --ip_address 127.0.0.1 \ --username ${{ secrets.RUNNER_USERNAME }} \ - --key_path ${{ secrets.RUNNER_KEY_PATH }} + --key_path ${{ secrets.RUNNER_KEY_PATH }} \ + --ebu_ip ${{ secrets.RUNNER_EBU_LIST_IP }} \ + --ebu_user ${{ secrets.RUNNER_EBU_LIST_USER }} \ + --ebu_password ${{ secrets.RUNNER_EBU_LIST_PASSWORD }} - name: 'preparation: Kill MtlManager and pytest routines' run: | sudo killall -SIGINT pipenv || true sudo killall -SIGINT pytest || true sudo killall -SIGINT MtlManager || true + sudo killall -SIGINT phc2sys || true + - name: Export workflow tag for PCAP naming + shell: bash + run: | + echo "MTL_GITHUB_WORKFLOW=${{ github.workflow }}:${{ matrix.nic }}" >> "$GITHUB_ENV" - name: 'preparation: Start MtlManager at background' run: | sudo MtlManager & diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index a01c2567b..1df3682fc 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -67,11 +67,11 @@ jobs: if: ${{ needs.smoke-check-for-changes.outputs.changed == 'true' }} run: | if [ "${{ matrix.nic }}" = "e810" ]; then - echo "PCI_DEVICE=8086:1592" >> "$GITHUB_ENV" + echo "PCI_DEVICE=8086:1592,8086:1592" >> "$GITHUB_ENV" elif [ "${{ matrix.nic }}" = "e810-dell" ]; then - echo "PCI_DEVICE=8086:1592" >> "$GITHUB_ENV" + echo "PCI_DEVICE=8086:1592,8086:1592" >> "$GITHUB_ENV" elif [ "${{ matrix.nic }}" = "e830" ]; then - echo "PCI_DEVICE=8086:12d2" >> "$GITHUB_ENV" + echo "PCI_DEVICE=8086:12d2,8086:12d2" >> "$GITHUB_ENV" fi - name: Generate test framework config files if: ${{ needs.smoke-check-for-changes.outputs.changed == 'true' }} @@ -84,13 +84,22 @@ jobs: --pci_device ${{ env.PCI_DEVICE }} \ --ip_address 127.0.0.1 \ --username ${{ secrets.RUNNER_USERNAME }} \ - --key_path ${{ secrets.RUNNER_KEY_PATH }} + --key_path ${{ secrets.RUNNER_KEY_PATH }} \ + --ebu_ip ${{ secrets.RUNNER_EBU_LIST_IP }} \ + --ebu_user ${{ secrets.RUNNER_EBU_LIST_USER }} \ + --ebu_password ${{ secrets.RUNNER_EBU_LIST_PASSWORD }} - name: 'preparation: Kill MtlManager and pytest routines' if: ${{ needs.smoke-check-for-changes.outputs.changed == 'true' }} run: | sudo killall -SIGINT pipenv || true sudo killall -SIGINT pytest || true sudo killall -SIGINT MtlManager || true + sudo killall -SIGINT phc2sys || true + - name: Export workflow tag for PCAP naming + if: ${{ needs.smoke-check-for-changes.outputs.changed == 'true' }} + shell: bash + run: | + echo "MTL_GITHUB_WORKFLOW=${{ github.workflow }}:${{ matrix.nic }}" >> "$GITHUB_ENV" - name: 'preparation: Start MtlManager at background' if: ${{ needs.smoke-check-for-changes.outputs.changed == 'true' }} run: | diff --git a/.gitignore b/.gitignore index e6f18f244..c8e6ae1e0 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ dkms.conf # Ours .* +!.github/ +!.github/** *.log *.log_* logs_* diff --git a/tests/validation/common/integrity/audio_integrity.py b/tests/validation/common/integrity/audio_integrity.py index f93b2b40c..099b76fef 100644 --- a/tests/validation/common/integrity/audio_integrity.py +++ b/tests/validation/common/integrity/audio_integrity.py @@ -46,17 +46,34 @@ def check_integrity_file(self, out_url) -> bool: ) src_chunk_sums = self.src_chunk_sums out_chunk_sums = calculate_chunk_hashes(out_url, self.frame_size) + + src_frames = len(src_chunk_sums) + out_frames = len(out_chunk_sums) + + # In some pipelines the input is looped and transmitted multiple times. + # In that case the output may contain multiple repetitions of the source. + # We only validate the first `src_frames` frames and ignore any trailing frames. + if out_frames > src_frames: + self.logger.info( + f"Output contains {out_frames} frames; source contains {src_frames} frames. " + f"Ignoring {out_frames - src_frames} trailing frames (looped output)." + ) + + frames_to_check = min(src_frames, out_frames) bad_frames = 0 - for idx, chunk_sum in enumerate(out_chunk_sums): - if idx >= len(src_chunk_sums) or chunk_sum != src_chunk_sums[idx]: + for idx in range(frames_to_check): + if out_chunk_sums[idx] != src_chunk_sums[idx]: self.logger.error(f"Bad audio frame at index {idx} in {out_url}") bad_frames += 1 if bad_frames: self.logger.error( - f"Received {bad_frames} bad frames out of {len(out_chunk_sums)} checked." + f"Received {bad_frames} bad frames out of {frames_to_check} checked." ) return False - self.logger.info(f"All {len(out_chunk_sums)} frames in {out_url} are correct.") + + self.logger.info( + f"All {frames_to_check} checked frames in {out_url} are correct." + ) return True diff --git a/tests/validation/configs/README.md b/tests/validation/configs/README.md index 9e4eb0e7b..252d1528d 100644 --- a/tests/validation/configs/README.md +++ b/tests/validation/configs/README.md @@ -63,6 +63,21 @@ ramdisk: - **pcap_dir**: Directory to store capture files - **capture_time**: Duration of packet capture in seconds - **interface**: Network interface to capture from + +### Packet Capture (netsniff-ng) + +When `capture_cfg.enable: true`, the framework can capture traffic with `netsniff-ng`. + +By default it captures on the **second PF of the same NIC** (same `pci_device`) if available, otherwise it uses `host.network_interfaces[0]`. + +To capture on a specific NIC, set one of: + +- `capture_cfg.sniff_interface`: OS interface name (e.g., `enp24s0f0`) +- `capture_cfg.sniff_interface_index`: index into `host.network_interfaces` +- `capture_cfg.sniff_pci_device`: vendor:device ID (e.g., `8086:12d2`) + +Priority is `sniff_interface` > `sniff_interface_index` > `sniff_pci_device`. + - **ramdisk**: RAM disk configuration for high-performance testing - **media.mountpoint**: Mount point for media RAM disk - **media.size_gib**: Size of media RAM disk in GiB diff --git a/tests/validation/configs/examples/test_config.yaml b/tests/validation/configs/examples/test_config.yaml index aecd31fb6..d8022ac56 100644 --- a/tests/validation/configs/examples/test_config.yaml +++ b/tests/validation/configs/examples/test_config.yaml @@ -13,6 +13,9 @@ compliance: false capture_cfg: enable: false pcap_dir: /mnt/ramdisk/pcap + # Optional: pick a different NIC for netsniff-ng capture than the one used by the test. + # Priority: sniff_interface > sniff_interface_index > sniff_pci_device. + # sniff_interface_index: 1 ebu_server: ebu_ip: ebu_ip user: user diff --git a/tests/validation/configs/examples/topology_config.yaml b/tests/validation/configs/examples/topology_config.yaml index c0de04c9e..ab49eb600 100644 --- a/tests/validation/configs/examples/topology_config.yaml +++ b/tests/validation/configs/examples/topology_config.yaml @@ -6,8 +6,12 @@ hosts: instantiate: true role: sut network_interfaces: + # Test NIC(s) - pci_device: 8086:1592 interface_index: 0 + # Optional: separate NIC for packet capture (netsniff-ng) + - pci_device: 8086:1592 + interface_index: 1 connections: - ip_address: 127.0.0.1 connection_type: SSHConnection diff --git a/tests/validation/configs/gen_config.py b/tests/validation/configs/gen_config.py index 93d7a42cd..38cd0f1e0 100644 --- a/tests/validation/configs/gen_config.py +++ b/tests/validation/configs/gen_config.py @@ -3,7 +3,24 @@ import yaml -def gen_test_config(session_id: int, build: str, mtl_path: str) -> str: +def gen_test_config( + session_id: int, + build: str, + mtl_path: str, + pci_device: str, + ebu_ip: str, + ebu_user: str, + ebu_password: str, +) -> str: + # Support comma-separated PCI devices for multiple interfaces. + # The capture sniff interface must be explicitly provided as the second device. + pci_devices = [dev.strip() for dev in pci_device.split(",") if dev.strip()] + if len(pci_devices) < 2: + raise ValueError( + "At least two PCI devices are required (e.g., '0000:4b:00.0,0000:4b:00.1'); " + "the second device is used as sniff_pci_device" + ) + test_config = { "session_id": session_id, "build": build, @@ -11,14 +28,19 @@ def gen_test_config(session_id: int, build: str, mtl_path: str) -> str: "media_path": "/mnt/media", "ramdisk": { "media": {"mountpoint": "/mnt/ramdisk/media", "size_gib": 32}, - "pcap": {"mountpoint": "/mnt/ramdisk/pcap", "size_gib": 768}, + "tmpfs_size_gib": 12, + "pcap_dir": "/mnt/ramdisk/pcap", + }, + "compliance": True, + "capture_cfg": { + "enable": True, + "pcap_dir": "/mnt/ramdisk/pcap", + "sniff_pci_device": pci_devices[1], }, - "compliance": False, - "capture_cfg": {"enable": False, "pcap_dir": "/mnt/ramdisk/pcap"}, "ebu_server": { - "ebu_ip": "ebu_ip", - "user": "user", - "password": "password", + "ebu_ip": ebu_ip, + "user": ebu_user, + "password": ebu_password, "proxy": False, }, } @@ -95,8 +117,26 @@ def main() -> None: "--pci_device", type=str, required=True, - help="specify PCI ID of the NIC (comma-separated for multiple interfaces, e.g., \ - '8086:1592')", + help="specify PCI BDF(s) of the NIC (comma-separated for multiple interfaces, e.g., \ + '0000:4b:00.0,0000:4b:00.1'); the second device is used for capture sniffing", + ) + parser.add_argument( + "--ebu_ip", + type=str, + required=True, + help="EBU LIST server IP/hostname (RUNNER_EBU_LIST_IP)", + ) + parser.add_argument( + "--ebu_user", + type=str, + required=True, + help="EBU LIST username (RUNNER_EBU_LIST_USER)", + ) + parser.add_argument( + "--ebu_password", + type=str, + required=True, + help="EBU LIST password (RUNNER_EBU_LIST_PASSWORD)", ) parser.add_argument( "--ip_address", @@ -125,12 +165,22 @@ def main() -> None: args = parser.parse_args() if args.password == "None" and args.key_path == "None": parser.error("one of the arguments --password --key_path is required") - with open("test_config.yaml", "w") as file: - file.write( - gen_test_config( - session_id=args.session_id, build=args.build, mtl_path=args.mtl_path - ) + + try: + test_config_yaml = gen_test_config( + session_id=args.session_id, + build=args.build, + mtl_path=args.mtl_path, + pci_device=args.pci_device, + ebu_ip=args.ebu_ip, + ebu_user=args.ebu_user, + ebu_password=args.ebu_password, ) + except ValueError as exc: + parser.error(str(exc)) + + with open("test_config.yaml", "w") as file: + file.write(test_config_yaml) with open("topology_config.yaml", "w") as file: file.write( gen_topology_config( diff --git a/tests/validation/conftest.py b/tests/validation/conftest.py index 5775fd77e..0345da93a 100755 --- a/tests/validation/conftest.py +++ b/tests/validation/conftest.py @@ -6,6 +6,7 @@ import logging import os import shutil +import signal import time from typing import Any, Dict @@ -42,6 +43,137 @@ phase_report_key = pytest.StashKey[Dict[str, pytest.CollectReport]]() +def _select_sniff_interface(host, capture_cfg: dict): + def _pci_device_id(nic) -> str: + """Return lowercased PCI vendor:device identifier (e.g., '8086:1592').""" + return f"{nic.pci_device.vendor_id}:{nic.pci_device.device_id}".lower() + + sniff_interface = capture_cfg.get("sniff_interface") + if sniff_interface: + for nic in host.network_interfaces: + if nic.name == str(sniff_interface): + return nic + available = [ + f"{nic.name} ({nic.pci_address.lspci})" for nic in host.network_interfaces + ] + raise RuntimeError( + f"capture_cfg.sniff_interface={sniff_interface} not found on host {host.name}. " + f"Available interfaces: {', '.join(available)}" + ) + + sniff_interface_index = capture_cfg.get("sniff_interface_index") + if sniff_interface_index is not None: + return host.network_interfaces[int(sniff_interface_index)] + + sniff_pci_device = capture_cfg.get("sniff_pci_device") + if sniff_pci_device: + target = str(sniff_pci_device).lower() + + direct_matches = [ + nic for nic in host.network_interfaces if target == _pci_device_id(nic) + ] + if direct_matches: + return direct_matches[1] if len(direct_matches) > 1 else direct_matches[0] + + available = [ + f"{nic.name} ({nic.pci_address.lspci})" for nic in host.network_interfaces + ] + raise RuntimeError( + f"capture_cfg.sniff_pci_device={sniff_pci_device} not found on host {host.name}. " + f"Available interfaces: {', '.join(available)}" + ) + + # Default behavior: capture on 2nd PF. + if len(host.network_interfaces) < 2: + raise RuntimeError( + f"Host {host.name} has less than 2 network interfaces; " + f"Cannot select 2nd PF for capture. Add more interfaces to config or turn off capture." + ) + + return host.network_interfaces[1] + + +def _select_sniff_interface_name(host, capture_cfg: dict) -> str: + return _select_sniff_interface(host, capture_cfg).name + + +def _select_capture_host(hosts: dict): + return hosts["client"] if "client" in hosts else list(hosts.values())[0] + + +@pytest.fixture(scope="session") +def phc2sys_session(test_config: dict, hosts): + """Start phc2sys for the capture interface before any tests. + + - Uses the same interface selection logic as PCAP capture. + - Detects the PTP Hardware Clock via `ethtool -T`. + - Runs `phc2sys -s /dev/ptpX -c CLOCK_REALTIME -O 0 -m`. + + The process is stopped at the end of the session. + """ + + capture_cfg = test_config.get("capture_cfg", {}) + if not (capture_cfg and capture_cfg.get("enable")): + yield + return + + host = _select_capture_host(hosts) + sniff_nic = _select_sniff_interface(host, capture_cfg) + capture_iface = sniff_nic.name + + ptp_details = host.connection.execute_command( + f"sudo ethtool -T '{capture_iface}' 2>/dev/null || true" + ) + ptp_idx = "" + for line in (ptp_details.stdout or "").splitlines(): + # Keep this equivalent to: awk -F': ' '/PTP Hardware Clock:/ {print $2; exit}' + if "PTP Hardware Clock:" in line: + ptp_idx = line.split(": ", 1)[1].strip() if ": " in line else "" + break + + if not ptp_idx.isdigit(): + raise RuntimeError( + "ERROR: failed to parse PTP Hardware Clock index for " + f"{capture_iface}. Details: {ptp_details.stdout}{ptp_details.stderr}" + ) + + capture_ptp = f"/dev/ptp{ptp_idx}" + + logger.info( + f"Starting phc2sys: {capture_ptp} -> CLOCK_REALTIME (iface={capture_iface})" + ) + + log_path = f"/tmp/phc2sys-{capture_iface}.log" + phc2sys_cmd = "sudo phc2sys " f"-s '{capture_ptp}' -c CLOCK_REALTIME -O 0 -m" + phc2sys_process = host.connection.start_process( + phc2sys_cmd, + stderr_to_stdout=True, + output_file=log_path, + ) + + # Give phc2sys a moment to fail fast (e.g., missing /dev/ptpX permissions). + time.sleep(0.3) + if not phc2sys_process.running: + raise RuntimeError( + f"Failed to start phc2sys (iface={capture_iface}, ptp={capture_ptp}). " + f"log={log_path}" + ) + + try: + yield + finally: + if not phc2sys_process: + return + + if not phc2sys_process.running: + raise RuntimeError( + f"phc2sys process (iface={capture_iface}, ptp={capture_ptp}) " + f"stopped unexpectedly. See log: {log_path}" + ) + + phc2sys_process.kill(wait=None, with_signal=signal.SIGINT) + + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object @@ -293,7 +425,7 @@ def log_session(): @pytest.fixture(scope="function") -def pcap_capture(request, media_file, test_config, hosts, mtl_path): +def pcap_capture(request, media_file, test_config, hosts, mtl_path, phc2sys_session): capture_cfg = test_config.get("capture_cfg", {}) capturer = None if capture_cfg and capture_cfg.get("enable"): @@ -318,7 +450,7 @@ def pcap_capture(request, media_file, test_config, hosts, mtl_path): host=host, test_name=test_name, pcap_dir=capture_cfg.get("pcap_dir", "/tmp"), - interface=host.network_interfaces[0].name, + interface=_select_sniff_interface_name(host, capture_cfg), silent=capture_cfg.get("silent", True), packets_capture=capture_cfg.get("packets_number", None), capture_time=capture_cfg.get("capture_time", None), @@ -339,7 +471,7 @@ def pcap_capture(request, media_file, test_config, hosts, mtl_path): f" --ip {ebu_ip}" f" --user {ebu_login}" f" --password {ebu_passwd}" - f" --pcap {capturer.pcap_file}{proxy_cmd}", + f" --pcap '{capturer.pcap_file}'{proxy_cmd}", cwd=f"{str(mtl_path)}", ) if compliance_upl.return_code != 0: diff --git a/tests/validation/create_pcap_file/netsniff.py b/tests/validation/create_pcap_file/netsniff.py index 80ddb22d0..975f95b1d 100644 --- a/tests/validation/create_pcap_file/netsniff.py +++ b/tests/validation/create_pcap_file/netsniff.py @@ -1,7 +1,9 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright 2025 Intel Corporation +import datetime import logging import os +import re from time import sleep from mfd_connect.exceptions import ( @@ -59,7 +61,7 @@ def __init__( self.host = host self.test_name = test_name self.pcap_dir = pcap_dir - self.pcap_file = f"'{os.path.join(pcap_dir, test_name)}.pcap'" + self.pcap_file = None if interface is not None: self.interface = interface else: @@ -70,6 +72,41 @@ def __init__( self.packets_capture = packets_capture self.capture_time = capture_time + @staticmethod + def _sanitize_filename_component(value: str, *, max_len: int = 64) -> str: + cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", (value or "").strip()) + cleaned = cleaned.strip("-._") + if not cleaned: + cleaned = "unknown" + return cleaned[:max_len] + + def _get_remote_hostname(self) -> str: + try: + res = self.host.connection.execute_command("uname -n") + hostname = (res.stdout or "").strip().splitlines()[0] + if hostname: + return hostname + except Exception: + pass + return str(getattr(self.host, "name", "unknown")) + + def _build_pcap_path(self) -> str: + hostname = self._sanitize_filename_component(self._get_remote_hostname()) + timestamp = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y%m%dT%H%M%SZ" + ) + job = ( + os.environ.get("MTL_GITHUB_WORKFLOW") or os.environ.get("GITHUB_JOB") or "" + ) + job = self._sanitize_filename_component(job, max_len=96) if job else "" + + test = self._sanitize_filename_component(self.test_name, max_len=128) + parts = [test, hostname, timestamp] + if job: + parts.append(job) + filename = "__".join(parts) + ".pcap" + return os.path.join(self.pcap_dir, filename) + def start(self): """ Starts the netsniff-ng @@ -77,13 +114,14 @@ def start(self): if not self.netsniff_process or not self.netsniff_process.running: connection = self.host.connection try: + self.pcap_file = self._build_pcap_path() cmd = [ "netsniff-ng", "--silent" if self.silent else "", "--in", str(self.interface), "--out", - self.pcap_file, + f"'{self.pcap_file}'", ( f"--num {self.packets_capture}" if self.packets_capture is not None @@ -95,9 +133,7 @@ def start(self): self.netsniff_process = connection.start_process( " ".join(cmd), stderr_to_stdout=True ) - logger.info( - f"PCAP file will be saved at: {os.path.abspath(self.pcap_file)}" - ) + logger.info(f"PCAP file will be saved at: {self.pcap_file}") if not self.netsniff_process.running: err = self.netsniff_process.stdout_text diff --git a/tests/validation/tests/single/st20p/resolutions/test_resolutions.py b/tests/validation/tests/single/st20p/resolutions/test_resolutions.py index 0d2f02a24..78a5c1949 100755 --- a/tests/validation/tests/single/st20p/resolutions/test_resolutions.py +++ b/tests/validation/tests/single/st20p/resolutions/test_resolutions.py @@ -12,9 +12,19 @@ @pytest.mark.nightly @pytest.mark.parametrize( "media_file", - list(yuv_files_422rfc10.values()), + [ + pytest.param(yuv_files_422rfc10["Penguin_1080p"], marks=pytest.mark.smoke), + *[ + yuv_files_422rfc10[k] + for k in yuv_files_422rfc10.keys() + if k != "Penguin_1080p" + ], + ], indirect=["media_file"], - ids=list(yuv_files_422rfc10.keys()), + ids=[ + "Penguin_1080p", + *[k for k in yuv_files_422rfc10.keys() if k != "Penguin_1080p"], + ], ) def test_resolutions( hosts, diff --git a/tests/validation/tests/single/st30p/st30p_channel/test_st30p_channel.py b/tests/validation/tests/single/st30p/st30p_channel/test_st30p_channel.py index 8b9db082f..8b068ac68 100755 --- a/tests/validation/tests/single/st30p/st30p_channel/test_st30p_channel.py +++ b/tests/validation/tests/single/st30p/st30p_channel/test_st30p_channel.py @@ -13,23 +13,25 @@ logger = logging.getLogger(__name__) +_AUDIO_FORMATS = ["PCM8", "PCM16", "PCM24"] +_AUDIO_CHANNELS = ["M", "DM", "ST", "LtRt", "51", "71", "222", "SGRP"] +_SMOKE_CASE = ("PCM16", "M") + + @pytest.mark.nightly @pytest.mark.parametrize( - "media_file", + ("media_file", "audio_channel"), [ - audio_files["PCM8"], - audio_files["PCM16"], - audio_files["PCM24"], + pytest.param( + audio_files[fmt], + ch, + marks=[pytest.mark.smoke] if (fmt, ch) == _SMOKE_CASE else [], + id=f"{fmt}-{ch}", + ) + for fmt in _AUDIO_FORMATS + for ch in _AUDIO_CHANNELS ], indirect=["media_file"], - ids=[ - "PCM8", - "PCM16", - "PCM24", - ], -) -@pytest.mark.parametrize( - "audio_channel", ["M", "DM", "ST", "LtRt", "51", "71", "222", "SGRP"] ) def test_st30p_channel( hosts,