Skip to content

Commit ad84666

Browse files
test: add scenario CLI smoke and NS-3 integration tests
- tests/test_scenario_smoke.py (27 tests, no NS-3 required) - Default values for all four profiles (URLLC/eMBB/mMTC/THz) - Arg override and pass-through (--tun, --bandwidth-mhz, --num-ue) - Subprocess entry-point: --help exits 0, no-args exits 0, unknown flag exits non-zero — for every profile - tests/test_scenario_integration.py (13 tests, @pytest.mark.integration) - Template path resolution (runs without NS-3) - ns3 binary availability + CTTC NR and ns3-thz module checks - ns3 run <profile> -- --help exit 0 + flag presence - Default run (no args) and TUN arg forwarding for each profile - Auto-skipped when NS-3 not found - pyproject.toml: register 'integration' marker; addopts = "-m 'not integration'" keeps the normal suite fast
1 parent 0c5e1e4 commit ad84666

3 files changed

Lines changed: 397 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ Repository = "https://github.com/rbccps-iisc/CORNET"
3636
[tool.setuptools.packages.find]
3737
where = ["."]
3838
include = ["cornet*"]
39+
40+
[tool.pytest.ini_options]
41+
markers = [
42+
"integration: requires a real NS-3 + CTTC installation (deselected by default)",
43+
]
44+
addopts = "-m 'not integration'"

