Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 288 additions & 0 deletions dockers/docker-restapi-sidecar/cli-plugin-tests/test_systemd_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,3 +580,291 @@ def mock_open(file, *args, **kwargs):
# Verify IS_V1_ENABLED is set correctly
assert ss.IS_V1_ENABLED == is_v1_enabled


def test_restapi_service_uses_per_branch_file(ss):
"""Test that restapi.service sync uses the per-branch service file"""
systemd_stub, container_fs, host_fs, commands, config_db = ss

# Prepare container unit content and host old content (branch 202311 from fixture)
container_restapi_service_path = "/usr/share/sonic/systemd_scripts/restapi.service_202311"
container_fs[container_restapi_service_path] = b"UNIT-NEW-PER-BRANCH"
container_fs["/usr/share/sonic/systemd_scripts/restapi.sh"] = b"DUMMY"
container_fs["/usr/share/sonic/systemd_scripts/container_checker_202311"] = b"DUMMY"
container_fs["/usr/share/sonic/scripts/k8s_pod_control.sh"] = b"DUMMY"

host_fs[systemd_stub.HOST_RESTAPI_SERVICE] = b"UNIT-OLD"
host_fs["/usr/bin/restapi.sh"] = b"DUMMY"
host_fs["/bin/container_checker"] = b"DUMMY"
host_fs["/usr/share/sonic/scripts/k8s_pod_control.sh"] = b"DUMMY"

# Add post actions for restapi.service
systemd_stub.POST_COPY_ACTIONS[systemd_stub.HOST_RESTAPI_SERVICE] = [
["sudo", "systemctl", "daemon-reload"],
["sudo", "systemctl", "restart", "restapi"],
]

ok = systemd_stub.ensure_sync()
assert ok is True
assert host_fs[systemd_stub.HOST_RESTAPI_SERVICE] == b"UNIT-NEW-PER-BRANCH"

# Verify systemctl actions were invoked
post_cmds = [args for _, args in commands if args and args[0] == "sudo"]
assert ("sudo", "systemctl", "daemon-reload") in post_cmds
assert ("sudo", "systemctl", "restart", "restapi") in post_cmds


# ─────────────────────────── Tests for _resolve_branch ───────────────────────────

@pytest.mark.parametrize("branch_input, expected", [
# Exact matches
("202311", "202311"),
("202405", "202405"),
("202411", "202411"),
("202505", "202505"),
("202511", "202511"),
# Between two supported → nearest lower
("202404", "202311"),
("202407", "202405"),
("202412", "202411"), # e.g. version 20241211.35
("202504", "202411"),
("202510", "202505"),
("202600", "202511"),
# Below minimum → falls back to 202311 (ERROR)
("202210", "202311"),
("202305", "202311"),
("202310", "202311"),
# master / internal / private → latest
("master", "202511"),
("internal", "202511"),
("private", "202511"),
# Non-numeric → falls back to 202311 (ERROR)
("foobar", "202311"),
])
def test_resolve_branch(ss, branch_input, expected):
systemd_stub, *_ = ss
assert systemd_stub._resolve_branch(branch_input) == expected


def test_resolve_branch_with_version_20241211(ss, monkeypatch, tmp_path):
"""End-to-end: SONiC.20241211.35 → branch 202412 → resolved to 202411."""
systemd_stub, *_ = ss

version_file = tmp_path / "sonic_version.yml"
version_file.write_text("build_version: 'SONiC.20241211.35'")

original_exists = os.path.exists
def mock_exists(path):
if path == "/etc/sonic/sonic_version.yml":
return True
return original_exists(path)
monkeypatch.setattr("os.path.exists", mock_exists)

original_open = open
def mock_open(file, *args, **kwargs):
if file == "/etc/sonic/sonic_version.yml":
return original_open(str(version_file), *args, **kwargs)
return original_open(file, *args, **kwargs)
monkeypatch.setattr("builtins.open", mock_open)

detected = systemd_stub._get_branch_name()
assert detected == "202412"
assert systemd_stub._resolve_branch(detected) == "202411"


def test_resolve_branch_with_version_20241211_kube(ss, monkeypatch, tmp_path):
"""End-to-end: 20241211.35-kube → branch 202412 → resolved to 202411."""
systemd_stub, *_ = ss

