Log analysis functionality for self-assessment
This commit is contained in:
parent
97a4a1fa37
commit
cd43c7cfec
12 changed files with 1573 additions and 2 deletions
99
src/open_cmis_tck/archive.py
Normal file
99
src/open_cmis_tck/archive.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""Durable archive helpers for guide-board OpenCMIS assessment runs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
MANIFEST_NAME = "archive-manifest.json"
|
||||
|
||||
|
||||
def archive_run(
|
||||
run_dir: Path,
|
||||
archive_root: Path,
|
||||
*,
|
||||
target_id: str | None = None,
|
||||
archive_name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Copy a guide-board run directory into a durable archive path."""
|
||||
|
||||
source = run_dir.resolve()
|
||||
if not source.exists() or not source.is_dir():
|
||||
raise FileNotFoundError(f"run directory does not exist: {source}")
|
||||
|
||||
run_metadata = _load_json(source / "run.json")
|
||||
target_profile = _load_json(source / "target-profile.snapshot.json")
|
||||
resolved_target = target_id or target_profile.get("id") or run_metadata.get("target_profile_ref") or "unknown-target"
|
||||
resolved_run_id = archive_name or run_metadata.get("id") or source.name
|
||||
archive_dir = archive_root.resolve() / _safe_segment(str(resolved_target)) / _safe_segment(str(resolved_run_id))
|
||||
if archive_dir.exists():
|
||||
raise FileExistsError(f"archive directory already exists: {archive_dir}")
|
||||
|
||||
archive_dir.mkdir(parents=True)
|
||||
copied_files = _copy_tree(source, archive_dir)
|
||||
files = [_file_manifest_entry(archive_dir, relative_path) for relative_path in copied_files]
|
||||
manifest = {
|
||||
"id": f"opencmis-run-archive:{resolved_run_id}",
|
||||
"created_at": _now(),
|
||||
"source_run_dir": str(source),
|
||||
"archive_dir": str(archive_dir),
|
||||
"run_id": run_metadata.get("id") or source.name,
|
||||
"target_profile_ref": run_metadata.get("target_profile_ref") or target_profile.get("id"),
|
||||
"assessment_profile_ref": run_metadata.get("assessment_profile_ref"),
|
||||
"file_count": len(files),
|
||||
"total_bytes": sum(item["size_bytes"] for item in files),
|
||||
"files": files,
|
||||
}
|
||||
(archive_dir / MANIFEST_NAME).write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def _copy_tree(source: Path, destination: Path) -> list[Path]:
|
||||
copied: list[Path] = []
|
||||
for path in sorted(source.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative_path = path.relative_to(source)
|
||||
target = destination / relative_path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, target)
|
||||
copied.append(relative_path)
|
||||
return copied
|
||||
|
||||
|
||||
def _file_manifest_entry(root: Path, relative_path: Path) -> dict[str, Any]:
|
||||
path = root / relative_path
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return {
|
||||
"path": relative_path.as_posix(),
|
||||
"size_bytes": path.stat().st_size,
|
||||
"sha256": digest.hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def _safe_segment(value: str) -> str:
|
||||
safe = "".join(char if char.isalnum() or char in {"-", "_", "."} else "-" for char in value.strip())
|
||||
safe = "-".join(part for part in safe.split("-") if part)
|
||||
return safe or "unknown"
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
562
src/open_cmis_tck/log_review.py
Normal file
562
src/open_cmis_tck/log_review.py
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
"""OpenCMIS TCK run log review and warning policy classification."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from ipaddress import ip_address
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
DEFAULT_POLICY_PATH = Path(__file__).resolve().parents[2] / "profiles" / "expectations" / "opencmis-warning-policy.json"
|
||||
ERROR_TERMS = ("warn", "warning", "error", "severe", "exception", "caused by", "failed")
|
||||
|
||||
SKIP_BOUNDARY_RULES = [
|
||||
{
|
||||
"id": "relationship-type-not-creatable",
|
||||
"message_contains": "Relationship type 'cmis:relationship' is not creatable",
|
||||
"required_capability": "cmis.relationships",
|
||||
"classification": "declared_type_creatability_boundary",
|
||||
},
|
||||
{
|
||||
"id": "policy-type-not-creatable",
|
||||
"message_contains": "Policy type 'cmis:policy' is not creatable",
|
||||
"required_capability": "cmis.policy-mutation",
|
||||
"classification": "declared_type_creatability_boundary",
|
||||
},
|
||||
{
|
||||
"id": "item-type-not-creatable",
|
||||
"message_contains": "Item type 'cmis:item' is not creatable",
|
||||
"required_capability": "cmis.item-services",
|
||||
"classification": "declared_type_creatability_boundary",
|
||||
},
|
||||
{
|
||||
"id": "document-subtype-not-creatable",
|
||||
"message_contains": "Test document type doesn't allow creating a sub-type",
|
||||
"required_capability": "cmis.type-mutability",
|
||||
"classification": "declared_type_mutability_boundary",
|
||||
},
|
||||
{
|
||||
"id": "folder-name-change-not-supported",
|
||||
"message_contains": "Folder name can't be changed",
|
||||
"required_capability": "cmis.folder-name-mutation",
|
||||
"classification": "declared_folder_mutation_boundary",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def build_log_review(
|
||||
run_dir: Path,
|
||||
*,
|
||||
policy_path: Path | None = None,
|
||||
previous_run_dir: Path | None = None,
|
||||
server_log_paths: list[Path] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a compact review of warnings, stderr, skips, and hard errors."""
|
||||
|
||||
run = _load_run(run_dir)
|
||||
policy = _load_policy(policy_path)
|
||||
previous = _load_run(previous_run_dir) if previous_run_dir is not None else None
|
||||
warning_reviews = [
|
||||
_review_warning(case, run, policy)
|
||||
for case in run["cases"]
|
||||
if case.get("status") == "warning"
|
||||
]
|
||||
hard_errors = [
|
||||
_case_summary(case, run)
|
||||
for case in run["cases"]
|
||||
if case.get("status") in {"fail", "infrastructure_error", "blocked"}
|
||||
]
|
||||
skip_reviews = [
|
||||
_review_skip(case, run)
|
||||
for case in run["cases"]
|
||||
if case.get("status") == "skipped"
|
||||
]
|
||||
stderr_files = _collect_stderr_files(run["run_dir"])
|
||||
server_log_findings = _scan_server_logs(server_log_paths or [], run["run_dir"])
|
||||
previous_warning_signatures = {
|
||||
_case_signature(case)
|
||||
for case in (previous or {}).get("cases", [])
|
||||
if case.get("status") == "warning"
|
||||
}
|
||||
current_warning_signatures = {
|
||||
_case_signature(case)
|
||||
for case in run["cases"]
|
||||
if case.get("status") == "warning"
|
||||
}
|
||||
closed_warnings = [
|
||||
_case_summary(case, previous or {})
|
||||
for case in (previous or {}).get("cases", [])
|
||||
if case.get("status") == "warning"
|
||||
and _case_signature(case) not in current_warning_signatures
|
||||
]
|
||||
new_warnings = [
|
||||
item
|
||||
for item in warning_reviews
|
||||
if item["signature"] not in previous_warning_signatures
|
||||
]
|
||||
unexpected_findings = [
|
||||
finding
|
||||
for finding in run["findings"]
|
||||
if not finding.get("expected")
|
||||
]
|
||||
unaccepted_warnings = [item for item in warning_reviews if not item["accepted"]]
|
||||
unexpected_skips = [item for item in skip_reviews if not item["expected"]]
|
||||
nonempty_stderr = [item for item in stderr_files if item["size_bytes"] > 0]
|
||||
|
||||
status = _review_status(
|
||||
hard_errors=hard_errors,
|
||||
unexpected_findings=unexpected_findings,
|
||||
unaccepted_warnings=unaccepted_warnings,
|
||||
unexpected_skips=unexpected_skips,
|
||||
nonempty_stderr=nonempty_stderr,
|
||||
warning_reviews=warning_reviews,
|
||||
skip_reviews=skip_reviews,
|
||||
)
|
||||
summary = {
|
||||
"status": status,
|
||||
"case_count": len(run["cases"]),
|
||||
"case_status_counts": dict(sorted(Counter(case.get("status", "unknown") for case in run["cases"]).items())),
|
||||
"warning_count": len(warning_reviews),
|
||||
"accepted_warnings": sum(1 for item in warning_reviews if item["accepted"]),
|
||||
"unaccepted_warnings": len(unaccepted_warnings),
|
||||
"new_warnings": len(new_warnings),
|
||||
"closed_warnings": len(closed_warnings),
|
||||
"hard_error_count": len(hard_errors),
|
||||
"stderr_files": len(stderr_files),
|
||||
"nonempty_stderr_files": len(nonempty_stderr),
|
||||
"skipped_cases": len(skip_reviews),
|
||||
"expected_skips": sum(1 for item in skip_reviews if item["expected"]),
|
||||
"unexpected_skips": len(unexpected_skips),
|
||||
"unexpected_findings": len(unexpected_findings),
|
||||
"server_log_findings": len(server_log_findings),
|
||||
}
|
||||
return {
|
||||
"id": f"opencmis-log-review:{run['run_id']}",
|
||||
"created_at": _now(),
|
||||
"run": {
|
||||
"run_id": run["run_id"],
|
||||
"run_dir": str(run["run_dir"]),
|
||||
"target_profile_ref": run["target_profile_ref"],
|
||||
"assessment_profile_ref": run["assessment_profile_ref"],
|
||||
"target_environment": run["target_environment"],
|
||||
"browser_binding_url": run["browser_binding_url"],
|
||||
"declared_capabilities": run["declared_capabilities"],
|
||||
},
|
||||
"policy": {
|
||||
"id": policy.get("id"),
|
||||
"path": str(policy.get("_path")) if policy.get("_path") else None,
|
||||
},
|
||||
"summary": summary,
|
||||
"warnings": warning_reviews,
|
||||
"new_warnings": new_warnings,
|
||||
"closed_warnings": closed_warnings,
|
||||
"hard_errors": hard_errors,
|
||||
"stderr": stderr_files,
|
||||
"skips": skip_reviews,
|
||||
"unexpected_findings": unexpected_findings,
|
||||
"server_log_findings": server_log_findings,
|
||||
"certification_boundary": "This log review supports preparation and operational readiness only; it does not certify CMIS conformance.",
|
||||
}
|
||||
|
||||
|
||||
def write_log_review(
|
||||
run_dir: Path,
|
||||
*,
|
||||
output_dir: Path | None = None,
|
||||
policy_path: Path | None = None,
|
||||
previous_run_dir: Path | None = None,
|
||||
server_log_paths: list[Path] | None = None,
|
||||
) -> dict[str, str]:
|
||||
output = output_dir or run_dir / "reports"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
review = build_log_review(
|
||||
run_dir,
|
||||
policy_path=policy_path,
|
||||
previous_run_dir=previous_run_dir,
|
||||
server_log_paths=server_log_paths,
|
||||
)
|
||||
json_path = output / "opencmis-log-review.json"
|
||||
markdown_path = output / "opencmis-log-review.md"
|
||||
json_path.write_text(json.dumps(review, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
markdown_path.write_text(markdown_log_review(review), encoding="utf-8")
|
||||
return {
|
||||
"status": "written",
|
||||
"json": str(json_path),
|
||||
"markdown": str(markdown_path),
|
||||
}
|
||||
|
||||
|
||||
def markdown_log_review(review: dict[str, Any]) -> str:
|
||||
summary = review["summary"]
|
||||
lines = [
|
||||
f"# OpenCMIS Log Review: {review['run']['run_id']}",
|
||||
"",
|
||||
f"Target: {review['run']['target_profile_ref']}",
|
||||
f"Assessment: {review['run']['assessment_profile_ref']}",
|
||||
f"Status: {summary['status']}",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
f"- cases: {summary['case_count']}",
|
||||
f"- warnings: {summary['warning_count']} ({summary['accepted_warnings']} accepted, {summary['unaccepted_warnings']} unaccepted)",
|
||||
f"- hard errors: {summary['hard_error_count']}",
|
||||
f"- stderr files: {summary['nonempty_stderr_files']} non-empty / {summary['stderr_files']} scanned",
|
||||
f"- skipped cases: {summary['skipped_cases']} ({summary['expected_skips']} expected, {summary['unexpected_skips']} needs review)",
|
||||
f"- unexpected findings: {summary['unexpected_findings']}",
|
||||
f"- server log findings: {summary['server_log_findings']}",
|
||||
"",
|
||||
"## Warnings",
|
||||
"",
|
||||
]
|
||||
if review["warnings"]:
|
||||
for item in review["warnings"]:
|
||||
lines.extend(
|
||||
[
|
||||
f"- {item['severity']}: {item['classification']} ({item.get('policy_id') or 'no-policy'})",
|
||||
f" {item['selected_check_group']} / {item['test_name']}: {item['message']}",
|
||||
]
|
||||
)
|
||||
else:
|
||||
lines.append("- none")
|
||||
lines.extend(["", "## Skips", ""])
|
||||
if review["skips"]:
|
||||
for item in review["skips"]:
|
||||
expected = "expected" if item["expected"] else "needs review"
|
||||
lines.append(
|
||||
f"- {expected}: {item['classification']} / {item['selected_check_group']} / {item['test_name']}: {item['message']}"
|
||||
)
|
||||
else:
|
||||
lines.append("- none")
|
||||
lines.extend(["", "## Hard Errors And Stderr", ""])
|
||||
if review["hard_errors"]:
|
||||
for item in review["hard_errors"]:
|
||||
lines.append(f"- {item['status']}: {item['selected_check_group']} / {item['test_name']}: {item['message']}")
|
||||
else:
|
||||
lines.append("- no hard OpenCMIS case errors")
|
||||
for item in review["stderr"]:
|
||||
if item["size_bytes"] > 0:
|
||||
lines.append(f"- non-empty stderr: {item['path']} ({item['size_bytes']} bytes)")
|
||||
lines.extend(["", "## Closed Warnings", ""])
|
||||
if review["closed_warnings"]:
|
||||
for item in review["closed_warnings"]:
|
||||
lines.append(f"- {item['selected_check_group']} / {item['test_name']}: {item['message']}")
|
||||
else:
|
||||
lines.append("- none")
|
||||
lines.extend(["", "## Boundary", "", review["certification_boundary"], ""])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _load_run(run_dir: Path | None) -> dict[str, Any]:
|
||||
if run_dir is None:
|
||||
return {}
|
||||
resolved = run_dir.resolve()
|
||||
run_metadata = _load_json(resolved / "run.json")
|
||||
target_profile = _load_json(resolved / "target-profile.snapshot.json")
|
||||
assessment_profile = _load_json(resolved / "assessment-profile.snapshot.json")
|
||||
evidence = _load_json(resolved / "normalized" / "evidence.json").get("evidence", [])
|
||||
findings = _load_json(resolved / "normalized" / "findings.json").get("findings", [])
|
||||
endpoint = _browser_binding_url(target_profile, evidence)
|
||||
cases = _cases_from_evidence(evidence)
|
||||
return {
|
||||
"run_dir": resolved,
|
||||
"run_id": run_metadata.get("id") or resolved.name,
|
||||
"target_profile_ref": run_metadata.get("target_profile_ref") or target_profile.get("id"),
|
||||
"assessment_profile_ref": run_metadata.get("assessment_profile_ref") or assessment_profile.get("id"),
|
||||
"target_environment": target_profile.get("environment"),
|
||||
"target_profile": target_profile,
|
||||
"assessment_profile": assessment_profile,
|
||||
"browser_binding_url": endpoint,
|
||||
"declared_capabilities": sorted(target_profile.get("declared_capabilities") or []),
|
||||
"cases": cases,
|
||||
"findings": findings if isinstance(findings, list) else [],
|
||||
}
|
||||
|
||||
|
||||
def _cases_from_evidence(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
cases: list[dict[str, Any]] = []
|
||||
for item in evidence:
|
||||
facts = item.get("facts") or {}
|
||||
for case in facts.get("cases") or []:
|
||||
if not isinstance(case, dict):
|
||||
continue
|
||||
enriched = dict(case)
|
||||
enriched.setdefault("selected_check_group", facts.get("selected_check_group") or facts.get("check_group"))
|
||||
enriched.setdefault("check_id", item.get("check_id"))
|
||||
enriched.setdefault("evidence_id", item.get("id"))
|
||||
cases.append(enriched)
|
||||
return cases
|
||||
|
||||
|
||||
def _review_warning(case: dict[str, Any], run: dict[str, Any], policy: dict[str, Any]) -> dict[str, Any]:
|
||||
policy_item = _matching_warning_policy(case, policy)
|
||||
accepted = _warning_is_accepted(policy_item, run) if policy_item else False
|
||||
if not policy_item:
|
||||
classification = "unclassified_warning"
|
||||
severity = "warning"
|
||||
reason = "No warning policy matched this OpenCMIS warning."
|
||||
policy_id = None
|
||||
elif accepted:
|
||||
classification = policy_item.get("classification", "accepted_warning")
|
||||
severity = policy_item.get("severity", "info")
|
||||
reason = policy_item.get("reason", "")
|
||||
policy_id = policy_item.get("id")
|
||||
else:
|
||||
classification = policy_item.get("unaccepted_classification", "unaccepted_warning")
|
||||
severity = policy_item.get("unaccepted_severity", "warning")
|
||||
reason = policy_item.get("unaccepted_reason", policy_item.get("reason", "Warning policy matched but acceptance conditions were not met."))
|
||||
policy_id = policy_item.get("id")
|
||||
summary = _case_summary(case, run)
|
||||
summary.update(
|
||||
{
|
||||
"accepted": accepted,
|
||||
"classification": classification,
|
||||
"severity": severity,
|
||||
"reason": reason,
|
||||
"policy_id": policy_id,
|
||||
"signature": _case_signature(case),
|
||||
}
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def _matching_warning_policy(case: dict[str, Any], policy: dict[str, Any]) -> dict[str, Any] | None:
|
||||
for item in policy.get("warning_policies") or []:
|
||||
if _policy_matches_case(item.get("match") or {}, case):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def _policy_matches_case(match: dict[str, Any], case: dict[str, Any]) -> bool:
|
||||
message = str(case.get("message") or "")
|
||||
if match.get("message_contains") and str(match["message_contains"]) not in message:
|
||||
return False
|
||||
if match.get("test_name_contains") and str(match["test_name_contains"]) not in str(case.get("test_name") or ""):
|
||||
return False
|
||||
if match.get("selected_check_group") and match["selected_check_group"] != case.get("selected_check_group"):
|
||||
return False
|
||||
source_match = match.get("source_location") or {}
|
||||
source_location = case.get("source_location") or {}
|
||||
if source_match.get("file") and source_match["file"] != source_location.get("file"):
|
||||
return False
|
||||
if source_match.get("line") and source_match["line"] != source_location.get("line"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _warning_is_accepted(policy_item: dict[str, Any] | None, run: dict[str, Any]) -> bool:
|
||||
if not policy_item:
|
||||
return False
|
||||
accepted_when = policy_item.get("accepted_when") or {}
|
||||
target_refs = accepted_when.get("target_profile_refs")
|
||||
if target_refs and run.get("target_profile_ref") not in target_refs:
|
||||
return False
|
||||
environments = accepted_when.get("environments")
|
||||
if environments and run.get("target_environment") not in environments:
|
||||
return False
|
||||
scheme = accepted_when.get("scheme")
|
||||
parsed = urlparse(str(run.get("browser_binding_url") or ""))
|
||||
if scheme and parsed.scheme != scheme:
|
||||
return False
|
||||
if accepted_when.get("host_scope") == "loopback" and not _is_loopback_host(parsed.hostname):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _review_skip(case: dict[str, Any], run: dict[str, Any]) -> dict[str, Any]:
|
||||
message = str(case.get("message") or "")
|
||||
declared = set(run.get("declared_capabilities") or [])
|
||||
for rule in SKIP_BOUNDARY_RULES:
|
||||
if rule["message_contains"] not in message:
|
||||
continue
|
||||
required = rule["required_capability"]
|
||||
expected = required not in declared
|
||||
classification = (
|
||||
rule["classification"]
|
||||
if expected
|
||||
else "advertised_capability_not_exercised"
|
||||
)
|
||||
summary = _case_summary(case, run)
|
||||
summary.update(
|
||||
{
|
||||
"expected": expected,
|
||||
"classification": classification,
|
||||
"required_capability": required,
|
||||
"rule_id": rule["id"],
|
||||
}
|
||||
)
|
||||
return summary
|
||||
summary = _case_summary(case, run)
|
||||
summary.update(
|
||||
{
|
||||
"expected": False,
|
||||
"classification": "unclassified_skip",
|
||||
"required_capability": None,
|
||||
"rule_id": None,
|
||||
}
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def _case_summary(case: dict[str, Any], run: dict[str, Any]) -> dict[str, Any]:
|
||||
source_location = case.get("source_location") or {}
|
||||
return {
|
||||
"id": case.get("id"),
|
||||
"status": case.get("status"),
|
||||
"status_native": case.get("status_native"),
|
||||
"selected_check_group": case.get("selected_check_group"),
|
||||
"group_name": case.get("group_name"),
|
||||
"test_name": case.get("test_name"),
|
||||
"message": case.get("message"),
|
||||
"source_location": source_location,
|
||||
"evidence_id": case.get("evidence_id"),
|
||||
"run_id": run.get("run_id"),
|
||||
}
|
||||
|
||||
|
||||
def _case_signature(case: dict[str, Any]) -> str:
|
||||
source = case.get("source_location") or {}
|
||||
parts = [
|
||||
str(case.get("selected_check_group") or ""),
|
||||
str(case.get("test_name") or ""),
|
||||
str(case.get("message") or ""),
|
||||
str(source.get("file") or ""),
|
||||
str(source.get("line") or ""),
|
||||
]
|
||||
return "|".join(parts)
|
||||
|
||||
|
||||
def _collect_stderr_files(run_dir: Path) -> list[dict[str, Any]]:
|
||||
patterns = [
|
||||
"artifacts/open-cmis-tck/tck/**/console-runner-stderr.txt",
|
||||
"artifacts/open-cmis-tck/tck/**/stderr.log",
|
||||
]
|
||||
files: list[Path] = []
|
||||
seen: set[Path] = set()
|
||||
for pattern in patterns:
|
||||
for path in sorted(run_dir.glob(pattern)):
|
||||
resolved = path.resolve()
|
||||
if path.is_file() and resolved not in seen:
|
||||
files.append(path)
|
||||
seen.add(resolved)
|
||||
return [
|
||||
{
|
||||
"path": _relative(path, run_dir),
|
||||
"size_bytes": path.stat().st_size,
|
||||
"excerpt": _excerpt(path) if path.stat().st_size else "",
|
||||
}
|
||||
for path in files
|
||||
]
|
||||
|
||||
|
||||
def _scan_server_logs(paths: list[Path], run_dir: Path) -> list[dict[str, Any]]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
for root in paths:
|
||||
for path in _expand_log_paths(root):
|
||||
for line_number, line in _matching_log_lines(path):
|
||||
findings.append(
|
||||
{
|
||||
"path": _relative(path, run_dir),
|
||||
"line": line_number,
|
||||
"message": line.strip(),
|
||||
}
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _expand_log_paths(path: Path) -> list[Path]:
|
||||
if path.is_file():
|
||||
return [path]
|
||||
if path.is_dir():
|
||||
return sorted(item for item in path.rglob("*") if item.is_file())
|
||||
return []
|
||||
|
||||
|
||||
def _matching_log_lines(path: Path) -> list[tuple[int, str]]:
|
||||
matches: list[tuple[int, str]] = []
|
||||
try:
|
||||
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||
for index, line in enumerate(handle, start=1):
|
||||
lowered = line.lower()
|
||||
if any(term in lowered for term in ERROR_TERMS):
|
||||
matches.append((index, line))
|
||||
if len(matches) >= 50:
|
||||
break
|
||||
except OSError:
|
||||
return []
|
||||
return matches
|
||||
|
||||
|
||||
def _review_status(
|
||||
*,
|
||||
hard_errors: list[dict[str, Any]],
|
||||
unexpected_findings: list[dict[str, Any]],
|
||||
unaccepted_warnings: list[dict[str, Any]],
|
||||
unexpected_skips: list[dict[str, Any]],
|
||||
nonempty_stderr: list[dict[str, Any]],
|
||||
warning_reviews: list[dict[str, Any]],
|
||||
skip_reviews: list[dict[str, Any]],
|
||||
) -> str:
|
||||
if hard_errors or unexpected_findings or unaccepted_warnings or unexpected_skips or nonempty_stderr:
|
||||
return "review_required"
|
||||
if warning_reviews or skip_reviews:
|
||||
return "pass_with_review_notes"
|
||||
return "pass"
|
||||
|
||||
|
||||
def _browser_binding_url(target_profile: dict[str, Any], evidence: list[dict[str, Any]]) -> str | None:
|
||||
for endpoint in target_profile.get("endpoints") or []:
|
||||
if endpoint.get("binding") == "cmis-browser":
|
||||
return endpoint.get("url")
|
||||
for item in evidence:
|
||||
facts = item.get("facts") or {}
|
||||
value = facts.get("browser_binding_url") or facts.get("url")
|
||||
if value:
|
||||
return str(value)
|
||||
return None
|
||||
|
||||
|
||||
def _load_policy(policy_path: Path | None) -> dict[str, Any]:
|
||||
path = policy_path or DEFAULT_POLICY_PATH
|
||||
if not path.exists():
|
||||
return {"id": "none", "warning_policies": [], "_path": None}
|
||||
policy = _load_json(path)
|
||||
policy["_path"] = path
|
||||
return policy
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def _is_loopback_host(host: str | None) -> bool:
|
||||
if not host:
|
||||
return False
|
||||
if host in {"localhost"}:
|
||||
return True
|
||||
try:
|
||||
return ip_address(host).is_loopback
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _excerpt(path: Path, limit: int = 500) -> str:
|
||||
return path.read_text(encoding="utf-8", errors="replace")[:limit]
|
||||
|
||||
|
||||
def _relative(path: Path, root: Path) -> str:
|
||||
try:
|
||||
return str(path.resolve().relative_to(root.resolve()))
|
||||
except ValueError:
|
||||
return str(path.resolve())
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
Loading…
Add table
Add a link
Reference in a new issue