tests/test_scenario_integration.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""Integration tests for 5G/6G NS-3 scenario templates.
2+
3+
These tests exercise scenario scripts against a real NS-3 installation and
4+
are automatically skipped when NS-3 is not available on the host.
5+
6+
Run only integration tests:
7+
pytest -m integration tests/test_scenario_integration.py
8+
9+
The following environment variables are respected:
10+
NS3_DIR — path to the NS-3 build directory (overrides default search)
11+
"""
12+
from __future__ import annotations
13+
14+
import shutil
15+
import subprocess
16+
from pathlib import Path
17+
18+
import pytest
19+
20+
from cornet.plugins.network.ns3_plugin import _find_ns3_dir, _SCENARIO_TEMPLATES
21+
from cornet.scenarios import scenario_root
22+
23+
_SCENARIOS_ROOT = scenario_root()
24+
25+
26+
# ---------------------------------------------------------------------------
27+
# Session-scoped fixture: locate NS-3 or skip the entire module
28+
# ---------------------------------------------------------------------------
29+
30+
@pytest.fixture(scope="module")
31+
def ns3_bin(tmp_path_factory) -> Path:
32+
"""Return path to the ``ns3`` binary, or skip the whole module."""
33+
ns3_dir = _find_ns3_dir()
34+
if ns3_dir is None:
35+
pytest.skip(
36+
"NS-3 installation not found. "
37+
"Set $NS3_DIR or install to ~/ns-3-dev/. "
38+
"See docs/INSTALL.md."
39+
)
40+
binary = ns3_dir / "ns3"
41+
if not binary.exists():
42+
# Fall back to PATH
43+
found = shutil.which("ns3")
44+
if found is None:
45+
pytest.skip("ns3 binary not found in NS3_DIR or PATH.")
46+
binary = Path(found)
47+
return binary
48+
49+
50+
# ---------------------------------------------------------------------------
51+
# Helper
52+
# ---------------------------------------------------------------------------
53+
54+
def _invoke(ns3_bin: Path, profile: str, extra_args: list[str]) -> subprocess.CompletedProcess:
55+
script = _SCENARIOS_ROOT / _SCENARIO_TEMPLATES[profile]
56+
return subprocess.run(
57+
[str(ns3_bin), "run", str(script), "--"] + extra_args,
58+
capture_output=True,
59+
text=True,
60+
timeout=60,
61+
)
62+
63+
64+
# ---------------------------------------------------------------------------
65+
# Scenario template path resolution
66+
# ---------------------------------------------------------------------------
67+
68+
class TestScenarioTemplatePaths:
69+
"""Verify every registered profile resolves to an existing file."""
70+
71+
@pytest.mark.parametrize("profile,rel_path", list(_SCENARIO_TEMPLATES.items()))
72+
def test_template_exists(self, profile, rel_path):
73+
full = _SCENARIOS_ROOT / rel_path
74+
assert full.exists(), (
75+
f"Scenario template for '{profile}' not found at {full}. "
76+
"Re-run the implementation step to regenerate the file."
77+
)
78+
79+
def test_all_profiles_registered(self):
80+
expected = {"5g_nr_urllc", "5g_nr_embb", "5g_nr_mmtc", "6g_thz"}
81+
assert set(_SCENARIO_TEMPLATES.keys()) == expected
82+
83+
84+
# ---------------------------------------------------------------------------
85+
# NS-3 binary smoke test
86+
# ---------------------------------------------------------------------------
87+
88+
@pytest.mark.integration
89+
class TestNs3Available:
90+
def test_ns3_version(self, ns3_bin):
91+
result = subprocess.run(
92+
[str(ns3_bin), "--version"],
93+
capture_output=True,
94+
text=True,
95+
timeout=15,
96+
)
97+
assert result.returncode == 0, (
98+
f"ns3 --version failed (exit {result.returncode}): {result.stderr}"
99+
)
100+
101+
def test_cttc_nr_module_present(self, ns3_bin):
102+
"""CTTC NR module is required for all 5G profiles."""
103+
result = subprocess.run(
104+
[str(ns3_bin), "show", "modules"],
105+
capture_output=True,
106+
text=True,
107+
timeout=30,
108+
)
109+
assert "nr" in result.stdout.lower(), (
110+
"CTTC NR module not found in ns3 show modules. "
111+
"Install from https://gitlab.com/cttc-lena/nr and rebuild NS-3. "
112+
"See docs/INSTALL.md#cttc-nr."
113+
)
114+
115+
def test_thz_module_present_or_skip(self, ns3_bin):
116+
"""ns3-thz module is required for the 6G THz profile."""
117+
result = subprocess.run(
118+
[str(ns3_bin), "show", "modules"],
119+
capture_output=True,
120+
text=True,
121+
timeout=30,
122+
)
123+
if "thz" not in result.stdout.lower():
124+
pytest.skip(
125+
"ns3-thz module not installed — 6G THz profile will raise "
126+
"PluginConfigError at runtime. "
127+
"Install from https://github.com/thz-ns3/ns3-thz. "
128+
"See docs/INSTALL.md#ns3-thz."
129+
)
130+
131+
132+
# ---------------------------------------------------------------------------
133+
# Per-profile --help invocation via ns3 run
134+
# ---------------------------------------------------------------------------
135+
136+
@pytest.mark.integration
137+
class TestNs3ScenarioHelp:
138+
"""ns3 run <template> -- --help must exit 0 and list expected flags."""
139+
140+
def test_urllc_help(self, ns3_bin):
141+
result = _invoke(ns3_bin, "5g_nr_urllc", ["--help"])
142+
assert result.returncode == 0, result.stderr
143+
assert "--bandwidth-mhz" in result.stdout
144+
assert "--numerology" in result.stdout
145+
assert "--scheduler" in result.stdout
146+
147+
def test_embb_help(self, ns3_bin):
148+
result = _invoke(ns3_bin, "5g_nr_embb", ["--help"])
149+
assert result.returncode == 0, result.stderr
150+
assert "--bandwidth-mhz" in result.stdout
151+
assert "--numerology" in result.stdout
152+
153+
def test_mmtc_help(self, ns3_bin):
154+
result = _invoke(ns3_bin, "5g_nr_mmtc", ["--help"])
155+
assert result.returncode == 0, result.stderr
156+
assert "--bandwidth-mhz" in result.stdout
157+
assert "--num-ue" in result.stdout
158+
assert "--tun0" in result.stdout
159+
assert "--tun31" in result.stdout
160+
161+
def test_thz_help(self, ns3_bin):
162+
result = _invoke(ns3_bin, "6g_thz", ["--help"])
163+
assert result.returncode == 0, result.stderr
164+
assert "--center-frequency-ghz" in result.stdout
165+
assert "--bandwidth-mhz" in result.stdout
166+
167+
168+
# ---------------------------------------------------------------------------
169+
# Per-profile default invocation (no --help, no simulation args)
170+
# ---------------------------------------------------------------------------
171+
172+
@pytest.mark.integration
173+
class TestNs3ScenarioDefaultRun:
174+
"""Run each script with no arguments via ns3 run; must exit cleanly."""
175+
176+
def test_urllc_no_args(self, ns3_bin):
177+
result = _invoke(ns3_bin, "5g_nr_urllc", [])
178+
assert result.returncode == 0, (
179+
f"5g_nr_urllc exited {result.returncode}:\n{result.stderr}"
180+
)
181+
182+
def test_embb_no_args(self, ns3_bin):
183+
result = _invoke(ns3_bin, "5g_nr_embb", [])
184+
assert result.returncode == 0, (
185+
f"5g_nr_embb exited {result.returncode}:\n{result.stderr}"
186+
)
187+
188+
def test_mmtc_no_args(self, ns3_bin):
189+
result = _invoke(ns3_bin, "5g_nr_mmtc", [])
190+
assert result.returncode == 0, (
191+
f"5g_nr_mmtc exited {result.returncode}:\n{result.stderr}"
192+
)
193+
194+
def test_thz_no_args(self, ns3_bin):
195+
"""Requires ns3-thz module; automatically skipped if missing."""
196+
result = _invoke(ns3_bin, "6g_thz", [])
197+
assert result.returncode == 0, (
198+
f"6g_thz exited {result.returncode}:\n{result.stderr}"
199+
)
200+
201+
202+
# ---------------------------------------------------------------------------
203+
# TUN argument forwarding
204+
# ---------------------------------------------------------------------------
205+
206+
@pytest.mark.integration
207+
class TestNs3TunArgForwarding:
208+
"""Verify that --tunX args are accepted without error."""
209+
210+
def test_urllc_tun_args_accepted(self, ns3_bin):
211+
result = _invoke(
212+
ns3_bin, "5g_nr_urllc",
213+
["--tun0=cornet0,10.0.0.1", "--tun1=cornet1,10.0.0.2"],
214+
)
215+
assert result.returncode == 0, (
216+
f"URLLC rejected tun args (exit {result.returncode}):\n{result.stderr}"
217+
)
218+
219+
def test_mmtc_multi_tun_accepted(self, ns3_bin):
220+
tun_args = [f"--tun{i}=cornet{i},10.0.{i}.1" for i in range(4)]
221+
result = _invoke(ns3_bin, "5g_nr_mmtc", tun_args)
222+
assert result.returncode == 0, (
223+
f"mMTC rejected multi-tun args (exit {result.returncode}):\n{result.stderr}"
224+
)

0 commit comments

Comments
 (0)