version_file = tmp_path / "sonic_version.yml"
version_file.write_text("build_version: '20241211.35-kube'")

original_exists = os.path.exists
def mock_exists(path):
if path == "/etc/sonic/sonic_version.yml":
return True
return original_exists(path)
monkeypatch.setattr("os.path.exists", mock_exists)

original_open = open
def mock_open(file, *args, **kwargs):
if file == "/etc/sonic/sonic_version.yml":
return original_open(str(version_file), *args, **kwargs)
return original_open(file, *args, **kwargs)
monkeypatch.setattr("builtins.open", mock_open)

detected = systemd_stub._get_branch_name()
assert detected == "202412"
assert systemd_stub._resolve_branch(detected) == "202411"


def test_resolve_branch_master_maps_to_latest(ss):
"""Test that master branch maps to latest supported branch (202511)."""
systemd_stub, *_ = ss
assert systemd_stub._resolve_branch("master") == "202511"


def test_resolve_branch_internal_maps_to_latest(ss):
"""Test that internal branch maps to latest supported branch (202511)."""
systemd_stub, *_ = ss
assert systemd_stub._resolve_branch("internal") == "202511"


def test_resolve_branch_private_maps_to_latest(ss):
"""Test that private branch maps to latest supported branch (202511)."""
systemd_stub, *_ = ss
assert systemd_stub._resolve_branch("private") == "202511"


def test_resolve_branch_below_minimum_falls_back(ss):
"""Test that branches below 202311 fall back to 202311."""
systemd_stub, *_ = ss
assert systemd_stub._resolve_branch("202210") == "202311"
assert systemd_stub._resolve_branch("202305") == "202311"
assert systemd_stub._resolve_branch("202310") == "202311"


def test_resolve_branch_between_supported_uses_nearest_lower(ss):
"""Test that branches between two supported versions use nearest lower."""
systemd_stub, *_ = ss
# Between 202311 and 202405
assert systemd_stub._resolve_branch("202404") == "202311"
# Between 202405 and 202411
assert systemd_stub._resolve_branch("202407") == "202405"
assert systemd_stub._resolve_branch("202410") == "202405"
# Between 202411 and 202505
assert systemd_stub._resolve_branch("202504") == "202411"
# Between 202505 and 202511
assert systemd_stub._resolve_branch("202510") == "202505"
# Beyond 202511
assert systemd_stub._resolve_branch("202600") == "202511"


def test_resolve_branch_supported_branches_constant(ss):
"""Test that SUPPORTED_BRANCHES is defined and contains expected values."""
systemd_stub, *_ = ss
assert hasattr(systemd_stub, "SUPPORTED_BRANCHES")
assert systemd_stub.SUPPORTED_BRANCHES == ["202311", "202405", "202411", "202505", "202511"]


def test_master_branch_uses_resolved_branch_for_sync(monkeypatch, tmp_path):
"""Test that master branch gets resolved to 202511 and uses proper sync files."""
if "systemd_stub" in sys.modules:
del sys.modules["systemd_stub"]

# Create fake sonic_version.yml for master
version_file = tmp_path / "sonic_version.yml"
version_file.write_text("build_version: 'SONiC.master.921927-18199d73f'\n")

monkeypatch.delenv("IS_V1_ENABLED", raising=False)

# Mock file operations
original_exists = os.path.exists
def mock_exists(p):
if p == "/etc/sonic/sonic_version.yml":
return True
return original_exists(p)
monkeypatch.setattr("os.path.exists", mock_exists)

original_open = open
def mock_open(file, *args, **kwargs):
if file == "/etc/sonic/sonic_version.yml":
return original_open(str(version_file), *args, **kwargs)
return original_open(file, *args, **kwargs)

monkeypatch.setattr("builtins.open", mock_open)

ss = importlib.import_module("systemd_stub")

# Verify branch detection and resolution
detected = ss._get_branch_name()
assert detected == "master"
resolved = ss._resolve_branch(detected)
assert resolved == "202511"


# ─────────────────────────── Tests for regex pattern optimization ───────────────────────────

def test_regex_patterns_compiled_at_module_level(ss):
"""Test that regex patterns are compiled at module level, not in functions."""
systemd_stub, *_ = ss

