test_ccheck_compatibility.py #1

  • //
  • guest/
  • russell_jackson/
  • sdp/
  • test/
  • test_ccheck_compatibility.py
  • View
  • Commits
  • Open Download .zip Download (10 KB)
#!/usr/bin/env python3
"""Offline configuration compatibility checks; never connects to a server."""
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]
CHECK = ROOT / 'Server/Unix/p4/common/bin/ccheck.sh'
SETUP = ROOT / 'Server/Unix/setup/configure_new_server.sh'
BASH = os.environ.get('SDP_TEST_BASH', shutil.which('bash'))


def function(name):
    text = CHECK.read_text()
    start = re.search(r'^function ' + name + r'\s*\(\)\s*\{', text, re.M)
    return text[start.start():text.index('\n}', start.end()) + 2]


class Compatibility(unittest.TestCase):
    def shell(self, code, **environment):
        return subprocess.run([BASH, '-c', code], env=dict(os.environ, **environment), text=True, capture_output=True)

    def test_numeric_release_boundaries(self):
        helper = function('ccheck_release_at_least')
        for current, minimum, expected in [('2023.2.999', '2024.1', 1), ('2024.1.1', '2024.1', 0), ('2024.2.1', '2025.1', 1), ('2025.1.1', '2025.1', 0), ('2025.1.999', '2025.2', 1), ('2025.2.1', '2025.2', 0), ('2024.10.1', '2024.2', 0), ('invalid', '2024.1', 2)]:
            with self.subTest(current=current, minimum=minimum):
                result = self.shell(helper + '\nccheck_release_at_least "$CURRENT" "$MINIMUM"', CURRENT=current, MINIMUM=minimum)
                self.assertEqual(result.returncode, expected)

    def test_only_documented_new_configurables_are_skipped(self):
        helpers = function('ccheck_release_at_least') + '\n' + function('ccheck_configurable_supported')
        for setting, before, supported in [('dm.user.hideinvalid', '2023.2.9', '2024.1.1'), ('db.journalrotate.warnthresh', '2023.2.9', '2024.1.1'), ('server.startup.autorestart', '2024.2.9', '2025.1.1'), ('lbr.unloaddepot.compress', '2025.1.9', '2025.2.1')]:
            with self.subTest(setting=setting):
                code = 'msg(){ printf "%s\\n" "$*"; };\n' + helpers + '\nccheck_configurable_supported "$SETTING"'
                old = self.shell(code, SETTING=setting, P4D_VERSION=before)
                self.assertEqual(old.returncode, 1)
                self.assertIn('COMPATIBILITY: Skipping ' + setting, old.stdout)
                new = self.shell(code, SETTING=setting, P4D_VERSION=supported)
                self.assertEqual(new.returncode, 0)
                self.assertNotIn('Skipping', new.stdout)
        unknown = self.shell(helpers + '\nccheck_configurable_supported unknown.typo', P4D_VERSION='2023.1.1')
        self.assertEqual(unknown.returncode, 0, 'An unknown name must reach normal query/error handling')

    def test_automation_login_is_optional_but_real_failures_propagate(self):
        helper = function('ccheck_login_automation')
        code = 'msg(){ printf "%s\\n" "$*"; }; run(){ echo "CALLED:$1"; return "$FAILURE"; };\n' + helper + '\nccheck_login_automation'
        absent_env = dict(os.environ)
        absent_env.pop('SDP_AUTOMATION_USERS', None)
        absent = subprocess.run([BASH, '-c', code], env=dict(absent_env, FAILURE='7'), text=True, capture_output=True)
        self.assertEqual(absent.returncode, 0)
        self.assertIn('not required', absent.stdout)
        self.assertNotIn('CALLED:', absent.stdout)
        result = self.shell(code, SDP_AUTOMATION_USERS='builder,trigger', FAILURE='7')
        self.assertEqual(result.returncode, 7)
        self.assertIn('CALLED:p4login -v -automation -L off', result.stdout)

    def test_failed_unknown_configurable_query_is_an_error_not_a_skip(self):
        with tempfile.TemporaryDirectory() as temp:
            folder = Path(temp)
            p4 = folder / 'p4'
            p4.write_text('#!' + sys.executable + '\nimport sys\nprint("permission denied", file=sys.stderr)\nsys.exit(9)\n')
            p4.chmod(0o755)
            config = folder / 'config'
            config.write_text('always|unknown.typo|1|Exact|Required|any|None\n')
            text = CHECK.read_text()
            block = text[text.index('# shellcheck disable=2094'):text.index('done < "$TmpFile"', text.index('# shellcheck disable=2094')) + len('done < "$TmpFile"')]
            code = 'ErrorCount=0; msg(){ echo "$*"; }; dbg(){ :; }; errmsg(){ echo "$*"; ErrorCount=$((ErrorCount+1)); }; declare -A CurrentValue CurrentValueByDefault ExpectedValue CompareStyle Optionality ServerIDType SetNotes Categories;\n' + function('ccheck_release_at_least') + '\n' + function('ccheck_configurable_supported') + '\n' + block + '\nexit "$ErrorCount"'
            result = self.shell(code, TmpFile=str(config), P4=str(p4), P4D_VERSION='2023.1.1', DoSecurityCheck='0', SDPInstance='1', P4ROOT='/unused', P4SERVER='p4_1', KEEPLOGS='7', LOGS='/unused')
            self.assertEqual(result.returncode, 1)
            self.assertIn('Could not query configurable', result.stdout)
            self.assertNotIn('COMPATIBILITY:', result.stdout)

    def test_setup_aborts_after_configuration_application_failure(self):
        with tempfile.TemporaryDirectory() as temp:
            folder = Path(temp)
            check = folder / 'ccheck.sh'
            check.write_text('#!/bin/sh\nexit 7\n')
            check.chmod(0o755)
            text = SETUP.read_text()
            block = text[text.index('if [[ -x "$P4CBIN/ccheck.sh" ]]'):text.index('msg "Restarting server to ensure all configurable changes take effect."')]
            code = 'msg(){ :; }; bail(){ echo "$*"; exit 1; };\n' + block + '\necho UNSAFE_CONTINUE'
            result = self.shell(code, P4CBIN=str(folder), INSTANCE='1')
            self.assertEqual(result.returncode, 1)
            self.assertIn('failed to apply configurables', result.stdout)
            self.assertNotIn('UNSAFE_CONTINUE', result.stdout)

    def disk_helpers(self):
        return '\n'.join(function(name) for name in ('ccheck_release_at_least', 'at_least_size_compare', 'convert_size_values', 'safety_check_for_filesys_min'))

    def test_diskspace_validates_status_and_every_record_before_comparing_minimum(self):
        code = '''ErrorCount=0; ArchiveRootPreview=0; P4=fake_p4
msg(){ echo "$*"; }; dbg(){ :; }; warnmsg(){ echo "$*"; }
errmsg(){ echo "ERROR:$*"; ErrorCount=$((ErrorCount+1)); }
fake_p4(){ printf '%s' "$RECORDS"; return "$QUERY_STATUS"; }
''' + self.disk_helpers() + '''
safety_check_for_filesys_min filesys.depot.min 5G
status=$?; echo "errors=$ErrorCount"; exit "$status"
'''
        cases = [('', '0', 1), ('6000000000', '7', 1), ('invalid', '0', 1), ('6000000000\ninvalid', '0', 1), ('9223372036854775808', '0', 1), ('6000000000\n4000000000', '0', 1), ('4000000000\n6000000000', '0', 1), ('6000000000\n7000000000', '0', 0), ('0006000000000', '0', 0)]
        for records, query_status, expected in cases:
            with self.subTest(records=records, query_status=query_status):
                result = self.shell(code, RECORDS=records, QUERY_STATUS=query_status, P4D_VERSION='2024.1.3006289')
                self.assertEqual(result.returncode, expected, result.stdout + result.stderr)
                self.assertIn('errors=' + str(expected), result.stdout)
                self.assertNotIn('convert_size_values(): Bad Usage', result.stdout)

    def test_unit_only_size_is_rejected(self):
        result = self.shell('errmsg(){ echo ERROR; };\n' + function('at_least_size_compare') + '\nat_least_size_compare B 5G')
        self.assertEqual(result.returncode, 1)
        self.assertIn('ERROR', result.stdout)

    def test_archive_root_dependency_preserves_fix_preview_and_failure_semantics(self):
        code = '''ErrorCount=0; ArchiveRootPreview=0; ConfigFixedCount=0; ArchiveConfigured=0; Queries=0; P4=fake_p4
declare -a EasyFixConfigurables EasyFixValues EasyFixApplied PlannedFixApplied
PlannedFixConfigurables=(server.depot.root); PlannedFixAdvice=(SET_ROOT); PlannedFixAutomated=(1)
msg(){ echo "$*"; }; dbg(){ :; }; warnmsg(){ echo "$*"; }
errmsg(){ echo "ERROR:$*"; ErrorCount=$((ErrorCount+1)); }
run(){ echo "RUN:$1"; [[ "$RUN_STATUS" == 0 ]] || return "$RUN_STATUS"; [[ "$NoOp" == 1 ]] || ArchiveConfigured=1; return 0; }
fake_p4(){ echo QUERY >&2; [[ "$ArchiveConfigured" == 1 ]] && echo 6000000000; return 0; }
if [[ "$CATEGORY" == easy ]]; then
 EasyFixConfigurables=(server.depot.root); EasyFixValues=(/srv/archives); PlannedFixConfigurables=()
fi
''' + self.disk_helpers() + '\n' + function('ccheck_prepare_archive_root') + '''
ccheck_prepare_archive_root || exit "$?"
safety_check_for_filesys_min filesys.depot.min 5G
status=$?
echo "configured=$ArchiveConfigured fixed=$ConfigFixedCount easy=${EasyFixApplied[0]:-0} planned=${PlannedFixApplied[0]:-0}"
exit "$status"
'''
        for category, all_fixes, preview, run_status, expected, configured in [('planned', '1', '0', '0', 0, 1), ('planned', '0', '0', '0', 1, 0), ('planned', '1', '1', '0', 0, 0), ('planned', '1', '0', '7', 1, None), ('easy', '0', '0', '0', 0, 1)]:
            with self.subTest(category=category, all_fixes=all_fixes, preview=preview, run_status=run_status):
                result = self.shell(code, CATEGORY=category, ApplyAllFixes=all_fixes, NoOp=preview, RUN_STATUS=run_status, P4D_VERSION='2024.1.3006289')
                self.assertEqual(result.returncode, expected, result.stdout + result.stderr)
                if configured is not None:
                    self.assertIn('configured=' + str(configured), result.stdout)
                if preview == '1':
                    self.assertNotIn('QUERY', result.stderr)
                    self.assertIn('NO_OP:', result.stdout)
                if all_fixes == '0' and category == 'planned':
                    self.assertNotIn('RUN:', result.stdout)
                if run_status != '0':
                    self.assertNotIn('QUERY', result.stderr)


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