generated from coulomb/repo-seed
511 lines
17 KiB
Python
511 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""OpenCMIS TCK wrapper boundary.
|
|
|
|
The wrapper owns extension-local orchestration only: dependency checks, optional
|
|
user-supplied TCK command execution, raw artifact capture, and normalization into
|
|
the guide-board runner result contract.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from open_cmis_tck.normalization import ( # noqa: E402
|
|
aggregate_case_result,
|
|
normalize_case_status,
|
|
parse_text_report,
|
|
result_counts,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--context", required=True)
|
|
args = parser.parse_args()
|
|
|
|
context = _load_context(Path(args.context))
|
|
selected_group = context["step"].get("check_group")
|
|
config = _opencmis_policy(context)
|
|
dependency_results = {
|
|
"java": _probe_command(["java", "-version"]),
|
|
"maven": _probe_command(["mvn", "-version"]),
|
|
}
|
|
missing = [
|
|
name
|
|
for name, result in dependency_results.items()
|
|
if not result["available"]
|
|
]
|
|
|
|
if config.get("requires_java_maven", True) and missing:
|
|
_emit(
|
|
{
|
|
"result": "blocked",
|
|
"observations": [
|
|
"OpenCMIS TCK execution skipped because required local dependencies are missing: "
|
|
+ ", ".join(missing)
|
|
+ "."
|
|
],
|
|
"facts": {
|
|
"blocked_reason": "missing_dependency",
|
|
"selected_check_group": selected_group,
|
|
"dependencies": dependency_results,
|
|
},
|
|
"artifact_refs": [],
|
|
}
|
|
)
|
|
return 0
|
|
|
|
command_template = _configured_command(config)
|
|
if command_template is not None:
|
|
_emit(_run_configured_tck(context, selected_group, dependency_results, command_template))
|
|
return 0
|
|
|
|
_emit(
|
|
{
|
|
"result": "blocked",
|
|
"observations": [
|
|
"Java and Maven are available, but the Apache Chemistry OpenCMIS TCK invocation is not configured yet."
|
|
],
|
|
"facts": {
|
|
"blocked_reason": "tck_invocation_not_configured",
|
|
"selected_check_group": selected_group,
|
|
"dependencies": dependency_results,
|
|
"next_step": "Configure runtime_policy.opencmis_tck.command or OPENCMIS_TCK_COMMAND_JSON with an argv list for the local Apache Chemistry TCK runner.",
|
|
},
|
|
"artifact_refs": [],
|
|
}
|
|
)
|
|
return 0
|
|
|
|
|
|
def _load_context(path: Path) -> dict[str, Any]:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
value = json.load(handle)
|
|
if not isinstance(value, dict):
|
|
raise ValueError("context must be a JSON object")
|
|
return value
|
|
|
|
|
|
def _probe_command(command: list[str]) -> dict[str, Any]:
|
|
executable = shutil.which(command[0])
|
|
if executable is None:
|
|
return {
|
|
"available": False,
|
|
"path": None,
|
|
"returncode": None,
|
|
"version_output": None,
|
|
}
|
|
|
|
completed = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
check=False,
|
|
)
|
|
output = "\n".join(
|
|
part.strip()
|
|
for part in [completed.stdout, completed.stderr]
|
|
if part.strip()
|
|
)
|
|
return {
|
|
"available": completed.returncode == 0,
|
|
"path": executable,
|
|
"returncode": completed.returncode,
|
|
"version_output": output[:2000],
|
|
}
|
|
|
|
|
|
def _run_configured_tck(
|
|
context: dict[str, Any],
|
|
selected_group: str | None,
|
|
dependency_results: dict[str, dict[str, Any]],
|
|
command_template: list[str],
|
|
) -> dict[str, Any]:
|
|
run_dir = Path(context["run_dir"])
|
|
artifact_dir = run_dir / "artifacts" / "open-cmis-tck" / "tck" / _safe_id(selected_group or "unknown")
|
|
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
command = [_expand_arg(arg, context, selected_group, artifact_dir) for arg in command_template]
|
|
invocation_ref = _write_json_artifact(
|
|
run_dir,
|
|
artifact_dir,
|
|
"invocation.json",
|
|
{
|
|
"selected_check_group": selected_group,
|
|
"command": command,
|
|
"target_profile_id": context["target_profile"]["id"],
|
|
"repository_id": _repository_id(context),
|
|
"browser_binding_url": _browser_url(context),
|
|
"credentials_ref": _credentials_ref(context),
|
|
},
|
|
)
|
|
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
cwd=Path(context["extension_path"]),
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=_timeout_seconds(context),
|
|
check=False,
|
|
)
|
|
except FileNotFoundError as exc:
|
|
return {
|
|
"result": "blocked",
|
|
"observations": [
|
|
f"OpenCMIS TCK command could not start because {exc.filename!r} was not found."
|
|
],
|
|
"facts": {
|
|
"blocked_reason": "missing_command",
|
|
"selected_check_group": selected_group,
|
|
"dependencies": dependency_results,
|
|
"command": command,
|
|
},
|
|
"artifact_refs": [invocation_ref],
|
|
}
|
|
except subprocess.TimeoutExpired:
|
|
return {
|
|
"result": "infrastructure_error",
|
|
"observations": [
|
|
f"OpenCMIS TCK command timed out after {_timeout_seconds(context)} seconds."
|
|
],
|
|
"facts": {
|
|
"selected_check_group": selected_group,
|
|
"dependencies": dependency_results,
|
|
"command": command,
|
|
},
|
|
"artifact_refs": [invocation_ref],
|
|
}
|
|
|
|
stdout_ref = _write_text_artifact(run_dir, artifact_dir, "stdout.log", completed.stdout)
|
|
stderr_ref = _write_text_artifact(run_dir, artifact_dir, "stderr.log", completed.stderr)
|
|
normalized = _normalize_tck_output(completed, artifact_dir, selected_group)
|
|
normalized["facts"].update(
|
|
{
|
|
"selected_check_group": selected_group,
|
|
"dependencies": dependency_results,
|
|
"command": command,
|
|
"returncode": completed.returncode,
|
|
"browser_binding_url": _browser_url(context),
|
|
"repository_id": _repository_id(context),
|
|
}
|
|
)
|
|
normalized_ref = _write_json_artifact(
|
|
run_dir,
|
|
artifact_dir,
|
|
"normalized-runner-result.json",
|
|
normalized,
|
|
)
|
|
normalized["artifact_refs"].extend([invocation_ref, stdout_ref, stderr_ref, normalized_ref])
|
|
return normalized
|
|
|
|
|
|
def _normalize_tck_output(
|
|
completed: subprocess.CompletedProcess[str],
|
|
artifact_dir: Path,
|
|
selected_group: str | None,
|
|
) -> dict[str, Any]:
|
|
parsed_stdout = _parse_json(completed.stdout)
|
|
if isinstance(parsed_stdout, dict):
|
|
return _normalize_json_result(parsed_stdout, completed.returncode, selected_group)
|
|
|
|
junit_files = sorted(artifact_dir.glob("*.xml"))
|
|
if junit_files:
|
|
return _normalize_junit_result(junit_files[0], completed.returncode, selected_group)
|
|
|
|
text_cases = parse_text_report(completed.stdout, selected_group)
|
|
if text_cases:
|
|
counts = result_counts(text_cases)
|
|
return {
|
|
"result": aggregate_case_result(counts, completed.returncode),
|
|
"observations": [
|
|
f"OpenCMIS TCK group {selected_group!r} produced native TextReport output.",
|
|
"Normalized OpenCMIS TextReport case statuses: "
|
|
+ ", ".join(f"{key}: {value}" for key, value in counts.items())
|
|
+ ".",
|
|
],
|
|
"facts": {
|
|
"normalizer": "opencmis-text-report",
|
|
"result_counts": counts,
|
|
"cases": text_cases[:500],
|
|
},
|
|
"artifact_refs": [],
|
|
}
|
|
|
|
if completed.returncode == 0:
|
|
return {
|
|
"result": "pass",
|
|
"observations": [
|
|
"OpenCMIS TCK command completed successfully, but no structured result payload was found."
|
|
],
|
|
"facts": {
|
|
"normalizer": "exit-code",
|
|
"result_counts": {"pass": 1},
|
|
},
|
|
"artifact_refs": [],
|
|
}
|
|
return {
|
|
"result": "fail",
|
|
"observations": [
|
|
"OpenCMIS TCK command exited with a non-zero status and no structured result payload was found."
|
|
],
|
|
"facts": {
|
|
"normalizer": "exit-code",
|
|
"result_counts": {"fail": 1},
|
|
},
|
|
"artifact_refs": [],
|
|
}
|
|
|
|
|
|
def _normalize_json_result(
|
|
payload: dict[str, Any],
|
|
returncode: int,
|
|
selected_group: str | None,
|
|
) -> dict[str, Any]:
|
|
artifact_refs = _artifact_refs_from_payload(payload)
|
|
source_facts = payload.get("facts", {})
|
|
if not isinstance(source_facts, dict):
|
|
source_facts = {}
|
|
cases = _json_cases(payload)
|
|
if cases:
|
|
counts: dict[str, int] = {}
|
|
normalized_cases = []
|
|
for case in cases:
|
|
status = normalize_case_status(str(case.get("status", "unknown")))
|
|
counts[status] = counts.get(status, 0) + 1
|
|
normalized_cases.append(_normalize_json_case(case, status))
|
|
return {
|
|
"result": aggregate_case_result(counts, returncode),
|
|
"observations": [
|
|
f"OpenCMIS TCK group {selected_group!r} produced {sum(counts.values())} normalized case result(s)."
|
|
],
|
|
"facts": {
|
|
**source_facts,
|
|
"normalizer": "json-cases",
|
|
"result_counts": counts,
|
|
"cases": normalized_cases[:200],
|
|
},
|
|
"artifact_refs": artifact_refs,
|
|
}
|
|
|
|
result = normalize_case_status(str(payload.get("result", "unknown")))
|
|
if returncode != 0 and result in {"pass", "warning", "skipped"}:
|
|
result = "infrastructure_error"
|
|
return {
|
|
"result": result,
|
|
"observations": _observations_from_payload(payload, selected_group),
|
|
"facts": {
|
|
**source_facts,
|
|
"normalizer": "json-runner-result",
|
|
"result_counts": {result: 1},
|
|
"payload": payload,
|
|
},
|
|
"artifact_refs": artifact_refs,
|
|
}
|
|
|
|
|
|
def _normalize_junit_result(
|
|
path: Path,
|
|
returncode: int,
|
|
selected_group: str | None,
|
|
) -> dict[str, Any]:
|
|
tree = ET.parse(path)
|
|
root = tree.getroot()
|
|
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
|
|
total = sum(int(suite.get("tests", "0")) for suite in suites)
|
|
failures = sum(int(suite.get("failures", "0")) for suite in suites)
|
|
errors = sum(int(suite.get("errors", "0")) for suite in suites)
|
|
skipped = sum(int(suite.get("skipped", "0")) for suite in suites)
|
|
passed = max(0, total - failures - errors - skipped)
|
|
counts = {
|
|
"pass": passed,
|
|
"fail": failures + errors,
|
|
"skipped": skipped,
|
|
}
|
|
counts = {key: value for key, value in counts.items() if value}
|
|
return {
|
|
"result": aggregate_case_result(counts, returncode),
|
|
"observations": [
|
|
f"OpenCMIS TCK group {selected_group!r} produced JUnit-style XML results."
|
|
],
|
|
"facts": {
|
|
"normalizer": "junit-xml",
|
|
"result_counts": counts,
|
|
"junit_xml": str(path),
|
|
},
|
|
"artifact_refs": [],
|
|
}
|
|
|
|
|
|
def _json_cases(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
for key in ("tests", "cases", "results"):
|
|
value = payload.get(key)
|
|
if isinstance(value, list) and all(isinstance(item, dict) for item in value):
|
|
return value
|
|
return []
|
|
|
|
|
|
def _normalize_json_case(case: dict[str, Any], status: str) -> dict[str, Any]:
|
|
normalized = {
|
|
"id": str(case.get("id", case.get("name", "unnamed"))),
|
|
"status": status,
|
|
"message": str(case.get("message", case.get("reason", ""))),
|
|
}
|
|
for key in [
|
|
"status_native",
|
|
"group_name",
|
|
"selected_check_group",
|
|
"test_name",
|
|
"class_name",
|
|
"group_class",
|
|
"group_classes",
|
|
"duration_ms",
|
|
"level",
|
|
"source",
|
|
"source_location",
|
|
]:
|
|
if key in case:
|
|
normalized[key] = case[key]
|
|
return normalized
|
|
|
|
|
|
def _artifact_refs_from_payload(payload: dict[str, Any]) -> list[str]:
|
|
refs = payload.get("artifact_refs", [])
|
|
if not isinstance(refs, list):
|
|
return []
|
|
return [ref for ref in refs if isinstance(ref, str) and ref]
|
|
|
|
|
|
def _observations_from_payload(payload: dict[str, Any], selected_group: str | None) -> list[str]:
|
|
observations = payload.get("observations")
|
|
if isinstance(observations, list):
|
|
return [str(item) for item in observations]
|
|
message = payload.get("message")
|
|
if isinstance(message, str) and message:
|
|
return [message]
|
|
return [f"OpenCMIS TCK group {selected_group!r} returned a structured result."]
|
|
|
|
|
|
def _opencmis_policy(context: dict[str, Any]) -> dict[str, Any]:
|
|
policy = context["assessment_profile"].get("runtime_policy", {}).get("opencmis_tck", {})
|
|
return policy if isinstance(policy, dict) else {}
|
|
|
|
|
|
def _configured_command(config: dict[str, Any]) -> list[str] | None:
|
|
command = config.get("command")
|
|
if isinstance(command, list) and all(isinstance(item, str) and item for item in command):
|
|
return command
|
|
|
|
env_json = os.environ.get("OPENCMIS_TCK_COMMAND_JSON")
|
|
if env_json:
|
|
parsed = json.loads(env_json)
|
|
if isinstance(parsed, list) and all(isinstance(item, str) and item for item in parsed):
|
|
return parsed
|
|
raise ValueError("OPENCMIS_TCK_COMMAND_JSON must be a JSON string array")
|
|
|
|
env_command = os.environ.get("OPENCMIS_TCK_COMMAND")
|
|
if env_command:
|
|
return shlex.split(env_command)
|
|
return None
|
|
|
|
|
|
def _expand_arg(
|
|
value: str,
|
|
context: dict[str, Any],
|
|
selected_group: str | None,
|
|
artifact_dir: Path,
|
|
) -> str:
|
|
return (
|
|
value.replace("{run_dir}", context["run_dir"])
|
|
.replace("{artifact_dir}", str(artifact_dir))
|
|
.replace("{extension_path}", context["extension_path"])
|
|
.replace("{browser_url}", _browser_url(context) or "")
|
|
.replace("{repository_id}", _repository_id(context) or "")
|
|
.replace("{credentials_ref}", _credentials_ref(context) or "")
|
|
.replace("{target_profile_dir}", _target_profile_dir(context) or "")
|
|
.replace("{check_group}", selected_group or "")
|
|
.replace("{target_id}", context["target_profile"]["id"])
|
|
.replace("{timeout_seconds}", str(int(_timeout_seconds(context))))
|
|
)
|
|
|
|
|
|
def _browser_url(context: dict[str, Any]) -> str | None:
|
|
for endpoint in context["target_profile"].get("endpoints", []):
|
|
if endpoint.get("binding") == "cmis-browser":
|
|
return endpoint.get("url")
|
|
return None
|
|
|
|
|
|
def _repository_id(context: dict[str, Any]) -> str | None:
|
|
value = _opencmis_policy(context).get("repository_id")
|
|
return value if isinstance(value, str) else None
|
|
|
|
|
|
def _credentials_ref(context: dict[str, Any]) -> str | None:
|
|
value = context["target_profile"].get("credentials_ref")
|
|
return value if isinstance(value, str) else None
|
|
|
|
|
|
def _target_profile_dir(context: dict[str, Any]) -> str | None:
|
|
path = context["plan"].get("profile_paths", {}).get("target_profile_path")
|
|
if not isinstance(path, str) or not path:
|
|
return None
|
|
return str(Path(path).resolve().parent)
|
|
|
|
|
|
def _timeout_seconds(context: dict[str, Any]) -> float:
|
|
runtime_policy = context["assessment_profile"].get("runtime_policy", {})
|
|
opencmis_policy = _opencmis_policy(context)
|
|
configured = opencmis_policy.get("timeout_seconds", runtime_policy.get("timeout_seconds", 300))
|
|
if not isinstance(configured, (int, float)):
|
|
return 300.0
|
|
return max(1.0, float(configured))
|
|
|
|
|
|
def _write_text_artifact(run_dir: Path, artifact_dir: Path, name: str, value: str) -> str:
|
|
path = artifact_dir / name
|
|
path.write_text(value, encoding="utf-8")
|
|
return str(path.relative_to(run_dir))
|
|
|
|
|
|
def _write_json_artifact(
|
|
run_dir: Path,
|
|
artifact_dir: Path,
|
|
name: str,
|
|
value: dict[str, Any],
|
|
) -> str:
|
|
path = artifact_dir / name
|
|
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
return str(path.relative_to(run_dir))
|
|
|
|
|
|
def _parse_json(value: str) -> Any:
|
|
try:
|
|
return json.loads(value)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
|
|
|
|
def _safe_id(value: str) -> str:
|
|
return "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in value)
|
|
|
|
|
|
def _emit(value: dict[str, Any]) -> None:
|
|
print(json.dumps(value, indent=2, sort_keys=True))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|