# Verify that the pre-compiled patterns exist
assert hasattr(systemd_stub, "_MASTER_PATTERN")
assert hasattr(systemd_stub, "_INTERNAL_PATTERN")
assert hasattr(systemd_stub, "_DATE_PATTERN")
assert hasattr(systemd_stub, "_DATE_EXTRACT_PATTERN")

# Verify they are compiled regex pattern objects
import re
assert isinstance(systemd_stub._MASTER_PATTERN, re.Pattern)
assert isinstance(systemd_stub._INTERNAL_PATTERN, re.Pattern)
assert isinstance(systemd_stub._DATE_PATTERN, re.Pattern)
assert isinstance(systemd_stub._DATE_EXTRACT_PATTERN, re.Pattern)


def test_master_pattern_matches_correctly(ss):
"""Test that _MASTER_PATTERN correctly matches master branch versions."""
systemd_stub, *_ = ss

# Should match
assert systemd_stub._MASTER_PATTERN.match("SONiC.master.921927-18199d73f")
assert systemd_stub._MASTER_PATTERN.match("master.921927-18199d73f")
assert systemd_stub._MASTER_PATTERN.match("SONIC.MASTER.123456-abcdef12") # case insensitive

# Should not match
assert not systemd_stub._MASTER_PATTERN.match("SONiC.internal.123456-abcdef12")
assert not systemd_stub._MASTER_PATTERN.match("SONiC.20231110.19")
assert not systemd_stub._MASTER_PATTERN.match("master") # missing numbers/hash


def test_internal_pattern_matches_correctly(ss):
"""Test that _INTERNAL_PATTERN correctly matches internal branch versions."""
systemd_stub, *_ = ss

# Should match
assert systemd_stub._INTERNAL_PATTERN.match("SONiC.internal.135691748-dbb8d29985")
assert systemd_stub._INTERNAL_PATTERN.match("internal.135691748-dbb8d29985")
assert systemd_stub._INTERNAL_PATTERN.match("SONIC.INTERNAL.123456789-abc1234567")

# Should not match
assert not systemd_stub._INTERNAL_PATTERN.match("SONiC.master.123456-abcdef12")
assert not systemd_stub._INTERNAL_PATTERN.match("SONiC.20231110.19")
assert not systemd_stub._INTERNAL_PATTERN.match("internal") # missing numbers/hash


def test_date_pattern_matches_correctly(ss):
"""Test that _DATE_PATTERN correctly matches date-based versions."""
systemd_stub, *_ = ss

# Should match
assert systemd_stub._DATE_PATTERN.match("SONiC.20231110.19")
assert systemd_stub._DATE_PATTERN.match("20240515.25")
assert systemd_stub._DATE_PATTERN.match("SONiC.20241110.kw.24")
assert systemd_stub._DATE_PATTERN.match("20250515")

# Should not match
assert not systemd_stub._DATE_PATTERN.match("SONiC.master.123456-abcdef12")
assert not systemd_stub._DATE_PATTERN.match("SONiC.internal.123456-abcdef12")
assert not systemd_stub._DATE_PATTERN.match("2023111") # only 7 digits


def test_date_extract_pattern_extracts_correctly(ss):
"""Test that _DATE_EXTRACT_PATTERN correctly extracts year and month."""
systemd_stub, *_ = ss

# Test various date formats
match = systemd_stub._DATE_EXTRACT_PATTERN.search("SONiC.20231110.19")
assert match
assert match.groups() == ("2023", "11")

match = systemd_stub._DATE_EXTRACT_PATTERN.search("20240515.25")
assert match
assert match.groups() == ("2024", "05")

match = systemd_stub._DATE_EXTRACT_PATTERN.search("SONiC.20241110.kw.24")
assert match
assert match.groups() == ("2024", "11")

# Should not match
match = systemd_stub._DATE_EXTRACT_PATTERN.search("SONiC.master.123456-abcdef12")
assert not match
59 changes: 48 additions & 11 deletions dockers/docker-restapi-sidecar/systemd_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@

logger.log_notice(f"IS_V1_ENABLED={IS_V1_ENABLED}")

