export_baseline.py #1

  • //
  • guest/
  • russell_jackson/
  • sdp/
  • test/
  • containers/
  • export_baseline.py
  • View
  • Commits
  • Open Download .zip Download (5 KB)
#!/usr/bin/env python3
"""Export immutable submitted Perforce revisions for paired regression tests.

Never syncs or changes the workspace. Pending/local bytes are not baseline data.
"""
import argparse
from concurrent.futures import ThreadPoolExecutor
import hashlib
import io
import json
import marshal
import os
from pathlib import Path, PurePosixPath
import subprocess


def records(arguments):
    result = subprocess.run(["p4", "-G", *arguments], capture_output=True)
    stream = io.BytesIO(result.stdout)
    rows, errors = [], []
    while True:
        try:
            row = marshal.load(stream)
        except EOFError:
            break
        row = {(k.decode() if isinstance(k, bytes) else k):
               (v.decode() if isinstance(v, bytes) else v) for k, v in row.items()}
        if row.get("code") == "error":
            errors.append(row.get("data", str(row)))
        elif row.get("depotFile"):
            rows.append(row)
    if result.returncode or errors:
        raise RuntimeError("Perforce baseline query failed: " + "\n".join(errors) + result.stderr.decode())
    return rows


def relative_path(depot_root, depot_file):
    prefix = depot_root.rstrip("/") + "/"
    if not depot_file.startswith(prefix):
        raise ValueError("File is outside the requested baseline depot")
    relative = PurePosixPath(depot_file[len(prefix):])
    if relative.is_absolute() or not relative.parts or ".." in relative.parts:
        raise ValueError("Invalid baseline path")
    return relative.as_posix()


def export(depot_root, change, destination):
    if not depot_root.startswith("//") or any(c in depot_root for c in "@#*%") or "..." in depot_root:
        raise ValueError("Use an explicit depot directory without revision syntax or wildcards")
    if change <= 0:
        raise ValueError("A positive submitted changelist is required")
    if destination.exists():
        raise ValueError("Baseline destination must be new")
    inventory = records(["files", depot_root.rstrip("/") + "/...@" + str(change)])
    inventory = [row for row in inventory if row.get("action") not in ("delete", "move/delete")]
    if not inventory:
        raise ValueError("No active files at the requested changelist")
    paths = [relative_path(depot_root, row["depotFile"]) for row in inventory]
    if len(set(paths)) != len(paths):
        raise ValueError("Duplicate baseline paths")
    destination.mkdir(parents=True)

    def download(item):
        row, relative = item
        target = destination / relative
        target.parent.mkdir(parents=True, exist_ok=True)
        revision = row["depotFile"] + "#" + str(row["rev"])
        result = subprocess.run(["p4", "print", "-q", revision], capture_output=True)
        if result.returncode:
            raise RuntimeError("Failed to export " + revision + ": " + result.stderr.decode())
        data = result.stdout
        symlink = None
        if row.get("type", "").startswith("symlink"):
            symlink = data.decode().rstrip("\n")
            # An exported baseline must not redirect a test outside its tree.
            if Path(symlink).is_absolute() or not (target.parent / symlink).resolve().is_relative_to(destination.resolve()):
                raise ValueError("Unsafe baseline symlink: " + relative)
            target.symlink_to(symlink)
            data = symlink.encode()
        else:
            target.write_bytes(data)
            target.chmod(0o755 if "x" in row.get("type", "").partition("+")[2] else 0o644)
        return {"path": relative, "depot": row["depotFile"], "haveRev": str(row["rev"]),
                "action": "", "source_kind": "submitted", "baseline_change": change,
                "sha256": hashlib.sha256(data).hexdigest(), "symlink": symlink}

    with ThreadPoolExecutor(max_workers=6) as pool:
        manifest = sorted(pool.map(download, zip(inventory, paths)), key=lambda row: row["path"])
    # Publication of this file marks a complete export; partial downloads cannot
    # be accepted as a baseline by the runner or acceptance gate.
    (destination / ".export-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
    (destination / ".baseline-info.json").write_text(json.dumps({
        "source_kind": "submitted", "depot_root": depot_root, "change": change,
        "production_equivalence": "Unconfirmed until the operator identifies the deployed revision",
        "files": len(manifest)}, indent=2) + "\n")
    return manifest


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("depot_root")
    parser.add_argument("change", type=int)
    parser.add_argument("destination", type=Path)
    args = parser.parse_args()
    try:
        manifest = export(args.depot_root, args.change, args.destination)
    except (OSError, ValueError, RuntimeError) as error:
        parser.exit(1, str(error) + "\n")
    print("Exported %d submitted files at changelist %d" % (len(manifest), args.change))


if __name__ == "__main__":
    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