#!/usr/bin/env python3
"""Deterministic trigger behavior using temporary forms and fake P4/RADIUS APIs."""
import contextlib
import io
import os
from pathlib import Path
import runpy
import subprocess
import sys
import tempfile
import types
import unittest
from unittest import mock
SDP = Path(__file__).resolve().parents[1]
TRIGGERS = SDP / "Server/Unix/p4/common/bin/triggers"
class TriggerUtilityTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="sdp-trigger-utilities-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
def script(self, name, *args):
return subprocess.run([sys.executable, str(TRIGGERS / name), *args], cwd=self.root, text=True, capture_output=True, timeout=10)
def form(self, content):
path = self.root / "form.txt"
path.write_text(content)
return path
def test_jobs_filter_rejects_expensive_patterns_and_allows_specific_search(self):
for pattern in ("*", "**", "description=*", "*text*"):
self.assertEqual(self.script("JobsCmdFilter.py", "-e", pattern).returncode, 1)
self.assertEqual(self.script("JobsCmdFilter.py", "-e", "status=open", "-m", "10").returncode, 0)
def test_workspace_ascii_and_name_checks(self):
for content, expected in [("Client: valid_name\nRoot: /work/test\n", 0), ("Client: bad$name\n", 1), ("Client: valid\nDescription: caf\u00e9\n", 1)]:
self.assertEqual(self.script("PreventWsNonAscii.py", str(self.form(content))).returncode, expected)
def test_workspace_defaults_change_only_new_forms(self):
for script in ("SetWsOptions.py", "SetWsOptionsAndView.py"):
content = "Client: test\nOptions: noallwrite nomodtime normdir\nSubmitOptions: submitunchanged\nView:\n\t//depot/... //test/...\n"
path = self.form(content)
result = self.script(script, str(path))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(" modtime rmdir", path.read_text())
self.assertIn("leaveunchanged", path.read_text())
if script.endswith("AndView.py"):
self.assertIn("//EditMe-AdjustThisPath/...", path.read_text())
existing = content.replace("Options:", "Access: 2026/10/16 12:00:00\nOptions:")
path.write_text(existing)
self.assertEqual(self.script(script, str(path)).returncode, 0)
self.assertEqual(path.read_text(), existing)
def test_workspace_template_and_stream_views_are_preserved(self):
content = "Client: test\nOptions: nomodtime normdir\nView:\n\t//specific/... //test/...\n"
path = self.form(content)
self.assertEqual(self.script("SetWsOptionsAndView.py", str(path), "-t,template,test").returncode, 0)
self.assertEqual(path.read_text(), content)
stream = content.replace("View:\n", "Stream: //stream/main\nView:\n")
path.write_text(stream)
self.assertEqual(self.script("SetWsOptionsAndView.py", str(path)).returncode, 0)
self.assertIn("//specific/...", path.read_text())
def invoke_main(self, name, arguments, output=None, error=None):
namespace = runpy.run_path(str(TRIGGERS / name))
with mock.patch.object(sys, "argv", [name, *arguments]), mock.patch("subprocess.check_output", return_value=output, side_effect=error) as command, contextlib.redirect_stdout(io.StringIO()), self.assertRaises(SystemExit) as stop:
namespace["main"]()
return stop.exception.code, command
def test_tfs_job_must_be_real_job_not_description_text(self):
valid = "Change 1\n\tdescription\nJobs fixed ...\nTFS123 fixed on today\n\tdetail\nAffected files ...\n"
invalid = "Change 1\n\tTFS123 in description\nJobs fixed ...\nOTHER99 fixed\n\tTFS456 in job description\nAffected files ...\n"
self.assertEqual(self.invoke_main("TFSJobCheck.py", ["1"], valid)[0], 0)
self.assertEqual(self.invoke_main("TFSJobCheck.py", ["1"], invalid)[0], 1)
self.assertEqual(self.invoke_main("TFSJobCheck.py", ["1"], "No linked jobs")[0], 1)
self.assertEqual(self.invoke_main("TFSJobCheck.py", ["1"], error=OSError("offline"))[0], 1)
def test_description_template_applies_only_to_group_members(self):
content = "Change: new\nDescription:\n\t<enter description here>\n"
path = self.form(content)
self.assertEqual(self.invoke_main("submit_form_1.py", [str(path), "alice"], "other_group\n")[0], 0)
self.assertEqual(path.read_text(), content)
self.assertEqual(self.invoke_main("submit_form_1.py", [str(path), "alice"], "submit_form_1_users\n")[0], 0)
self.assertIn("Incident:", path.read_text())
self.assertIn("Reviewed by:", path.read_text())
def test_command_block_honors_exempt_user_and_read_only_option(self):
self.assertEqual(self.script("command_block.py", "perforce").returncode, 0)
self.assertEqual(self.script("command_block.py", "ordinary", "-o").returncode, 0)
self.assertEqual(self.script("command_block.py", "ordinary", "-i").returncode, 1)
def test_checkpoint_trigger_blocks_checkpoint_and_allows_journal(self):
for command, expected in [("checkpoint", 1), ("checkpoint -z", 1), ("journal", 0), ("stop", 0)]:
result = subprocess.run(["/bin/bash", str(TRIGGERS / "block_checkpoint.sh"), "ordinary", *command.split()], capture_output=True, text=True)
self.assertEqual(result.returncode, expected)
def p4_module(self):
module = types.ModuleType("P4")
module.P4Exception = type("P4Exception", (Exception,), {})
module.P4 = mock.Mock(return_value=types.SimpleNamespace(connect=lambda: None, errors=[], warnings=[]))
def depot(path):
obj = types.SimpleNamespace(depotFile=path, revisions=[])
def revision():
rev = types.SimpleNamespace(); obj.revisions.append(rev); return rev
obj.new_revision = revision
return obj
module.DepotFile = depot
return module
def test_p4change_preserves_files_revisions_and_jobs(self):
with mock.patch.dict(sys.modules, {"P4": self.p4_module(), "sdp_structured_log": None}):
namespace = runpy.run_path(str(TRIGGERS / "P4Triggers.py"))
change = namespace["P4Change"]({"change": "1", "user": "u", "client": "c", "desc": "d", "time": "0", "status": "submitted", "depotFile": ["//d/a"], "type": ["text"], "rev": ["2"], "action": ["edit"], "job": ["j1"], "jobstat": ["closed"]})
self.assertEqual(change.files[0].depotFile, "//d/a")
self.assertEqual(change.files[0].revisions[0].rev, "2")
self.assertEqual(change.jobs, {"j1": "closed"})
def test_p4trigger_unexpected_exception_returns_failure_and_logs(self):
with mock.patch.dict(sys.modules, {"P4": self.p4_module(), "sdp_structured_log": None}):
namespace = runpy.run_path(str(TRIGGERS / "P4Triggers.py"))
trigger = namespace["P4Trigger"](log=str(self.root / "trigger.log"))
self.addCleanup(trigger.log.close)
trigger.getChange = mock.Mock(side_effect=RuntimeError("fixture exception"))
self.assertEqual(trigger.parseChange("1"), 1)
self.assertIn("fixture exception", (self.root / "trigger.log").read_text())
def radius(self, replies):
pyrad = types.ModuleType("pyrad")
packet = types.ModuleType("pyrad.packet"); packet.AccessRequest=1; packet.AccessAccept=2
pyrad.packet = packet
client = types.ModuleType("pyrad.client")
dictionary = types.ModuleType("pyrad.dictionary"); dictionary.Dictionary = lambda path: path
request = dict(); request = type("Request", (dict,), {"PwCrypt": lambda self, text: b"encrypted"})()
outcomes = iter(replies)
def create(**kwargs):
reply = next(outcomes)
if isinstance(reply, Exception):
raise reply
return types.SimpleNamespace(CreateAuthPacket=lambda **kwargs: request, SendPacket=lambda req: types.SimpleNamespace(code=reply, keys=lambda: []))
client.Client = mock.Mock(side_effect=create)
env = {"SDP_AUTH_RADIUS_SERVERS": "one two", "SDP_AUTH_RADIUS_SECRET": "fixture"}
with mock.patch.dict(sys.modules, {"pyrad": pyrad, "pyrad.packet": packet, "pyrad.client": client, "pyrad.dictionary": dictionary}), mock.patch.dict(os.environ, env), mock.patch.object(sys, "argv", ["radtest", "alice"]), mock.patch.object(sys, "stdin", io.StringIO("password")), contextlib.redirect_stdout(io.StringIO()):
try:
runpy.run_path(str(TRIGGERS / "radtest.py"), run_name="__main__")
rc = 0
except SystemExit as error:
rc = error.code
return rc, client.Client.call_count
def test_radius_diagnostic_reports_denial_as_failure(self):
self.assertEqual(self.radius([3, 3]), (1, 2))
def test_radius_diagnostic_tries_next_server_after_failure(self):
self.assertEqual(self.radius([OSError("offline"), 2]), (0, 2))
if __name__ == "__main__":
unittest.main()
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 34004 | Russell C. Jackson (Rusty) |
Improve SDP backup/recovery, Maintenance, logging and cloud operation. Add owned checkpoint locks, recovery-aware retention, preserved database switches, bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password initialization, edge staging, standby promotion state parsing, proxy cache setup, typed depot verification and historical-server configuration/error handling. Remove unused OTP programs; keep Windows removed, server.locks cleanup and the journal-count condition unchanged. Add no global shell strict modes. Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips; 19 deployed-archive comparisons per combination. Three six-node topology runs pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9 pass all 14 cells and 98 scenarios. Static, documentation and provenance checks pass. Containers do not certify every fleet kernel, patch build or integration. Tested source manifest SHA-256: e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695 Deployed comparison archive SHA-256: 1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a |