# Compile regex patterns once at module level to avoid repeated compilation
_MASTER_PATTERN = re.compile(r'^(?:SONiC\.)?master\.\d+-[a-f0-9]+$', re.IGNORECASE)
_INTERNAL_PATTERN = re.compile(r'^(?:SONiC\.)?internal\.\d+-[a-f0-9]+$', re.IGNORECASE)
_DATE_PATTERN = re.compile(r'^(?:SONiC\.)?\d{8}\b', re.IGNORECASE)
_DATE_EXTRACT_PATTERN = re.compile(r'^(?:SONiC\.)?(\d{4})(\d{2})\d{2}\b', re.IGNORECASE)


def _get_branch_name() -> str:
"""
Expand Down Expand Up @@ -61,19 +67,18 @@ def _get_branch_name() -> str:
return "private"

# Pattern 1: Master - [SONiC.]master.XXXXXX-XXXXXXXX
master_pattern = re.compile(r'^(?:SONiC\.)?master\.\d+-[a-f0-9]+$', re.IGNORECASE)
if master_pattern.match(version):
if _MASTER_PATTERN.match(version):
logger.log_notice(f"Detected master branch from version: {version}")
return "master"

# Pattern 2: Internal - [SONiC.]internal.XXXXXXXXX-XXXXXXXXXX
elif re.match(r'^(?:SONiC\.)?internal\.\d+-[a-f0-9]+$', version, re.IGNORECASE):
elif _INTERNAL_PATTERN.match(version):
logger.log_notice(f"Detected internal branch from version: {version}")
return "internal"

# Pattern 3: Official feature branch - [SONiC.]YYYYMMDD.* (e.g., 20241110.kw.24)
elif re.match(r'^(?:SONiC\.)?\d{8}\b', version, re.IGNORECASE):
date_match = re.search(r'^(?:SONiC\.)?(\d{4})(\d{2})\d{2}\b', version, re.IGNORECASE)
elif _DATE_PATTERN.match(version):
date_match = _DATE_EXTRACT_PATTERN.search(version)
if date_match:
year, month = date_match.groups()
branch = f"{year}{month}"
Expand Down Expand Up @@ -106,14 +111,46 @@ def _get_branch_name() -> str:
}


SUPPORTED_BRANCHES = sorted(["202311", "202405", "202411", "202505", "202511"])


def _resolve_branch(branch_name: str) -> str:
"""Map detected branch to the nearest lower supported branch.

- Exact match in SUPPORTED_BRANCHES → use as-is.
- "master" / "internal" / "private" → latest supported branch (WARN).
- Numeric YYYYMM between two supported branches → highest supported <= it.
- Below 202311 → falls back to 202311 (ERROR).
"""
if branch_name in SUPPORTED_BRANCHES:
return branch_name

if branch_name in ("master", "internal", "private"):
resolved = SUPPORTED_BRANCHES[-1]
logger.log_warning(f"Branch '{branch_name}' mapped to latest supported: {resolved}")
return resolved

if not branch_name.isdigit():
logger.log_error(f"Cannot resolve non-numeric branch: {branch_name}, falling back to {SUPPORTED_BRANCHES[0]}")
return SUPPORTED_BRANCHES[0]

# String comparison is safe: all YYYYMM values are fixed 6-digit format
candidates = [b for b in SUPPORTED_BRANCHES if b <= branch_name]
if not candidates:
logger.log_error(f"Branch '{branch_name}' is below minimum supported, falling back to {SUPPORTED_BRANCHES[0]}")
return SUPPORTED_BRANCHES[0]

resolved = candidates[-1]
if resolved != branch_name:
logger.log_notice(f"Branch '{branch_name}' mapped to nearest lower supported: {resolved}")
return resolved


def ensure_sync() -> bool:
# Evaluate branch and source paths each time to allow retry on runtime failures
branch_name = _get_branch_name()

# Map to available branch-specific files: {202311,202405,202411,202505,202511}
if branch_name not in ["202311", "202405", "202411", "202505", "202511"]:
logger.log_error(f"Unsupported branch: {branch_name}. Only 202311, 202405, 202411, 202505, 202511 are supported.")
return False
detected_branch = _get_branch_name()

branch_name = _resolve_branch(detected_branch)

# restapi.sh: per-branch when IS_V1_ENABLED, otherwise use common restapi.sh
restapi_src = (
Expand Down
Loading