G: EPOD acceptance tests; harness: hide and clean other runs' mid-name objects (proxy, teardown, sweep); lock leak cause (Eclipse restart during a write)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,9 @@ def activation_messages(text, is_error=False):
|
||||
|
||||
|
||||
RUN_PREFIX = re.compile(r"^Z\d[0-9A-Z]{5,6}_", re.I)
|
||||
# The model sometimes names its own helper or test class with the prefix inside the name (ZCL_Z4CGT1HJ_JOB_COST_TEST). Such objects of
|
||||
# other runs stay behind in A4H and show up in lists and searches (57 of 90 accepted trajectories saw them, 2026-10-06).
|
||||
MID_PREFIX = re.compile(r"^[A-Z]{1,5}_(Z\d[0-9A-Z]{6}_)", re.I)
|
||||
|
||||
|
||||
class BudgetExceeded(Exception):
|
||||
@@ -111,7 +114,10 @@ class ToolProxy:
|
||||
|
||||
def _foreign(self, name):
|
||||
n = (name or "").upper()
|
||||
return bool(RUN_PREFIX.match(n)) and not n.startswith(self.prefix)
|
||||
if RUN_PREFIX.match(n):
|
||||
return not n.startswith(self.prefix)
|
||||
m = MID_PREFIX.match(n)
|
||||
return bool(m) and m.group(1).upper() != self.prefix
|
||||
|
||||
def _filter(self, tool, text):
|
||||
if tool == "sap_short_dumps": # only dumps of this run: other runs (and mutants) also write dumps
|
||||
@@ -124,7 +130,7 @@ class ToolProxy:
|
||||
data["count"] = len(data["dumps"])
|
||||
return json.dumps(data)
|
||||
return text
|
||||
if tool not in ("sap_search_object", "sap_usage_references"):
|
||||
if tool not in ("sap_search_object", "sap_usage_references", "sap_inactive_objects"):
|
||||
return text
|
||||
try:
|
||||
data = json.loads(text)
|
||||
@@ -148,7 +154,9 @@ class ToolProxy:
|
||||
else:
|
||||
self.calls += 1
|
||||
name = str(args.get("objectName", "")).upper()
|
||||
if tool in WRITE_TOOLS and self._foreign(name):
|
||||
if (tool in WRITE_TOOLS or tool in ("sap_pull_source", "sap_object_structure", "sap_object_members", "sap_element_info",
|
||||
"sap_run_unit_test", "sap_check_object", "sap_syntax_check", "sap_atc_run")) \
|
||||
and self._foreign(name):
|
||||
result = (True, f"{name} is not available.")
|
||||
else:
|
||||
if tool in WRITE_TOOLS and (tool == "sap_activate" or args.get("activate", True)):
|
||||
|
||||
@@ -153,9 +153,16 @@ class Runner:
|
||||
return out
|
||||
|
||||
def _objects_with_prefix(self, mcp, prefix):
|
||||
_, text = mcp.call("sap_search_object", {"query": prefix + "*", "maxResults": 200})
|
||||
return [d for d in (_json(text) or []) if d.get("name", "").upper().startswith(prefix)
|
||||
and d.get("objectType")] # skips STOB entries of CDS entities
|
||||
"""Objects of the run: the name starts with the prefix, or contains it after a short type part (the model named its own
|
||||
helper or test class ZCL_<prefix>_..., 27 such objects were left behind before 2026-10-06)."""
|
||||
out = {}
|
||||
for q in (prefix + "*", "*_" + prefix + "*"):
|
||||
_, text = mcp.call("sap_search_object", {"query": q, "maxResults": 200})
|
||||
for d in (_json(text) or []):
|
||||
n = d.get("name", "").upper()
|
||||
if d.get("objectType") and (n.startswith(prefix) or re.match(r"^[A-Z]{1,5}_" + re.escape(prefix), n)):
|
||||
out[n] = d # skips STOB entries of CDS entities (no objectType)
|
||||
return list(out.values())
|
||||
|
||||
def _source(self, mcp, otype, name, fg=None):
|
||||
err, text = mcp.call("sap_pull_source", _obj_args(otype, name, fg))
|
||||
|
||||
89
harness/sweep.py
Normal file
89
harness/sweep.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Find (and delete) objects that harness runs left in A4H: names with a run prefix at the start (Z4CGT1HJ_X) or inside (ZCL_Z4CGT1HJ_X).
|
||||
|
||||
python3 -m harness.sweep dry run: list them by prefix
|
||||
python3 -m harness.sweep --delete delete them (ADT deletion API, dependency order); a locked object is reported, not forced
|
||||
python3 -m harness.sweep --keep-recent 60 do not touch prefixes of runs whose directory changed in the last 60 minutes (default 60)
|
||||
|
||||
Never run it while a controller or a series is writing: it can only judge by directory age.
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from .adt_client import load_env
|
||||
from .mcp_client import McpClient
|
||||
from .runner import DELETE_ORDER, delete_uris
|
||||
from .task import prefix_for
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
START = re.compile(r"^(Z\d[0-9A-Z]{6}_)", re.I)
|
||||
MID = re.compile(r"^[A-Z]{1,5}_(Z\d[0-9A-Z]{6}_)", re.I)
|
||||
|
||||
|
||||
def recent_prefixes(minutes):
|
||||
keep = set()
|
||||
for d in glob.glob(os.path.join(ROOT, "runs", "**", "*_G*_*"), recursive=True) + glob.glob(os.path.join(ROOT, "runs", "**", "*_T*_*"), recursive=True):
|
||||
b = os.path.basename(d)
|
||||
m = re.match(r"^(\d+)_([GT]\d+)_", b)
|
||||
if m and time.time() - os.path.getmtime(d) < minutes * 60:
|
||||
keep.add(prefix_for(int(m.group(1)), m.group(2)).upper())
|
||||
return keep
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--delete", action="store_true")
|
||||
ap.add_argument("--keep-recent", type=int, default=60)
|
||||
a = ap.parse_args()
|
||||
found = {}
|
||||
with McpClient() as m:
|
||||
# run prefixes are Z + a digit + 6 characters: one query per digit (a single "Z*" query is cut at 1000 results), at the start and after the type part
|
||||
for q in [f"Z{d}*" for d in "0123456789"] + [f"*_Z{d}*" for d in "0123456789"] + ["ZPROBE*", "ZTEST*"]:
|
||||
try:
|
||||
for o in json.loads(m.call("sap_search_object", {"query": q, "maxResults": 1000})[1]):
|
||||
if o.get("objectType"): # a STOB entry of a CDS view has the same name and no objectType: skip it
|
||||
found[(o["name"], o["objectType"])] = o
|
||||
except ValueError:
|
||||
pass
|
||||
keep = recent_prefixes(a.keep_recent)
|
||||
by_prefix = {}
|
||||
for (n, _t), o in found.items():
|
||||
mm = START.match(n.upper()) or MID.match(n.upper())
|
||||
if not mm or not o.get("objectType"):
|
||||
continue
|
||||
pre = mm.group(1).upper()
|
||||
if len(pre) != 9 or not pre[1].isdigit():
|
||||
continue
|
||||
if pre in keep:
|
||||
continue
|
||||
by_prefix.setdefault(pre, []).append(o)
|
||||
total = sum(len(v) for v in by_prefix.values())
|
||||
print(f"{total} objects of {len(by_prefix)} run prefixes ({len(keep)} recent prefixes kept)")
|
||||
for pre, objs in sorted(by_prefix.items()):
|
||||
print(" ", pre, [o["name"] for o in objs][:6])
|
||||
json.dump({p: [o["name"] for o in v] for p, v in by_prefix.items()}, open(os.path.join(ROOT, "runs", "sweep.json"), "w"), indent=1)
|
||||
if not a.delete:
|
||||
return
|
||||
objs = sorted([o for v in by_prefix.values() for o in v],
|
||||
key=lambda o: DELETE_ORDER.index(o["objectType"]) if o["objectType"] in DELETE_ORDER else 99)
|
||||
done = failed = 0
|
||||
for i in range(0, len(objs), 10):
|
||||
uris = []
|
||||
for o in objs[i:i + 10]:
|
||||
uris.append(o["uri"])
|
||||
res = delete_uris(uris)
|
||||
for u, v in res.items():
|
||||
if v["deleted"]:
|
||||
done += 1
|
||||
else:
|
||||
failed += 1
|
||||
print("not deleted:", u.split("/")[-1], v["msg"])
|
||||
print(f"deleted {done}, not deleted {failed}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user