From 1c6c85492e4971c3d12dbed664a0a7f0fe65fc15 Mon Sep 17 00:00:00 2001 From: Kral Date: Fri, 2 Oct 2026 21:42:51 +0200 Subject: [PATCH] Harness: runner, generator, tasks T01 T13 T14 T15, CLAUDE.md --- .env.example | 7 + .gitignore | 5 + CLAUDE.md | 90 ++++ harness/__init__.py | 0 harness/adt_client.py | 72 ++++ harness/agents.py | 139 ++++++ harness/cli.py | 89 ++++ harness/generator.py | 242 +++++++++++ harness/ledger.py | 42 ++ harness/mcp_client.py | 96 +++++ harness/pilot.py | 48 +++ harness/proxy.py | 80 ++++ harness/runner.py | 394 ++++++++++++++++++ harness/task.py | 62 +++ package-lock.json | 31 ++ package.json | 16 + tasks/T01/hidden/t01_hidden.clas.abap | 93 +++++ tasks/T01/reference/iban_validator.clas.abap | 84 ++++ .../reference/iban_validator.testclasses.abap | 48 +++ tasks/T01/seed/if_iban_check.intf.abap | 15 + tasks/T01/spec.md | 45 ++ tasks/T01/task.json | 50 +++ tasks/T13/hidden/t13_hidden.clas.abap | 70 ++++ tasks/T13/reference/evt_fee.func.abap | 30 ++ tasks/T13/reference/t13_test.clas.abap | 52 +++ tasks/T13/seed/evt_sample.func.abap | 14 + tasks/T13/spec.md | 35 ++ tasks/T13/task.json | 80 ++++ tasks/T14/hidden/t14_hidden.clas.abap | 72 ++++ tasks/T14/reference/sales_sum.prog.abap | 89 ++++ tasks/T14/seed/sale.tabl.asabap | 12 + tasks/T14/seed/t14_seed.clas.abap | 17 + tasks/T14/spec.md | 33 ++ tasks/T14/task.json | 18 + tasks/T15/hidden/t15_hidden.clas.abap | 64 +++ tasks/T15/reference/i_order_total.ddls.asddls | 14 + tasks/T15/reference/t15_test.clas.abap | 38 ++ tasks/T15/seed/itm.tabl.asabap | 13 + tasks/T15/seed/ord.tabl.asabap | 11 + tasks/T15/spec.md | 33 ++ tasks/T15/task.json | 22 + tasks_gen/eval/G0001/generation.json | 24 ++ .../eval/G0001/hidden/t07_hidden.clas.abap | 335 +++++++++++++++ .../G0001/reference/tour_planner.clas.abap | 143 +++++++ .../reference/tour_planner.testclasses.abap | 93 +++++ .../eval/G0001/seed/if_tour_planner.intf.abap | 55 +++ tasks_gen/eval/G0001/spec.md | 118 ++++++ tasks_gen/eval/G0001/task.json | 41 ++ tasks_gen/eval/G0002/generation.json | 25 ++ .../eval/G0002/hidden/t02_hidden.clas.abap | 132 ++++++ .../reference/delivery_schedule.clas.abap | 104 +++++ .../delivery_schedule.testclasses.abap | 72 ++++ tasks_gen/eval/G0002/spec.md | 84 ++++ tasks_gen/eval/G0002/task.json | 41 ++ 54 files changed, 3732 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 harness/__init__.py create mode 100644 harness/adt_client.py create mode 100644 harness/agents.py create mode 100644 harness/cli.py create mode 100644 harness/generator.py create mode 100644 harness/ledger.py create mode 100644 harness/mcp_client.py create mode 100644 harness/pilot.py create mode 100644 harness/proxy.py create mode 100644 harness/runner.py create mode 100644 harness/task.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tasks/T01/hidden/t01_hidden.clas.abap create mode 100644 tasks/T01/reference/iban_validator.clas.abap create mode 100644 tasks/T01/reference/iban_validator.testclasses.abap create mode 100644 tasks/T01/seed/if_iban_check.intf.abap create mode 100644 tasks/T01/spec.md create mode 100644 tasks/T01/task.json create mode 100644 tasks/T13/hidden/t13_hidden.clas.abap create mode 100644 tasks/T13/reference/evt_fee.func.abap create mode 100644 tasks/T13/reference/t13_test.clas.abap create mode 100644 tasks/T13/seed/evt_sample.func.abap create mode 100644 tasks/T13/spec.md create mode 100644 tasks/T13/task.json create mode 100644 tasks/T14/hidden/t14_hidden.clas.abap create mode 100644 tasks/T14/reference/sales_sum.prog.abap create mode 100644 tasks/T14/seed/sale.tabl.asabap create mode 100644 tasks/T14/seed/t14_seed.clas.abap create mode 100644 tasks/T14/spec.md create mode 100644 tasks/T14/task.json create mode 100644 tasks/T15/hidden/t15_hidden.clas.abap create mode 100644 tasks/T15/reference/i_order_total.ddls.asddls create mode 100644 tasks/T15/reference/t15_test.clas.abap create mode 100644 tasks/T15/seed/itm.tabl.asabap create mode 100644 tasks/T15/seed/ord.tabl.asabap create mode 100644 tasks/T15/spec.md create mode 100644 tasks/T15/task.json create mode 100644 tasks_gen/eval/G0001/generation.json create mode 100644 tasks_gen/eval/G0001/hidden/t07_hidden.clas.abap create mode 100644 tasks_gen/eval/G0001/reference/tour_planner.clas.abap create mode 100644 tasks_gen/eval/G0001/reference/tour_planner.testclasses.abap create mode 100644 tasks_gen/eval/G0001/seed/if_tour_planner.intf.abap create mode 100644 tasks_gen/eval/G0001/spec.md create mode 100644 tasks_gen/eval/G0001/task.json create mode 100644 tasks_gen/eval/G0002/generation.json create mode 100644 tasks_gen/eval/G0002/hidden/t02_hidden.clas.abap create mode 100644 tasks_gen/eval/G0002/reference/delivery_schedule.clas.abap create mode 100644 tasks_gen/eval/G0002/reference/delivery_schedule.testclasses.abap create mode 100644 tasks_gen/eval/G0002/spec.md create mode 100644 tasks_gen/eval/G0002/task.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b811c6e --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +MCP_URL=http://127.0.0.1:3000/mcp +MCP_TOKEN= +A4H_URL=http://192.168.178.40:50000 +A4H_USER= +A4H_PASSWORD= +A4H_CLIENT=001 +LLM_BASE_URL=http://127.0.0.1:11434/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f9a38a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules/ +runs/ +__pycache__/ +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33d5b08 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# ABAP LLM harness — instructions for Claude Code + +Talk to the user (Kral) in Turkish. Keep answers short; remove words that add no value. +When you write English text (specs, prompts, docs for the model), use ASD-STE100 Simplified Technical English. +Ask before an action that uses much cloud budget. + +## 1. Project + +- Goal: train an open-weight ABAP model (Apache 2.0). Base model: Apache 2.0 or MIT only. +- Role of the model: technical ABAP consultant. It writes ABAP, knows Clean ABAP and what to use how. + No SAP module knowledge; the functional side (spec or the /sapplan planner) gives it. +- Contract of the model: "ABAP task as text + generic ABAP MCP interface". + The plan format (plan-agent.md) gives the best result, but it is not mandatory. +- Training data: never Claude output. Eval tasks never go into training data. +- Read first: `docs/yol-haritasi.md` (roadmap, current position) and `docs/faz1-tasarim.md` (design). + When a decision or a result changes, update these files. + +## 2. Environment (this Mac mini) + +- A4H: Docker container `a4h`, HTTP `localhost:50000`, client 001, SAP_BASIS 816 SP01. +- MCP server (EPOD) runs inside ADT (Eclipse) at `127.0.0.1:3000`. Token: `.env` (`MCP_TOKEN`). + System name `A4H`, mode write. +- Ollama `127.0.0.1:11434`: + - `qwen3.8-27b-32k` (local, num_ctx 32k, ~18 GB). Slow: 20–40 min per task. + - `deepseek-v4.1-flash:cloud` (Ollama cloud, MIT): teacher and task author. ~0.05–0.08 USD per run. +- Python 3.9 (system), Node; abaplint in `node_modules`. + +## 3. Rules + +- Local model: one request at a time. No time limit (tool-call budget limits a run). +- Do not run local and cloud model runs at the same time: Ollama can queue them together. +- A4H is small: MCP limit is 8 sessions (by design). The server shares ONE RFC connection between + sessions; a parallel call gets "[LOCK] Concurrent call detected". `mcp_client.py` retries this. + Keep parallel runs low (max 3). DDIC activation during setup is sensitive to parallel runs. +- Budget: `runs/ledger.jsonl` (list prices, upper bound). `.env`: `BUDGET_LIMIT_USD`, `BUDGET_CYCLE_START`. + Ollama usage resets on 12 October 2026, then +60 USD per month. At the limit, stop cloud work. +- Objects: package `$TMP` only. Prefix `Z` + run (4 chars base36) + task (3 chars base36) + `_` + (`harness/task.py`). Teardown after each run with the ADT deletion API (`adt_client.py`, credentials in `.env`). + Delete only objects with a run prefix. Clean up probe objects. +- The model never sees delete/teardown. The proxy (`proxy.py`) has a tool whitelist and hides other runs' objects. + +## 4. Layout + +- `harness/mcp_client.py` MCP client (retry on 404 and on LOCK). +- `harness/adt_client.py` ADT deletion API. +- `harness/proxy.py` tool whitelist, budget, prefix filter, trajectory log. +- `harness/agents.py` oracle, null, llm (OpenAI-compatible; retries; ledger). +- `harness/runner.py` setup → agent → gates G1–G6 → hidden tests → own tests → ATC → abaplint → score → teardown. +- `harness/generator.py` task generator (cloud model writes a bundle; validation oracle = 100, null = 0; max 3 repairs). +- `harness/pilot.py` pilot list (20 tasks G0002–G0021). +- `harness/ledger.py` cost ledger and budget guard. +- `harness/cli.py` run, rescore, teardown, teardown-all, cleanup-list. +- `tasks/` hand-written tasks T01 (CLAS), T13 (FUNC), T14 (PROG + ALV), T15 (CDS). Oracle 100, null 0 for all. +- `tasks_gen/eval/`, `tasks_gen/train/` generated tasks (separate pools). +- `runs/` run results (git ignores it). `runs/_archive_v1` old runs. + +## 5. Commands + +``` +python3 -m harness.cli run T01 --agent oracle|null|llm [--model NAME] --run N +python3 -m harness.cli rescore runs/ +python3 -m harness.cli cleanup-list runs/ && python3 -m harness.cli teardown runs/ +python3 -m harness.generator --id G0100 --pool eval --object-type CLAS --category C --run-base 2000 +python3 -m harness.pilot 2 +python3 -c "from harness.ledger import spent; print(spent())" +``` + +## 6. Current state (2026-10-02) + +- Done: harness on Mac mini; test include creation in the server; teardown; FUNC/PROG/DDLS support; + task generator; ledger and budget guard. +- Results: T01 oracle 100, Qwen 27B 41.7 (old run 103), DeepSeek 98.5. T13 DeepSeek 85, T14 DeepSeek 100. +- Running: pilot (`harness.pilot`, 20 tasks) → `runs/gen/pilot.json`, log `runs/gen/pilot.log`. + A macOS notification shows when it ends. +- First generated task G0001: not accepted after 3 attempts (activation error, seed save error, + reference failed 1 hidden test). Feedback now includes the reference write/activation errors. + +## 7. Next steps + +1. Analyze the pilot: acceptance rate, failure causes, cost per task. Improve the generator prompt. +2. Step D: mutation check (hidden tests must fail on a broken reference). +3. Step F: eval set of 110 tasks: ~150 candidates, empirical filter (2–3 models), Claude review with a + checklist, Kral spot-checks ~10 flagged tasks. +4. Scoring for stop tasks (category H) and category K (free-text input, other tool schema). +5. Step 1.3b: generic ABAP MCP interface (spec `abap-mcp-arayuz.md`, later). The proxy is the first adapter. + +## 8. Open items for Kral (server) + +- Concurrency: queue calls per RFC connection, or use a connection pool. +- BDEF creation (needed for RAP tasks). diff --git a/harness/__init__.py b/harness/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/harness/adt_client.py b/harness/adt_client.py new file mode 100644 index 0000000..8be4c13 --- /dev/null +++ b/harness/adt_client.py @@ -0,0 +1,72 @@ +"""Direct ADT HTTP access. Only for teardown (deletion). The model never uses it. + +Credentials come from the environment (or .env): A4H_URL, A4H_USER, A4H_PASSWORD, A4H_CLIENT. +""" +import base64 +import http.cookiejar +import os +import re +import urllib.request +from xml.sax.saxutils import quoteattr + +DEL_REQ = "application/vnd.sap.adt.deletion.request.v1+xml" +DEL_RES = "application/vnd.sap.adt.deletion.response.v1+xml" + + +def load_env(path=".env"): + if os.path.exists(path): + for line in open(path): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, v = line.split("=", 1) + os.environ.setdefault(k.strip(), v.strip()) + + +class AdtClient: + def __init__(self): + load_env() + self.base = os.environ["A4H_URL"].rstrip("/") + self.client = os.environ.get("A4H_CLIENT", "001") + auth = f'{os.environ["A4H_USER"]}:{os.environ["A4H_PASSWORD"]}' + self.auth = "Basic " + base64.b64encode(auth.encode()).decode() + self.opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) + self.csrf = None + + def _req(self, method, path, body=None, headers=None): + url = f"{self.base}{path}{'&' if '?' in path else '?'}sap-client={self.client}" + h = {"Authorization": self.auth} + if self.csrf: + h["x-csrf-token"] = self.csrf + h.update(headers or {}) + req = urllib.request.Request(url, body.encode() if body else None, h, method=method) + with self.opener.open(req, timeout=300) as r: + return r.status, dict(r.headers), r.read().decode() + + def fetch_csrf(self): + _, hdr, _ = self._req("GET", "/sap/bc/adt/discovery", + headers={"x-csrf-token": "fetch", "Accept": "*/*"}) + self.csrf = hdr.get("x-csrf-token") or hdr.get("X-CSRF-Token") + + def delete(self, uris): + """Delete objects by ADT URI. Returns {uri: (deleted, message)}.""" + if not uris: + return {} + if not self.csrf: + self.fetch_csrf() + objs = "".join(f"" + for u in uris) + body = ('' + '' + objs + '') + _, _, text = self._req("POST", "/sap/bc/adt/deletion/delete", body, + {"Content-Type": DEL_REQ, "Accept": DEL_RES}) + out = {} + for m in re.finditer(r']*)>(.*?)', text, re.S): + attrs, inner = m.group(1), m.group(2) + uri = re.search(r'adtcore:uri="([^"]+)"', attrs) + ok = re.search(r'del:isDeleted="([^"]+)"', attrs) + msg = re.search(r'(.*?)', inner, re.S) + out[uri.group(1) if uri else "?"] = (ok is not None and ok.group(1) == "true", + msg.group(1) if msg else "") + return out diff --git a/harness/agents.py b/harness/agents.py new file mode 100644 index 0000000..ca5ff56 --- /dev/null +++ b/harness/agents.py @@ -0,0 +1,139 @@ +"""Agents: oracle (reference solution), null (does nothing), llm (OpenAI-compatible tool loop).""" +import json +import os +import time +import urllib.request + +from .ledger import add_usage, check_budget +from .proxy import BudgetExceeded + +SYSTEM_PROMPT = """You are an ABAP developer. You implement a plan on an SAP system with the tools. + +Rules: +- Read the plan. Implement the contract exactly. Do not change a public signature in the contract. +- You make the craft decisions: table types, keys, access paths, SQL strategy, exception design, + and syntax that fits the release target. +- If a business rule is missing, or two rules contradict, stop. Do not create objects. + Write the gap in your report. +- Do not change an object in the out-of-scope list. +- Create a new object in package $TMP with sap_create_object. Then write the source with + sap_push_source. +- Read an object that you do not know before you use it. +- After each write, check the result. If the same check fails two times, stop and report. +- Do not add behavior that the plan does not ask for. + +At the end, write a short report: +1. Decisions: one line for each craft decision, with the reason. +2. Objects: object, type, action, reason. +3. Verification: what you checked and the result. +4. Deviations and open points. +""" + + +class NullAgent: + name = "null" + + def run(self, task, proxy): + proxy.note("final", "No action.") + return "No action." + + +class OracleAgent: + """Writes the reference solution. Validates harness and scoring (expected score ~ max).""" + name = "oracle" + + def run(self, task, proxy): + for o in task.objects("reference"): + ident = {"objectType": o["type"], "objectName": o["name"]} + if o.get("functionGroup"): + ident["functionGroup"] = o["functionGroup"] + proxy.call("sap_create_object", dict(ident, packageName="$TMP", + description=o.get("description", o["name"])[:60])) + if o.get("source"): + proxy.call("sap_push_source", dict(ident, source=o["source"])) + if o.get("testclasses_source"): + proxy.call("sap_push_source", dict(ident, includeType="testclasses", + source=o["testclasses_source"])) + proxy.note("final", "Reference solution written.") + return "Reference solution written." + + +class LlmAgent: + """OpenAI-compatible chat completions with tool calls (Ollama, MLX server, vLLM, ...). + + Local models are slow: no time limit by default (tool-call budget limits the run). + One request at a time; tool calls run in sequence. + """ + + def __init__(self, model, base_url=None, api_key=None, max_turns=80, temperature=0.2, + max_seconds=None): + self.model = model + self.name = f"llm:{model}" + self.base_url = (base_url or os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")).rstrip("/") + self.api_key = api_key or os.environ.get("LLM_API_KEY", "none") + self.max_turns = max_turns + self.temperature = temperature + self.max_seconds = max_seconds + self.request_timeout = 3600 # local models are slow; a hung request still ends + + def _chat(self, messages, tools): + body = {"model": self.model, "messages": messages, "tools": tools, + "temperature": self.temperature, "parallel_tool_calls": False} + req = urllib.request.Request(f"{self.base_url}/chat/completions", json.dumps(body).encode(), + {"Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}"}) + last = None + for attempt in range(4): # model server errors (HTTP 5xx, timeouts): retry with backoff + try: + with urllib.request.urlopen(req, timeout=self.request_timeout) as r: + data = json.loads(r.read().decode()) + return data["choices"][0]["message"], data.get("usage", {}) + except Exception as e: # noqa: BLE001 + last = e + code = getattr(e, "code", None) + if code is not None and code < 500 and code != 429: + raise + time.sleep(10 * (attempt + 1)) + raise RuntimeError(f"model request failed after retries: {last}") + + def run(self, task, proxy): + tools = [{"type": "function", "function": {"name": t["name"], + "description": t.get("description", ""), + "parameters": t.get("inputSchema", {})}} + for t in proxy.schemas()] + messages = [{"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": task.spec}] + if ":cloud" in self.model: + check_budget() + final = "" + start = time.time() + for _ in range(self.max_turns): + if self.max_seconds and time.time() - start > self.max_seconds: + final = f"Stopped: time budget exceeded ({self.max_seconds} s)." + break + try: + msg, usage = self._chat(messages, tools) + add_usage(self.model, usage, kind="run", ref=proxy.prefix) + except RuntimeError as e: + final = f"Stopped: {e}" + break + proxy.note("assistant", {"content": msg.get("content"), + "tool_calls": msg.get("tool_calls"), "usage": usage}) + messages.append({k: v for k, v in msg.items() if k in ("role", "content", "tool_calls")}) + calls = msg.get("tool_calls") or [] + if not calls: + final = msg.get("content") or "" + break + try: + for c in calls: + fn = c["function"] + args = fn.get("arguments") or "{}" + args = json.loads(args) if isinstance(args, str) else args + err, text = proxy.call(fn["name"], args) + messages.append({"role": "tool", "tool_call_id": c.get("id", ""), + "content": ("ERROR: " if err else "") + text[:12000]}) + except BudgetExceeded as e: + final = f"Stopped: budget exceeded ({e})." + break + proxy.note("final", final) + return final diff --git a/harness/cli.py b/harness/cli.py new file mode 100644 index 0000000..fabe5e4 --- /dev/null +++ b/harness/cli.py @@ -0,0 +1,89 @@ +"""CLI. + python3 -m harness.cli run T01 --agent oracle|null|llm [--model NAME] [--run N] [--no-teardown] + python3 -m harness.cli teardown runs/ + python3 -m harness.cli cleanup-list Z900001_ runs/cleanup_probe +""" +import argparse +import json +import os + +from .agents import LlmAgent, NullAgent, OracleAgent +from .adt_client import load_env +from .runner import Runner, delete_uris + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def next_run_no(): + runs = os.path.join(ROOT, "runs") + nums = [int(d[:3]) for d in os.listdir(runs) if d[:3].isdigit()] + return max(nums, default=0) + 1 + + +def main(): + load_env(os.path.join(ROOT, ".env")) + ap = argparse.ArgumentParser() + sub = ap.add_subparsers(dest="cmd", required=True) + r = sub.add_parser("run") + r.add_argument("task") + r.add_argument("--agent", default="oracle", choices=["oracle", "null", "llm"]) + r.add_argument("--model") + r.add_argument("--base-url") + r.add_argument("--run", type=int) + r.add_argument("--no-teardown", action="store_true") + r.add_argument("--max-minutes", type=float, help="optional time limit for the llm agent (default: none)") + t = sub.add_parser("teardown") + t.add_argument("run_dir") + sub.add_parser("teardown-all", help="delete objects of every run that has delete_uris.json and no teardown.json") + rs = sub.add_parser("rescore", help="score an existing run again (objects must still exist)") + rs.add_argument("run_dir") + c = sub.add_parser("cleanup-list", help="write delete_uris.json for all objects with a prefix") + c.add_argument("prefix") + c.add_argument("out_dir") + a = ap.parse_args() + + if a.cmd == "run": + agent = {"oracle": OracleAgent, "null": NullAgent}.get(a.agent) + agent = agent() if agent else LlmAgent(a.model, a.base_url, + max_seconds=a.max_minutes * 60 if a.max_minutes else None) + runner = Runner(os.path.join(ROOT, "tasks"), os.path.join(ROOT, "runs")) + rep, run_dir = runner.run(a.task, agent, a.run or next_run_no(), teardown=not a.no_teardown) + print(json.dumps({k: rep[k] for k in ("task", "agent", "prefix", "gates", "score", + "tool_calls", "seconds", "teardown")}, indent=1)) + print("run dir:", run_dir) + elif a.cmd == "cleanup-list": + from .mcp_client import McpClient + from .runner import DELETE_ORDER, Runner as R + os.makedirs(a.out_dir, exist_ok=True) + with McpClient() as m: + objs = R("", "")._objects_with_prefix(m, a.prefix.upper()) + objs.sort(key=lambda o: DELETE_ORDER.index(o["objectType"]) if o["objectType"] in DELETE_ORDER else 99) + json.dump([o["uri"] for o in objs], open(os.path.join(a.out_dir, "delete_uris.json"), "w"), indent=1) + print(len(objs), "objects ->", os.path.join(a.out_dir, "delete_uris.json")) + elif a.cmd == "rescore": + rd = os.path.abspath(a.run_dir) + name = os.path.basename(rd) + run_no, task_id, agent_name = name.split("_", 2) + runner = Runner(os.path.join(ROOT, "tasks"), os.path.join(ROOT, "runs")) + agent = NullAgent() + agent.name = agent_name + rep, _ = runner.run(task_id, agent, int(run_no), teardown=True, rescore_dir=rd) + print(json.dumps({k: rep.get(k) for k in ("task", "agent", "gates", "score", "tool_calls", + "activations", "max_fail_streak", "agent_seconds")}, indent=1)) + elif a.cmd == "teardown-all": + runs = os.path.join(ROOT, "runs") + for d in sorted(os.listdir(runs)): + rd = os.path.join(runs, d) + if os.path.exists(os.path.join(rd, "delete_uris.json")) and not os.path.exists(os.path.join(rd, "teardown.json")): + res = delete_uris(json.load(open(os.path.join(rd, "delete_uris.json")))) + json.dump(res, open(os.path.join(rd, "teardown.json"), "w"), indent=1) + print(d, sum(v["deleted"] for v in res.values()), "/", len(res), "deleted") + else: + uris = json.load(open(os.path.join(a.run_dir, "delete_uris.json"))) + res = delete_uris(uris) + json.dump(res, open(os.path.join(a.run_dir, "teardown.json"), "w"), indent=1) + print(json.dumps(res, indent=1)) + + +if __name__ == "__main__": + main() diff --git a/harness/generator.py b/harness/generator.py new file mode 100644 index 0000000..df0e59f --- /dev/null +++ b/harness/generator.py @@ -0,0 +1,242 @@ +"""Task generator: a cloud model writes a task bundle; the harness validates it (oracle = 100, null = 0). + + python3 -m harness.generator --id G0001 --pool eval --object-type CLAS --category C --difficulty 2 +""" +import argparse +import json +import os +import re +import time +import urllib.request + +from .adt_client import load_env +from .agents import NullAgent, OracleAgent +from .ledger import add_usage, check_budget +from .runner import Runner + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15"} +CATEGORIES = { + "A": "pure logic in a new class (language, OO design, Clean ABAP)", + "B": "database access (ABAP SQL, CDS) with test doubles", + "C": "internal table craft (table kind, keys, access path)", + "D": "exception design (class-based exceptions, messages)", + "E": "refactoring legacy code (seed contains legacy code; behavior must stay the same)", + "F": "use an unknown object: the spec names a seed object but not its signature; the model must read it", + "G": "old release target (v702 or v740sp05): no newer syntax", + "I": "bug fix: the seed contains a faulty object; the spec describes the symptom", +} + +SYSTEM = """You write evaluation tasks for an ABAP developer model. Each task is a bundle of files. +The model gets only spec.md and works on an SAP ABAP Platform 2025 system (SAP_BASIS 816, client 001) +through ADT tools. The harness installs the seed objects, runs the model, then checks the result with +hidden ABAP Unit tests, ATC, and abaplint. + +Rules for the bundle: +- Use the placeholder {{P}} (upper case) and {{p}} (lower case) at the start of EVERY object name. + The harness replaces it with a run prefix of 9 characters (for example Z005P001_). + Name length after replacement: classes, programs, function modules, CDS entities max 30; + database tables max 16; function groups max 26. +- All objects are in package $TMP. Do not use transports. +- Do not use SAP application module data (no FI, SD, MM tables). Use generic business domains and + only objects that exist in every ABAP Platform system (language, ABAP SQL, CDS, CL_ABAP_*, CL_SALV_TABLE, + CL_OSQL_TEST_ENVIRONMENT, CL_CDS_TEST_ENVIRONMENT). Seed your own tables and data if needed. +- No dynpro (CALL SCREEN), no SmartForms, no BAdI, no RAP behavior definitions. +- spec.md uses Simplified Technical English and these sections in this order: + 1. Goal, 2. Open questions (write "None."), 3. Context, 4. Contract, 5. Business rules, + 6. Constraints (release target, coding standards, out of scope), 7. Acceptance. + The Contract fixes every public name the hidden tests use: object names, method signatures, + function module parameters, report parameters, ALV column names, CDS element names. + The Business rules are complete and unambiguous. Every rule is checked by at least one hidden test. + Do not tell the model HOW to implement (no table kinds, no SQL). Craft decisions belong to the model. +- Seed objects: TABL as DDL source ("define table ..."), FUGR without source, FUNC with "functionGroup" + and full source including the signature in the FUNCTION statement, classes with full source. + Seed table data: a seed class that implements IF_OO_ADT_CLASSRUN, with "run": true. +- Hidden tests: one global class, "FOR TESTING DURATION SHORT RISK LEVEL HARMLESS", 5 to 12 test + methods. Use only the public contract. Function modules: CALL FUNCTION with EXCEPTIONS. + Reports: SUBMIT ... AND RETURN with cl_salv_bs_runtime_info=>set( display = abap_false + metadata = abap_false data = abap_true ) and get_data_ref. CDS: cl_cds_test_environment. +- Reference solution: correct, Clean ABAP, methods below 40 statements, passes all hidden tests, + no ATC priority 1 or 2 findings (for example: pass large parameters by reference). + Include the model's expected own tests: for classes a "testclasses_file" (local test classes); + for reports local test classes inside the program; for function modules and CDS a global test class. +- task.json keys: id, category, object_type, difficulty, release_target, expected_outcome ("implement"), + budget {max_tool_calls, max_activations}, seed[], contract[], out_of_scope[], hidden_tests[], + reference[], craft_checks[]. Contract entries: CLAS {"implements"} optional; FUNC {"functionGroup", + "params":[{"name","type"}]}; PROG {"parameters":[...]}; DDLS {"fields":[...]}. + +Return ONLY one JSON object, no markdown fence: +{"task": , "files": {"": "", ...}} +""" + + +def bundle_of(task_dir): + files = {} + for base, _, names in os.walk(task_dir): + for n in names: + if n.startswith(".") or n == "generation.json": + continue + path = os.path.join(base, n) + rel = os.path.relpath(path, task_dir) + if rel != "task.json": + files[rel] = open(path).read() + return {"task": json.load(open(os.path.join(task_dir, "task.json"))), "files": files} + + +def chat(model, messages, base_url): + body = {"model": model, "messages": messages, "temperature": 0.7} + req = urllib.request.Request(f"{base_url}/chat/completions", json.dumps(body).encode(), + {"Content-Type": "application/json", "Authorization": "Bearer none"}) + for attempt in range(4): + try: + with urllib.request.urlopen(req, timeout=1800) as r: + data = json.loads(r.read().decode()) + add_usage(model, data.get("usage", {}), kind="generate") + return data["choices"][0]["message"].get("content") or "" + except Exception as e: # noqa: BLE001 + if getattr(e, "code", 500) < 500 and getattr(e, "code", 500) != 429: + raise + time.sleep(15 * (attempt + 1)) + raise RuntimeError("generation request failed") + + +def parse_bundle(text): + text = text.strip() + text = re.sub(r"^```(json)?\s*|\s*```$", "", text) + start, end = text.find("{"), text.rfind("}") + return json.loads(text[start:end + 1]) + + +def check_bundle(b): + errs = [] + t, files = b.get("task", {}), b.get("files", {}) + for k in ("seed", "contract", "hidden_tests", "reference"): + if k not in t: + errs.append(f"task.json misses '{k}'") + if "spec.md" not in files: + errs.append("spec.md missing") + for k in ("seed", "hidden_tests", "reference"): + for o in t.get(k, []): + for fk in ("file", "testclasses_file"): + if fk in o and o[fk] not in files: + errs.append(f"{k}: file {o[fk]} missing") + if not o.get("name", "").startswith("{{P}}"): + errs.append(f"{k}: name {o.get('name')} does not start with {{{{P}}}}") + limit = 16 if o.get("type") == "TABL" else 26 if o.get("type") == "FUGR" else 30 + if len(o.get("name", "").replace("{{P}}", "Z0000000_")) > limit: + errs.append(f"{k}: name {o.get('name')} too long (max {limit})") + if not t.get("hidden_tests"): + errs.append("no hidden test class") + return errs + + +def write_bundle(b, task_dir, task_id): + os.makedirs(task_dir, exist_ok=True) + b["task"]["id"] = task_id + json.dump(b["task"], open(os.path.join(task_dir, "task.json"), "w"), indent=2) + for rel, content in b["files"].items(): + path = os.path.join(task_dir, rel) + os.makedirs(os.path.dirname(path), exist_ok=True) + open(path, "w").write(content) + + +def validate(pool_root, task_id, run_base): + r = Runner(pool_root, os.path.join(ROOT, "runs", "gen")) + os.makedirs(os.path.join(ROOT, "runs", "gen"), exist_ok=True) + rep_o, dir_o = r.run(task_id, OracleAgent(), run_base) + rep_o["_dir"] = dir_o + rep_n, _ = r.run(task_id, NullAgent(), run_base + 1) + return rep_o, rep_n + + +def write_errors(run_dir): + """Failed writes and activation messages of the oracle run.""" + out = [] + path = os.path.join(run_dir or "", "trajectory.jsonl") + if not os.path.exists(path): + return out + for line in open(path): + e = json.loads(line) + if e.get("tool") in ("sap_create_object", "sap_push_source") and ( + e["is_error"] or '"success":false' in e["result"].replace(" ", "")): + out.append({"tool": e["tool"], "object": e["args"].get("objectName"), + "include": e["args"].get("includeType", "main"), "result": e["result"][:1500]}) + return out + + +def failure_summary(rep): + out = {"reference_write_errors": write_errors(rep.get("_dir")),"gates": rep.get("gates"), "score": rep.get("score"), "setup": rep.get("setup"), + "atc": rep.get("atc"), "abaplint": rep.get("abaplint"), "own_tests": rep.get("own_tests"), + "hidden_failed": [d for d in rep.get("hidden_tests", {}).get("detail", []) if not d["ok"]], + "hidden_install": rep.get("hidden_tests", {}).get("install")} + return json.dumps(out)[:6000] + + +def generate(task_id, pool, object_type, category, difficulty, model, base_url, run_base, topic=None, + max_repairs=3): + check_budget() + pool_root = os.path.join(ROOT, "tasks_gen", pool) + task_dir = os.path.join(pool_root, task_id) + example = bundle_of(os.path.join(ROOT, "tasks", EXAMPLE_FOR[object_type])) + ask = (f"Write one new task.\nObject type of the main contract object: {object_type}.\n" + f"Skill category {category}: {CATEGORIES[category]}.\nDifficulty {difficulty} of 3.\n" + + (f"Topic idea: {topic}\n" if topic else "Choose a new, realistic business topic.\n") + + "Here is an example bundle of a different task (same format):\n" + json.dumps(example)) + messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": ask}] + log = {"id": task_id, "pool": pool, "object_type": object_type, "category": category, "attempts": []} + for attempt in range(max_repairs + 1): + text = chat(model, messages, base_url) + messages.append({"role": "assistant", "content": text}) + try: + b = parse_bundle(text) + errs = check_bundle(b) + except Exception as e: # noqa: BLE001 + b, errs = None, [f"invalid JSON: {e}"] + if errs: + log["attempts"].append({"stage": "bundle", "errors": errs}) + messages.append({"role": "user", "content": "Fix these problems and return the full bundle again:\n" + + "\n".join(errs)}) + continue + b["task"].setdefault("object_type", object_type) + b["task"].setdefault("category", category) + write_bundle(b, task_dir, task_id) + rep_o, rep_n = validate(pool_root, task_id, run_base + 2 * attempt) + so, sn = (rep_o.get("score") or {}).get("total"), (rep_n.get("score") or {}).get("total") + log["attempts"].append({"stage": "validate", "oracle": so, "null": sn}) + if so == 100 and sn == 0: + log["accepted"] = True + break + messages.append({"role": "user", "content": + "The harness ran your reference solution (oracle) and an empty solution (null). " + f"Required: oracle 100, null 0. Result: oracle {so}, null {sn}.\n" + f"Oracle report: {failure_summary(rep_o)}\n" + "Fix the bundle (reference, hidden tests, seed, or contract) and return the full " + "bundle again."}) + else: + log["accepted"] = False + json.dump(log, open(os.path.join(task_dir if os.path.isdir(task_dir) else pool_root, "generation.json"), "w"), + indent=1) + return log + + +def main(): + load_env(os.path.join(ROOT, ".env")) + ap = argparse.ArgumentParser() + ap.add_argument("--id", required=True) + ap.add_argument("--pool", default="eval", choices=["eval", "train"]) + ap.add_argument("--object-type", required=True, choices=sorted(EXAMPLE_FOR)) + ap.add_argument("--category", required=True, choices=sorted(CATEGORIES)) + ap.add_argument("--difficulty", type=int, default=2) + ap.add_argument("--topic") + ap.add_argument("--model", default="deepseek-v4.1-flash:cloud") + ap.add_argument("--base-url", default=os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")) + ap.add_argument("--run-base", type=int, required=True) + a = ap.parse_args() + os.makedirs(os.path.join(ROOT, "tasks_gen", a.pool), exist_ok=True) + log = generate(a.id, a.pool, a.object_type, a.category, a.difficulty, a.model, a.base_url, + a.run_base, a.topic) + print(json.dumps(log)) + + +if __name__ == "__main__": + main() diff --git a/harness/ledger.py b/harness/ledger.py new file mode 100644 index 0000000..ab7dff4 --- /dev/null +++ b/harness/ledger.py @@ -0,0 +1,42 @@ +"""Cost ledger for cloud model usage and a budget guard. + +.env: BUDGET_LIMIT_USD (default 45), BUDGET_CYCLE_START (YYYY-MM-DD, default 2026-10-02). +Prices are DeepSeek V4.1 Flash list prices without cache discount (upper bound). +""" +import json +import os +import time + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LEDGER = os.path.join(ROOT, "runs", "ledger.jsonl") +PRICE_IN, PRICE_OUT = 0.30 / 1e6, 1.20 / 1e6 + + +class BudgetExceeded(Exception): + pass + + +def add_usage(model, usage, kind, ref=""): + if ":cloud" not in model: + return + pin, pout = usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0) + os.makedirs(os.path.dirname(LEDGER), exist_ok=True) + with open(LEDGER, "a") as f: + f.write(json.dumps({"t": time.time(), "day": time.strftime("%Y-%m-%d"), "model": model, "kind": kind, + "ref": ref, "in": pin, "out": pout, + "usd": round(pin * PRICE_IN + pout * PRICE_OUT, 5)}) + "\n") + + +def spent(since=None): + since = since or os.environ.get("BUDGET_CYCLE_START", "2026-10-02") + if not os.path.exists(LEDGER): + return 0.0 + return round(sum(json.loads(l)["usd"] for l in open(LEDGER) if json.loads(l)["day"] >= since), 4) + + +def check_budget(): + limit = float(os.environ.get("BUDGET_LIMIT_USD", "45")) + s = spent() + if s >= limit: + raise BudgetExceeded(f"cycle spend {s} USD >= limit {limit} USD") + return s diff --git a/harness/mcp_client.py b/harness/mcp_client.py new file mode 100644 index 0000000..7d0eaac --- /dev/null +++ b/harness/mcp_client.py @@ -0,0 +1,96 @@ +"""Minimal MCP streamable-http client. One session per worker.""" +import json +import os +import random +import time +import urllib.request +import urllib.error + + +class McpError(Exception): + pass + + +class McpClient: + def __init__(self, url=None, token=None, timeout=300): + self.url = url or os.environ.get("MCP_URL", "http://127.0.0.1:3000/mcp") + self.token = token or os.environ.get("MCP_TOKEN", "") + self.timeout = timeout + self.sid = None + self._id = 0 + self._lock_retries = 0 + self.lock_retry_total = 0 + + def _post(self, body, method="POST"): + h = {"Content-Type": "application/json", + "Accept": "application/json, text/event-stream"} + if self.token: + h["Authorization"] = f"Bearer {self.token}" + if self.sid: + h["Mcp-Session-Id"] = self.sid + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(self.url, data, h, method=method) + with urllib.request.urlopen(req, timeout=self.timeout) as r: + sid = r.headers.get("Mcp-Session-Id") + raw = r.read().decode() + if sid: + self.sid = sid + if "data:" in raw[:40]: + raw = "".join(l[5:].strip() for l in raw.splitlines() if l.startswith("data:")) + return json.loads(raw) if raw.strip() else None + + def open(self): + self._id += 1 + self._post({"jsonrpc": "2.0", "id": self._id, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "abap-llm-harness", "version": "0.1"}}}) + self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}) + return self + + def close(self): + if self.sid: + try: + self._post(None, method="DELETE") + except Exception: + pass + self.sid = None + + def list_tools(self): + self._id += 1 + res = self._post({"jsonrpc": "2.0", "id": self._id, "method": "tools/list"}) + return res["result"]["tools"] + + def call(self, name, args, retry=True): + """Return (is_error, text).""" + self._id += 1 + body = {"jsonrpc": "2.0", "id": self._id, "method": "tools/call", + "params": {"name": name, "arguments": args}} + try: + res = self._post(body) + except urllib.error.HTTPError as e: + if e.code == 404 and retry: # session dropped (capacity limit) + self.sid = None + self.open() + return self.call(name, args, retry=False) + raise McpError(f"{name}: HTTP {e.code}") from e + if "error" in res: + return True, json.dumps(res["error"]) + r = res["result"] + text = "\n".join(c.get("text", "") for c in r.get("content", [])) + # The server shares one RFC connection between sessions. A parallel call gets + # "Concurrent call detected" instead of waiting. Retry; the caller never sees it. + if "Concurrent call detected" in text and self._lock_retries < 8: + self._lock_retries += 1 + time.sleep(0.5 * self._lock_retries + random.random()) + try: + return self.call(name, args, retry) + finally: + self._lock_retries = 0 + self.lock_retry_total += self._lock_retries + return bool(r.get("isError")), text + + def __enter__(self): + return self.open() + + def __exit__(self, *a): + self.close() diff --git a/harness/pilot.py b/harness/pilot.py new file mode 100644 index 0000000..5904dc3 --- /dev/null +++ b/harness/pilot.py @@ -0,0 +1,48 @@ +"""Pilot: generate and validate a list of tasks, one after the other.""" +import json +import os +import sys + +from .adt_client import load_env +from .generator import ROOT, generate +from .ledger import BudgetExceeded, spent + +PLAN = [ + ("CLAS", "A"), ("CLAS", "B"), ("CLAS", "C"), ("CLAS", "D"), ("CLAS", "E"), + ("CLAS", "F"), ("CLAS", "G"), ("CLAS", "I"), ("CLAS", "C"), + ("FUNC", "A"), ("FUNC", "F"), ("FUNC", "I"), + ("PROG", "B"), ("PROG", "B"), ("PROG", "E"), + ("DDLS", "B"), ("DDLS", "B"), ("DDLS", "B"), ("DDLS", "B"), ("DDLS", "B"), +] +DDLS_TOPICS = ["join of two tables and a calculated element", "association and path expression", + "aggregation with CASE and GROUP BY", "view with input parameters", + "UNION of two tables with a literal source element"] + + +def main(): + load_env(os.path.join(ROOT, ".env")) + first = int(sys.argv[1]) if len(sys.argv) > 1 else 2 + results = [] + d = 0 + for k, (otype, cat) in enumerate(PLAN): + tid = f"G{first + k:04d}" + topic = None + if otype == "DDLS": + topic, d = DDLS_TOPICS[d], d + 1 + try: + log = generate(tid, "eval", otype, cat, 2, "deepseek-v4.1-flash:cloud", + os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1"), + 1100 + 10 * k, topic) + except BudgetExceeded as e: + print("BUDGET", e, flush=True) + break + except Exception as e: # noqa: BLE001 + log = {"id": tid, "error": str(e)[:500]} + log["spent_total"] = spent() + results.append(log) + print(json.dumps(log), flush=True) + json.dump(results, open(os.path.join(ROOT, "runs", "gen", "pilot.json"), "w"), indent=1) + + +if __name__ == "__main__": + main() diff --git a/harness/proxy.py b/harness/proxy.py new file mode 100644 index 0000000..5f9fb0a --- /dev/null +++ b/harness/proxy.py @@ -0,0 +1,80 @@ +"""Tool proxy between agent and MCP: whitelist, budget, prefix filter, trajectory log.""" +import json +import re +import time + +MODEL_TOOLS = { + "sap_search_object", "sap_pull_source", "sap_object_structure", "sap_object_members", + "sap_usage_references", "sap_element_info", "sap_inactive_objects", "sap_short_dumps", + "sap_sql_query", "sap_create_object", "sap_push_source", "sap_push_element", + "sap_push_message", "sap_activate", "sap_syntax_check", "sap_check_object", + "sap_run_unit_test", "sap_atc_run", "sap_pretty_print", "sap_run_class", +} +WRITE_TOOLS = {"sap_create_object", "sap_push_source", "sap_push_element", + "sap_push_message", "sap_activate"} +RUN_PREFIX = re.compile(r"^Z\d[0-9A-Z]{5,6}_", re.I) + + +class BudgetExceeded(Exception): + pass + + +class ToolProxy: + def __init__(self, mcp, prefix, budget, log_path): + self.mcp = mcp + self.prefix = prefix.upper() + self.max_calls = budget.get("max_tool_calls", 60) + self.max_activations = budget.get("max_activations", 15) + self.calls = 0 + self.activations = 0 + self.fail_streak = 0 + self.max_fail_streak = 0 + self.log = open(log_path, "a") + + def schemas(self): + return [t for t in self.mcp.list_tools() if t["name"] in MODEL_TOOLS] + + def _foreign(self, name): + n = (name or "").upper() + return bool(RUN_PREFIX.match(n)) and not n.startswith(self.prefix) + + def _filter(self, tool, text): + if tool not in ("sap_search_object", "sap_usage_references"): + return text + try: + data = json.loads(text) + except ValueError: + return text + if isinstance(data, list): + data = [d for d in data if not self._foreign(d.get("name") if isinstance(d, dict) else "")] + return json.dumps(data) + return text + + def call(self, tool, args): + entry = {"t": time.time(), "tool": tool, "args": args} + if tool not in MODEL_TOOLS: + result = (True, f"Tool {tool} is not available.") + elif self.calls >= self.max_calls: + raise BudgetExceeded(f"max_tool_calls={self.max_calls}") + else: + self.calls += 1 + name = str(args.get("objectName", "")).upper() + if tool in WRITE_TOOLS 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)): + self.activations += 1 + err, text = self.mcp.call(tool, args) + result = (err, self._filter(tool, text)) + if tool in WRITE_TOOLS: + failed = err or '"success":false' in text.replace(" ", "") + self.fail_streak = self.fail_streak + 1 if failed else 0 + self.max_fail_streak = max(self.max_fail_streak, self.fail_streak) + entry["is_error"], entry["result"] = result[0], result[1][:20000] + self.log.write(json.dumps(entry) + "\n") + self.log.flush() + return result + + def note(self, kind, content): + self.log.write(json.dumps({"t": time.time(), "kind": kind, "content": content}) + "\n") + self.log.flush() diff --git a/harness/runner.py b/harness/runner.py new file mode 100644 index 0000000..cd89c61 --- /dev/null +++ b/harness/runner.py @@ -0,0 +1,394 @@ +"""One task run: setup -> agent -> collect -> gates -> tests -> checks -> score -> teardown.""" +import json +import os +import re +import subprocess +import time + +from .adt_client import AdtClient, load_env +from .mcp_client import McpClient +from .proxy import ToolProxy +from .task import Task, prefix_for + +ABAPLINT = os.path.join(os.path.dirname(__file__), "..", "node_modules", ".bin", "abaplint") +CLEAN_RULES = { + "unused_variables": True, "unused_types": True, "prefer_xsdbool": True, "use_new": True, + "prefer_returning_to_exporting": True, "functional_writing": True, + "preferred_compare_operator": True, "use_line_exists": True, "line_length": {"length": 120}, + "max_one_statement": True, "empty_statement": True, "commented_code": True, + "omit_parameter_name": True, "prefer_is_not": True, "exporting": True, +} +DELETE_ORDER = ["CLAS", "INTF", "PROG", "FUNC", "FUGR", "SRVD", "DDLX", "DCLS", "DDLS", + "TTYP", "TABL", "STRU", "DTEL", "DOMA", "MSAG"] +SOURCE_TYPES = ("CLAS", "INTF", "PROG", "FUNC", "DDLS", "DCLS", "DDLX", "TABL") + + +def _obj_args(otype, name, fg=None): + a = {"objectType": otype, "objectName": name} + if fg: + a["functionGroup"] = fg + return a + + +def _json(text): + try: + return json.loads(text) + except ValueError: + return None + + +def _norm(src): + return "\n".join(l.rstrip().lower() for l in (src or "").splitlines() if l.strip()) + + +def trajectory_stats(path): + """Rebuild agent statistics from a trajectory (same rules as ToolProxy).""" + from .proxy import WRITE_TOOLS + calls = activations = streak = max_streak = 0 + final, t_first, t_last = "", None, None + for line in open(path): + e = json.loads(line) + t_first = t_first or e["t"] + t_last = e["t"] + if e.get("kind") == "final": + final = e.get("content") or "" + if "tool" not in e or e["result"].startswith("Tool ") and e["is_error"]: + continue + calls += 1 + tool, args = e["tool"], e.get("args", {}) + if tool in WRITE_TOOLS: + if tool == "sap_activate" or args.get("activate", True): + activations += 1 + failed = e["is_error"] or '"success":false' in e["result"].replace(" ", "") + streak = streak + 1 if failed else 0 + max_streak = max(max_streak, streak) + return {"tool_calls": calls, "activations": activations, "max_fail_streak": max_streak, + "final_report": final, "agent_seconds": round((t_last or 0) - (t_first or 0), 1)} + + +class Runner: + def __init__(self, tasks_root, runs_root): + self.tasks_root = tasks_root + self.runs_root = runs_root + + # ---------- helpers ---------- + def _install(self, mcp, objs): + out = [] + for o in objs: + fg = o.get("functionGroup") + cargs = dict(_obj_args(o["type"], o["name"], fg), packageName="$TMP", + description=o.get("description", o["name"])[:60]) + e1, t1 = mcp.call("sap_create_object", cargs) + ok, t2 = not e1, "" + if o.get("source"): + e2, t2 = mcp.call("sap_push_source", dict(_obj_args(o["type"], o["name"], fg), + source=o["source"])) + ok = not e2 and (_json(t2) or {}).get("success", False) + if o.get("testclasses_source"): + e3, t3 = mcp.call("sap_push_source", dict(_obj_args(o["type"], o["name"], fg), + includeType="testclasses", + source=o["testclasses_source"])) + ok = ok and not e3 and (_json(t3) or {}).get("success", False) + if o.get("run"): + e4, t4 = mcp.call("sap_run_class", {"className": o["name"]}) + ok = ok and not e4 and (_json(t4) or {}).get("success", False) + out.append({"name": o["name"], "ok": ok, "create": t1[:300], "push": t2[:500]}) + 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 + + def _source(self, mcp, otype, name, fg=None): + err, text = mcp.call("sap_pull_source", _obj_args(otype, name, fg)) + return None if err else text + + def _abaplint(self, run_dir, task, sources): + d = os.path.join(run_dir, "abaplint") + os.makedirs(os.path.join(d, "src"), exist_ok=True) + ext = {"CLAS": "clas", "INTF": "intf", "PROG": "prog"} + for (otype, name), src in sources.items(): + if otype not in ext: + continue + base = os.path.join(d, "src", f"{name.lower()}.{ext[otype]}") + open(base + ".abap", "w").write(src) + if otype == "PROG": + open(base + ".xml", "w").write( + f'' + f'{name}1EX' + f'X') + continue + tag = "VSEOCLASS" if otype == "CLAS" else "VSEOINTERF" + ser = "LCL_OBJECT_CLAS" if otype == "CLAS" else "LCL_OBJECT_INTF" + open(base + ".xml", "w").write( + f'' + f'<{tag}>{name}Ex' + f'1X') + rules = {"check_syntax": True, "unknown_types": True} + rules.update(CLEAN_RULES) + for r in task.meta.get("craft_checks", []): + rules[r] = {"statements": 40} if r == "method_length" else True + cfg = {"global": {"files": "/src/**/*.*"}, "dependencies": [], + "syntax": {"version": task.meta.get("release_target", "v758"), + # objects of this run that abaplint cannot read (TABL, DDLS, FUNC) are not errors; + # A4H syntax check (G1) covers them + "errorNamespace": f"^(?!{task.prefix})(Z|Y|LCL_|TY_|LIF_|LTC_)"}, + "rules": rules} + json.dump(cfg, open(os.path.join(d, "abaplint.json"), "w"), indent=1) + p = subprocess.run([ABAPLINT, "abaplint.json", "-f", "json"], cwd=d, + capture_output=True, text=True, timeout=300) + try: + issues = json.loads(p.stdout or "[]") + except ValueError: + issues = [{"key": "abaplint_failed", "description": (p.stdout + p.stderr)[:500], + "file": {"filename": ""}}] + def _fname(i): + f = i.get("file", "") + return f.get("filename", "") if isinstance(f, dict) else str(f) + return [{"rule": i.get("key"), "file": os.path.basename(_fname(i)), + "line": i.get("start", {}).get("row"), "msg": i.get("description")} for i in issues] + + # ---------- main ---------- + def run(self, task_id, agent, run_no, teardown=True, rescore_dir=None): + prefix = prefix_for(run_no, task_id) + task = Task(os.path.join(self.tasks_root, task_id), prefix) + run_dir = rescore_dir or os.path.join(self.runs_root, f"{int(run_no):03d}_{task_id}_{agent.name.replace(':', '_').replace('/', '_')}") + os.makedirs(run_dir, exist_ok=True) + rep = {"task": task_id, "agent": agent.name, "run": run_no, "prefix": prefix, + "started": time.strftime("%Y-%m-%dT%H:%M:%S")} + t0 = time.time() + with McpClient() as mcp: + # 1 setup + seed = task.objects("seed") + if rescore_dir: + seed_src = {o["name"].upper(): o["source"] for o in seed} + else: + rep["setup"] = self._install(mcp, seed) + if not all(x["ok"] for x in rep["setup"]): + rep["setup_failed"] = True + rep["score"] = {"total": None, "note": "setup failed; run not scored"} + all_objs = self._objects_with_prefix(mcp, prefix) + rep["teardown"] = self._teardown(all_objs, run_dir) if teardown else "skipped" + json.dump(rep, open(os.path.join(run_dir, "report.json"), "w"), indent=1) + return rep, run_dir + seed_src = {o["name"].upper(): self._source(mcp, o["type"], o["name"], o.get("functionGroup")) + for o in seed} + + # 2 run + if rescore_dir: + rep.update(trajectory_stats(os.path.join(run_dir, "trajectory.jsonl"))) + rep["rescored"] = True + else: + proxy = ToolProxy(mcp, prefix, task.meta.get("budget", {}), + os.path.join(run_dir, "trajectory.jsonl")) + proxy.note("spec", task.spec) + t1 = time.time() + rep["final_report"] = agent.run(task, proxy) + rep["agent_seconds"] = round(time.time() - t1, 1) + rep["tool_calls"], rep["activations"] = proxy.calls, proxy.activations + rep["max_fail_streak"] = proxy.max_fail_streak + + # 3 collect + hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")} + seed_names = set(seed_src) + objs = self._objects_with_prefix(mcp, prefix) + model_objs = [o for o in objs if o["name"].upper() not in seed_names | hidden_names] + sources = {} + for o in objs: + if o["objectType"] in SOURCE_TYPES: + src = self._source(mcp, o["objectType"], o["name"], o.get("functionGroup")) + if src is not None: + sources[(o["objectType"], o["name"].upper())] = src + os.makedirs(os.path.join(run_dir, "sources"), exist_ok=True) + for (t, n), s in sources.items(): + open(os.path.join(run_dir, "sources", f"{n.lower()}.{t.lower()}.abap"), "w").write(s) + rep["model_objects"] = [o["name"] for o in model_objs] + + # 4 gates + g = {} + _, inact = mcp.call("sap_inactive_objects", {}) + inactive = {d.get("name", "").upper() for d in (_json(inact) or []) if isinstance(d, dict)} + contract = task.objects("contract") + g1 = [] + for c in contract: + exists = any(o["name"].upper() == c["name"].upper() for o in objs) + ok = exists and c["name"].upper() not in inactive + if ok: + _, st = mcp.call("sap_syntax_check", _obj_args(c["type"], c["name"], c.get("functionGroup"))) + ok = (_json(st) or {}).get("errorCount", 1) == 0 + g1.append({"name": c["name"], "ok": ok}) + g["G1_active"] = all(x["ok"] for x in g1) and bool(g1) + g2 = True + for c in contract: + src = sources.get((c["type"], c["name"].upper()), "") + if c.get("implements") and not re.search(rf"INTERFACES\s+{re.escape(c['implements'])}\b", src, re.I): + g2 = False + if c["type"] == "FUNC": # signature: every parameter with its type in the FUNCTION header + header = src.split(".", 1)[0] + for prm in c.get("params", []): + if not re.search(rf"\b{re.escape(prm['name'])}\b\)?\s+TYPE\s+{re.escape(prm['type'])}\b", + header, re.I): + g2 = False + if c["type"] == "PROG": # selection screen parameters + for prm in c.get("parameters", []): + if not re.search(rf"(PARAMETERS|SELECT-OPTIONS)\s*:?[^.]*\b{re.escape(prm)}\b", src, re.I): + g2 = False + if c["type"] == "DDLS" and c.get("fields") and g["G1_active"]: + _, q = mcp.call("sap_sql_query", {"query": f"SELECT * FROM {c['name']}", "maxRows": 1}) + cols = {col.get("name", "").upper() for col in (_json(q) or {}).get("columns", [])} + if not {f.upper() for f in c["fields"]} <= cols: + g2 = False + g["G2_contract"] = g2 and g["G1_active"] + g["G4_out_of_scope"] = all( + _norm(self._source(mcp, o["type"], o["name"], o.get("functionGroup"))) + == _norm(seed_src[o["name"].upper()]) + for o in seed) + atc = [] + for c in contract: + if g["G1_active"]: + _, t = mcp.call("sap_atc_run", _obj_args(c["type"], c["name"], c.get("functionGroup"))) + for f in (_json(t) or []): + if isinstance(f, dict): + atc.append({"obj": c["name"], "prio": str(f.get("priority", "")), + "check": f.get("checkTitle") or f.get("checkId"), + "msg": (f.get("messageTitle") or f.get("text") or "")[:200]}) + rep["atc"] = atc + g["G5_no_p1"] = not any(a["prio"] == "1" for a in atc) + + # 5 hidden tests + hidden = {"total": 0, "passed": 0, "detail": []} + if g["G2_contract"]: + inst = self._install(mcp, task.objects("hidden_tests")) + hidden["install"] = inst + for o in task.objects("hidden_tests"): + _, t = mcp.call("sap_run_unit_test", {"objectType": "CLAS", "objectName": o["name"]}) + for prog in (_json(t) or {}).get("programs", []): + for tc in prog.get("testClasses", []): + for m in tc.get("testMethods", []): + ok = not m.get("alerts") + hidden["total"] += 1 + hidden["passed"] += ok + hidden["detail"].append({"method": m["name"], "ok": ok, + "alerts": json.dumps(m.get("alerts"))[:300]}) + rep["hidden_tests"] = hidden + g["G3_hidden_runs"] = hidden["passed"] > 0 + + # 6 own tests (local test include or own global test class) + own = {"tests": 0, "failures": 0, "coverage": None} + for c in contract: + if c["type"] == "CLAS" and g["G1_active"]: + _, t = mcp.call("sap_check_object", {"objectType": "CLAS", "objectName": c["name"], + "runAtc": False, "runUnitTest": True, "coverage": True}) + for st in (_json(t) or {}).get("steps", []): + if st.get("step") == "unittest": + own["tests"] += st.get("tests", 0) + own["failures"] += st.get("failures", 0) + cov = st.get("coverage", {}).get("class", {}).get("statement", {}) + own["coverage"] = cov.get("percent") + if c["type"] == "PROG" and g["G1_active"]: # local test classes inside the report + _, t = mcp.call("sap_run_unit_test", {"objectType": "PROG", "objectName": c["name"]}) + for prog in (_json(t) or {}).get("programs", []): + for tc in prog.get("testClasses", []): + for m in tc.get("testMethods", []): + own["tests"] += 1 + own["failures"] += bool(m.get("alerts")) + # own global test classes (FM and CDS tasks; also classes) + contract_names = {c["name"].upper() for c in contract} + for (otype, name), src in sources.items(): + if (otype == "CLAS" and name not in contract_names | seed_names | hidden_names + and re.search(r"FOR\s+TESTING", src, re.I)): + _, t = mcp.call("sap_run_unit_test", {"objectType": "CLAS", "objectName": name}) + for prog in (_json(t) or {}).get("programs", []): + for tc in prog.get("testClasses", []): + for m in tc.get("testMethods", []): + own["tests"] += 1 + own["failures"] += bool(m.get("alerts")) + own.setdefault("global_test_classes", []).append(name) + rep["own_tests"] = own + + # 7 abaplint + lint_sources = {k: v for k, v in sources.items() if k[1] not in hidden_names} + issues = self._abaplint(run_dir, task, lint_sources) + model_files = {f"{n.lower()}.{t.lower()}.abap" for (t, n) in lint_sources if n not in seed_names} + issues = [i for i in issues if i["file"] in model_files or i["rule"] == "abaplint_failed"] + rep["abaplint"] = issues + # Release gate only on contract objects. Helper objects (smoke runners, test classes) + # can use standard APIs that abaplint does not know; A4H syntax check covers them. + contract_files = {f"{c['name'].lower()}.{c['type'].lower()}.abap" for c in contract} + g["G6_release"] = not any(i["rule"] == "check_syntax" and i["file"] in contract_files + for i in issues) + + # 8 score + rep["gates"] = g + rep["score"] = self._score(task, rep) + rep["seconds"] = round(time.time() - t0, 1) + + # 9 teardown + all_objs = self._objects_with_prefix(mcp, prefix) + rep["teardown"] = self._teardown(all_objs, run_dir) if teardown else "skipped" + json.dump(rep, open(os.path.join(run_dir, "report.json"), "w"), indent=1) + return rep, run_dir + + def _score(self, task, rep): + g = rep["gates"] + if task.meta.get("expected_outcome") == "stop": + return {"total": None, "note": "stop-task scoring not implemented in skeleton"} + if not all(g.values()): + return {"total": 0, "failed_gates": [k for k, v in g.items() if not v]} + h = rep["hidden_tests"] + correctness = 40 * h["passed"] / max(h["total"], 1) + craft_rules = set(task.meta.get("craft_checks", [])) + contract_files = {f"{c['name'].lower()}.{c['type'].lower()}.abap" for c in task.objects("contract")} + craft_n = sum(1 for i in rep["abaplint"] if i["rule"] in craft_rules and i["file"] in contract_files) + craft_n += sum(1 for a in rep["atc"] if a["prio"] == "2") + craft = 20 * max(0.0, 1 - 0.25 * craft_n) + clean_n = sum(1 for i in rep["abaplint"] if i["rule"] in CLEAN_RULES) + clean = 15 * max(0.0, 1 - 0.1 * clean_n) + own = rep["own_tests"] + own_score = 0.0 + if own["tests"] > 0 and own["failures"] == 0: + own_score += 7.5 + if own["coverage"] is not None: + own_score += 7.5 * min(1.0, own["coverage"] / 70.0) + elif own_score: # no coverage figure for this object type (FUNC, PROG, DDLS): tests count fully + own_score = 15.0 + b = task.meta.get("budget", {}) + disc = 10.0 + if rep["tool_calls"] > b.get("max_tool_calls", 60): + disc -= 5 + if rep["activations"] > b.get("max_activations", 15): + disc -= 5 + if rep.get("max_fail_streak", 0) > 2: # rule: stop after two failures of the same check + disc -= 5 + final = (rep.get("final_report") or "").strip() + if not final or final.startswith("Stopped:"): # no report from the model + disc -= 5 + disc = max(disc, 0.0) + parts = {"correctness": round(correctness, 1), "craft": round(craft, 1), "clean": round(clean, 1), + "own_tests": round(own_score, 1), "discipline": disc} + parts["total"] = round(sum(parts.values()), 1) + parts["note"] = "own_tests: faulty-reference part not implemented in skeleton" + return parts + + def _teardown(self, objs, run_dir): + objs = sorted(objs, key=lambda o: DELETE_ORDER.index(o["objectType"]) + if o["objectType"] in DELETE_ORDER else 99) + uris = [o["uri"] for o in objs] + json.dump(uris, open(os.path.join(run_dir, "delete_uris.json"), "w"), indent=1) + load_env() + if not os.environ.get("A4H_PASSWORD"): + return {"status": "pending", "count": len(uris), + "hint": "Set A4H_URL/A4H_USER/A4H_PASSWORD in .env, then: python3 -m harness.cli teardown "} + return delete_uris(uris) + + +def delete_uris(uris): + """Delete one by one; this keeps the dependency order explicit.""" + adt = AdtClient() + res = {} + for u in uris: + res.update(adt.delete([u])) + return {u: {"deleted": ok, "msg": msg} for u, (ok, msg) in res.items()} diff --git a/harness/task.py b/harness/task.py new file mode 100644 index 0000000..6ba4c4c --- /dev/null +++ b/harness/task.py @@ -0,0 +1,62 @@ +"""Task loading and prefix substitution.""" +import json +import os + + +_B36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + +def _b36(n, width): + out = "" + while n: + n, r = divmod(n, 36) + out = _B36[r] + out + return out.rjust(width, "0")[-width:] + + +def prefix_for(run_no, task_id): + """Z + run (4 chars base36) + task (3 chars base36) + '_'. + + Run numbers below 36**3 keep a digit at position 2; the proxy uses this to tell + harness objects from other Z objects. Old prefixes (Z201001_) match the same pattern. + """ + task_no = int("".join(ch for ch in task_id if ch.isdigit()) or 0) + return f"Z{_b36(int(run_no), 4)}{_b36(task_no, 3)}_" + + +class Task: + def __init__(self, task_dir, prefix): + self.dir = task_dir + self.prefix = prefix + self.meta = json.loads(self._read("task.json")) + self.id = self.meta["id"] + + def _read(self, rel): + with open(os.path.join(self.dir, rel)) as f: + return f.read() + + def sub(self, text): + return text.replace("{{P}}", self.prefix).replace("{{p}}", self.prefix.lower()) + + def text(self, rel): + return self.sub(self._read(rel)) + + def objects(self, key): + """List of dicts with substituted names and loaded source (if 'file').""" + out = [] + for o in self.meta.get(key, []): + o = {k: self.sub(v) if isinstance(v, str) else v for k, v in o.items()} + if "file" in o: + o["source"] = self.text(o["file"]) + if "testclasses_file" in o: + o["testclasses_source"] = self.text(o["testclasses_file"]) + out.append(o) + return out + + @property + def spec(self): + return self.text("spec.md") + + @property + def out_of_scope(self): + return [self.sub(n) for n in self.meta.get("out_of_scope", [])] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1dce263 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "harness", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "harness", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@abaplint/cli": "^2.120.60" + } + }, + "node_modules/@abaplint/cli": { + "version": "2.120.63", + "resolved": "https://registry.npmjs.org/@abaplint/cli/-/cli-2.120.63.tgz", + "integrity": "sha512-7y4TCHO9vFIZvL273NCMLu1qofXpu4O6GWVwsjuu4sw6IcIzNvV9pTTdMa5mTCTstg+UvTgzFqOhtC2y7lBCnA==", + "license": "MIT", + "bin": { + "abaplint": "abaplint" + }, + "engines": { + "node": ">=22.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/larshp" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ede56ca --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "harness", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@abaplint/cli": "^2.120.60" + } +} diff --git a/tasks/T01/hidden/t01_hidden.clas.abap b/tasks/T01/hidden/t01_hidden.clas.abap new file mode 100644 index 0000000..bd9162c --- /dev/null +++ b/tasks/T01/hidden/t01_hidden.clas.abap @@ -0,0 +1,93 @@ +CLASS {{p}}t01_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + DATA mo_cut TYPE REF TO {{p}}if_iban_check. + METHODS setup. + METHODS valid_de FOR TESTING. + METHODS valid_letters_in_body FOR TESTING. + METHODS valid_all_countries FOR TESTING. + METHODS lower_case_ok FOR TESTING. + METHODS empty FOR TESTING. + METHODS only_spaces FOR TESTING. + METHODS bad_chars FOR TESTING. + METHODS digit_in_country FOR TESTING. + METHODS unknown_country FOR TESTING. + METHODS wrong_length FOR TESTING. + METHODS wrong_checksum FOR TESTING. + METHODS order_chars_before_country FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t01_hidden IMPLEMENTATION. + METHOD setup. + mo_cut = NEW {{p}}iban_validator( ). + ENDMETHOD. + + METHOD valid_de. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `DE89 3704 0044 0532 0130 00` ) ). + ENDMETHOD. + + METHOD valid_letters_in_body. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `GB82 WEST 1234 5698 7654 32` ) ). + ENDMETHOD. + + METHOD valid_all_countries. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `NL91 ABNA 0417 1643 00` ) msg = `NL` ). + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `FR14 2004 1010 0505 0001 3M02 606` ) msg = `FR` ). + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `TR33 0006 1005 1978 6457 8413 26` ) msg = `TR` ). + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `CH93 0076 2011 6238 5295 7` ) msg = `CH` ). + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `AT61 1904 3002 3457 3201` ) msg = `AT` ). + ENDMETHOD. + + METHOD lower_case_ok. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `de89370400440532013000` ) ). + ENDMETHOD. + + METHOD empty. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-empty + act = mo_cut->check( `` ) ). + ENDMETHOD. + + METHOD only_spaces. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-empty + act = mo_cut->check( ` ` ) ). + ENDMETHOD. + + METHOD bad_chars. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-chars + act = mo_cut->check( `DE89-3704-0044-0532-0130-00` ) ). + ENDMETHOD. + + METHOD digit_in_country. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-chars + act = mo_cut->check( `1E89370400440532013000` ) ). + ENDMETHOD. + + METHOD unknown_country. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-country + act = mo_cut->check( `XX89370400440532013000` ) ). + ENDMETHOD. + + METHOD wrong_length. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-length + act = mo_cut->check( `DE8937040044053201300` ) ). + ENDMETHOD. + + METHOD wrong_checksum. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-checksum + act = mo_cut->check( `DE89 3704 0044 0532 0130 01` ) ). + ENDMETHOD. + + METHOD order_chars_before_country. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-chars + act = mo_cut->check( `XX!!` ) ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T01/reference/iban_validator.clas.abap b/tasks/T01/reference/iban_validator.clas.abap new file mode 100644 index 0000000..e7d454a --- /dev/null +++ b/tasks/T01/reference/iban_validator.clas.abap @@ -0,0 +1,84 @@ +CLASS {{p}}iban_validator DEFINITION PUBLIC FINAL CREATE PUBLIC. + PUBLIC SECTION. + INTERFACES {{p}}if_iban_check. + + PRIVATE SECTION. + TYPES: + BEGIN OF ty_country, + code TYPE c LENGTH 2, + length TYPE i, + END OF ty_country, + tt_country TYPE HASHED TABLE OF ty_country WITH UNIQUE KEY code. + + METHODS normalize + IMPORTING iv_iban TYPE string + RETURNING VALUE(rv_iban) TYPE string. + METHODS has_valid_format + IMPORTING iv_iban TYPE string + RETURNING VALUE(rv_valid) TYPE abap_bool. + METHODS country_lengths + RETURNING VALUE(rt_country) TYPE tt_country. + METHODS remainder_97 + IMPORTING iv_iban TYPE string + RETURNING VALUE(rv_mod) TYPE i. +ENDCLASS. + + +CLASS {{p}}iban_validator IMPLEMENTATION. + METHOD {{p}}if_iban_check~check. + DATA(lv_iban) = normalize( iv_iban ). + IF lv_iban IS INITIAL. + rv_reason = {{p}}if_iban_check=>c_reason-empty. + RETURN. + ENDIF. + IF has_valid_format( lv_iban ) = abap_false. + rv_reason = {{p}}if_iban_check=>c_reason-chars. + RETURN. + ENDIF. + DATA(lt_country) = country_lengths( ). + ASSIGN lt_country[ code = substring( val = lv_iban len = 2 ) ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + rv_reason = {{p}}if_iban_check=>c_reason-country. + RETURN. + ENDIF. + IF strlen( lv_iban ) <> -length. + rv_reason = {{p}}if_iban_check=>c_reason-length. + RETURN. + ENDIF. + IF remainder_97( lv_iban ) <> 1. + rv_reason = {{p}}if_iban_check=>c_reason-checksum. + RETURN. + ENDIF. + rv_reason = {{p}}if_iban_check=>c_reason-ok. + ENDMETHOD. + + METHOD normalize. + rv_iban = to_upper( replace( val = iv_iban sub = ` ` with = `` occ = 0 ) ). + ENDMETHOD. + + METHOD has_valid_format. + rv_valid = xsdbool( matches( val = iv_iban pcre = `^[A-Z]{2}[0-9]{2}[A-Z0-9]*$` ) ). + ENDMETHOD. + + METHOD country_lengths. + rt_country = VALUE #( ( code = 'DE' length = 22 ) + ( code = 'GB' length = 22 ) + ( code = 'TR' length = 26 ) + ( code = 'FR' length = 27 ) + ( code = 'NL' length = 18 ) + ( code = 'AT' length = 20 ) + ( code = 'CH' length = 21 ) ). + ENDMETHOD. + + METHOD remainder_97. + DATA(lv_moved) = substring( val = iv_iban off = 4 ) && substring( val = iv_iban len = 4 ). + DO strlen( lv_moved ) TIMES. + DATA(lv_char) = substring( val = lv_moved off = sy-index - 1 len = 1 ). + IF lv_char CO '0123456789'. + rv_mod = ( rv_mod * 10 + CONV i( lv_char ) ) MOD 97. + ELSE. + rv_mod = ( rv_mod * 100 + find( val = sy-abcde sub = lv_char ) + 10 ) MOD 97. + ENDIF. + ENDDO. + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T01/reference/iban_validator.testclasses.abap b/tasks/T01/reference/iban_validator.testclasses.abap new file mode 100644 index 0000000..d1a5a40 --- /dev/null +++ b/tasks/T01/reference/iban_validator.testclasses.abap @@ -0,0 +1,48 @@ +CLASS ltc_iban_validator DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + DATA mo_cut TYPE REF TO {{p}}if_iban_check. + METHODS setup. + METHODS valid_iban_is_ok FOR TESTING. + METHODS empty_input FOR TESTING. + METHODS invalid_character FOR TESTING. + METHODS unknown_country FOR TESTING. + METHODS wrong_length FOR TESTING. + METHODS wrong_checksum FOR TESTING. +ENDCLASS. + + +CLASS ltc_iban_validator IMPLEMENTATION. + METHOD setup. + mo_cut = NEW {{p}}iban_validator( ). + ENDMETHOD. + + METHOD valid_iban_is_ok. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-ok + act = mo_cut->check( `GB82 WEST 1234 5698 7654 32` ) ). + ENDMETHOD. + + METHOD empty_input. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-empty + act = mo_cut->check( ` ` ) ). + ENDMETHOD. + + METHOD invalid_character. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-chars + act = mo_cut->check( `DE89*370400440532013000` ) ). + ENDMETHOD. + + METHOD unknown_country. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-country + act = mo_cut->check( `US89370400440532013000` ) ). + ENDMETHOD. + + METHOD wrong_length. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-length + act = mo_cut->check( `NL91ABNA041716430` ) ). + ENDMETHOD. + + METHOD wrong_checksum. + cl_abap_unit_assert=>assert_equals( exp = {{p}}if_iban_check=>c_reason-checksum + act = mo_cut->check( `GB82 WEST 1234 5698 7654 33` ) ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T01/seed/if_iban_check.intf.abap b/tasks/T01/seed/if_iban_check.intf.abap new file mode 100644 index 0000000..a813cd4 --- /dev/null +++ b/tasks/T01/seed/if_iban_check.intf.abap @@ -0,0 +1,15 @@ +INTERFACE {{p}}if_iban_check PUBLIC. + TYPES ty_reason TYPE c LENGTH 10. + CONSTANTS: + BEGIN OF c_reason, + ok TYPE ty_reason VALUE 'OK', + empty TYPE ty_reason VALUE 'EMPTY', + chars TYPE ty_reason VALUE 'CHARS', + country TYPE ty_reason VALUE 'COUNTRY', + length TYPE ty_reason VALUE 'LENGTH', + checksum TYPE ty_reason VALUE 'CHECKSUM', + END OF c_reason. + METHODS check + IMPORTING iv_iban TYPE string + RETURNING VALUE(rv_reason) TYPE ty_reason. +ENDINTERFACE. diff --git a/tasks/T01/spec.md b/tasks/T01/spec.md new file mode 100644 index 0000000..132968b --- /dev/null +++ b/tasks/T01/spec.md @@ -0,0 +1,45 @@ +# 1. Goal +Check the format and the checksum of an IBAN. A payment program calls the check +before it sends a payment. The program needs one reason code for each IBAN. + +# 2. Open questions +None. + +# 3. Context +- Interface {{P}}IF_IBAN_CHECK exists in package $TMP. It is active. +- The interface has the type TY_REASON (CHAR 10), the constant structure + C_REASON (OK, EMPTY, CHARS, COUNTRY, LENGTH, CHECKSUM), and one method: + CHECK IMPORTING iv_iban TYPE string RETURNING VALUE(rv_reason) TYPE ty_reason. + +# 4. Contract +- Create the class {{P}}IBAN_VALIDATOR in package $TMP. +- The class is public, final, and has a public constructor without parameters. +- The class implements {{P}}IF_IBAN_CHECK. +- Do not add other public methods. + +# 5. Business rules +Apply the rules in this sequence. Return the reason of the first rule that fails. +1. Remove all spaces. Convert all letters to upper case. +2. If the result is empty, return EMPTY. +3. The result must contain only the letters A to Z and the digits 0 to 9. + Positions 1 and 2 must be letters. Positions 3 and 4 must be digits. If not, + return CHARS. +4. Positions 1 and 2 are the country code. Use this list of codes and lengths: + DE 22, GB 22, TR 26, FR 27, NL 18, AT 20, CH 21. If the code is not in the + list, return COUNTRY. +5. If the length is not the length of the country, return LENGTH. +6. Calculate the checksum: move the first 4 characters to the end. Replace each + letter with a number (A = 10, B = 11, ..., Z = 35). Calculate the remainder + of this number divided by 97. If the remainder is not 1, return CHECKSUM. +7. Else return OK. + +# 6. Constraints +- Release target: 7.58. +- Coding standards: Clean ABAP. Method length below 40 statements. No global + variables. No comment that restates the code. +- Out of scope: do not change {{P}}IF_IBAN_CHECK. + +# 7. Acceptance +- The class is active and has no syntax error. +- The hidden tests pass. +- Write your own ABAP Unit tests for the class. diff --git a/tasks/T01/task.json b/tasks/T01/task.json new file mode 100644 index 0000000..cb7f30b --- /dev/null +++ b/tasks/T01/task.json @@ -0,0 +1,50 @@ +{ + "id": "T01", + "category": "A", + "difficulty": 1, + "release_target": "v758", + "expected_outcome": "implement", + "budget": { + "max_tool_calls": 40, + "max_activations": 10 + }, + "seed": [ + { + "type": "INTF", + "name": "{{P}}IF_IBAN_CHECK", + "file": "seed/if_iban_check.intf.abap", + "description": "IBAN check contract" + } + ], + "contract": [ + { + "type": "CLAS", + "name": "{{P}}IBAN_VALIDATOR", + "implements": "{{P}}IF_IBAN_CHECK" + } + ], + "out_of_scope": [ + "{{P}}IF_IBAN_CHECK" + ], + "hidden_tests": [ + { + "type": "CLAS", + "name": "{{P}}T01_HIDDEN", + "file": "hidden/t01_hidden.clas.abap", + "description": "T01 hidden tests" + } + ], + "reference": [ + { + "type": "CLAS", + "name": "{{P}}IBAN_VALIDATOR", + "file": "reference/iban_validator.clas.abap", + "description": "IBAN validator", + "testclasses_file": "reference/iban_validator.testclasses.abap" + } + ], + "craft_checks": [ + "method_length", + "db_operation_in_loop" + ] +} \ No newline at end of file diff --git a/tasks/T13/hidden/t13_hidden.clas.abap b/tasks/T13/hidden/t13_hidden.clas.abap new file mode 100644 index 0000000..e374cd7 --- /dev/null +++ b/tasks/T13/hidden/t13_hidden.clas.abap @@ -0,0 +1,70 @@ +CLASS {{p}}t13_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + METHODS call IMPORTING iv_type TYPE string iv_amount TYPE decfloat34 iv_fee TYPE decfloat34 DEFAULT 7 + EXPORTING ev_skip TYPE abap_bool ev_fee TYPE decfloat34 ev_subrc TYPE sysubrc. + METHODS invoice_percent FOR TESTING. + METHODS invoice_minimum FOR TESTING. + METHODS invoice_maximum FOR TESTING. + METHODS invoice_rounding FOR TESTING. + METHODS credit_memo_skips FOR TESTING. + METHODS other_type_zero FOR TESTING. + METHODS negative_amount FOR TESTING. + METHODS skip_false_for_invoice FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t13_hidden IMPLEMENTATION. + METHOD call. + ev_fee = iv_fee. + CALL FUNCTION '{{P}}EVT_FEE' + EXPORTING iv_doc_type = iv_type iv_amount = iv_amount + IMPORTING ev_skip = ev_skip + CHANGING cv_fee = ev_fee + EXCEPTIONS invalid_amount = 1 OTHERS = 2. + ev_subrc = sy-subrc. + ENDMETHOD. + + METHOD invoice_percent. + call( EXPORTING iv_type = `IN` iv_amount = 1000 IMPORTING ev_fee = DATA(lv_fee) ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 20 ) act = lv_fee ). + ENDMETHOD. + + METHOD invoice_minimum. + call( EXPORTING iv_type = `IN` iv_amount = 10 IMPORTING ev_fee = DATA(lv_fee) ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 1 ) act = lv_fee ). + ENDMETHOD. + + METHOD invoice_maximum. + call( EXPORTING iv_type = `IN` iv_amount = 100000 IMPORTING ev_fee = DATA(lv_fee) ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 50 ) act = lv_fee ). + ENDMETHOD. + + METHOD invoice_rounding. + call( EXPORTING iv_type = `IN` iv_amount = '123.25' IMPORTING ev_fee = DATA(lv_fee) ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '2.47' ) act = lv_fee ). + ENDMETHOD. + + METHOD credit_memo_skips. + call( EXPORTING iv_type = `CR` iv_amount = 500 iv_fee = 7 + IMPORTING ev_skip = DATA(lv_skip) ev_fee = DATA(lv_fee) ). + cl_abap_unit_assert=>assert_equals( exp = abap_true act = lv_skip ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 7 ) act = lv_fee ). + ENDMETHOD. + + METHOD other_type_zero. + call( EXPORTING iv_type = `XX` iv_amount = 500 IMPORTING ev_fee = DATA(lv_fee) ev_skip = DATA(lv_skip) ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = lv_fee ). + cl_abap_unit_assert=>assert_equals( exp = abap_false act = lv_skip ). + ENDMETHOD. + + METHOD negative_amount. + call( EXPORTING iv_type = `IN` iv_amount = -1 IMPORTING ev_subrc = DATA(lv_rc) ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_rc ). + ENDMETHOD. + + METHOD skip_false_for_invoice. + call( EXPORTING iv_type = `IN` iv_amount = 100 IMPORTING ev_skip = DATA(lv_skip) ). + cl_abap_unit_assert=>assert_equals( exp = abap_false act = lv_skip ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T13/reference/evt_fee.func.abap b/tasks/T13/reference/evt_fee.func.abap new file mode 100644 index 0000000..1bf0f42 --- /dev/null +++ b/tasks/T13/reference/evt_fee.func.abap @@ -0,0 +1,30 @@ +FUNCTION {{p}}evt_fee + IMPORTING + iv_doc_type TYPE string + iv_amount TYPE decfloat34 + EXPORTING + ev_skip TYPE abap_bool + CHANGING + cv_fee TYPE decfloat34 + EXCEPTIONS + invalid_amount. + + ev_skip = abap_false. + IF iv_amount < 0. + RAISE invalid_amount. + ENDIF. + CASE iv_doc_type. + WHEN 'IN'. + cv_fee = round( val = iv_amount * 2 / 100 dec = 2 mode = cl_abap_math=>round_half_up ). + IF cv_fee < 1. + cv_fee = 1. + ELSEIF cv_fee > 50. + cv_fee = 50. + ENDIF. + WHEN 'CR'. + ev_skip = abap_true. + WHEN OTHERS. + cv_fee = 0. + ENDCASE. + +ENDFUNCTION. diff --git a/tasks/T13/reference/t13_test.clas.abap b/tasks/T13/reference/t13_test.clas.abap new file mode 100644 index 0000000..5e81043 --- /dev/null +++ b/tasks/T13/reference/t13_test.clas.abap @@ -0,0 +1,52 @@ +CLASS {{p}}t13_test DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + METHODS fee_for IMPORTING iv_type TYPE string iv_amount TYPE decfloat34 + RETURNING VALUE(rv_fee) TYPE decfloat34. + METHODS invoice_fee FOR TESTING. + METHODS fee_limits FOR TESTING. + METHODS credit_memo FOR TESTING. + METHODS negative_amount FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t13_test IMPLEMENTATION. + METHOD fee_for. + DATA lv_skip TYPE abap_bool. + CALL FUNCTION '{{P}}EVT_FEE' + EXPORTING iv_doc_type = iv_type iv_amount = iv_amount + IMPORTING ev_skip = lv_skip + CHANGING cv_fee = rv_fee + EXCEPTIONS invalid_amount = 1 OTHERS = 2. + cl_abap_unit_assert=>assert_subrc( exp = 0 ). + ENDMETHOD. + + METHOD invoice_fee. + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 4 ) act = fee_for( iv_type = `IN` iv_amount = 200 ) ). + ENDMETHOD. + + METHOD fee_limits. + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 1 ) act = fee_for( iv_type = `IN` iv_amount = 5 ) ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 50 ) act = fee_for( iv_type = `IN` iv_amount = 9000 ) ). + ENDMETHOD. + + METHOD credit_memo. + DATA lv_skip TYPE abap_bool. + DATA lv_fee TYPE decfloat34 VALUE 3. + CALL FUNCTION '{{P}}EVT_FEE' + EXPORTING iv_doc_type = `CR` iv_amount = 100 + IMPORTING ev_skip = lv_skip + CHANGING cv_fee = lv_fee. + cl_abap_unit_assert=>assert_true( lv_skip ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 3 ) act = lv_fee ). + ENDMETHOD. + + METHOD negative_amount. + DATA lv_fee TYPE decfloat34. + CALL FUNCTION '{{P}}EVT_FEE' + EXPORTING iv_doc_type = `IN` iv_amount = -5 + CHANGING cv_fee = lv_fee + EXCEPTIONS invalid_amount = 1 OTHERS = 2. + cl_abap_unit_assert=>assert_equals( exp = 1 act = sy-subrc ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T13/seed/evt_sample.func.abap b/tasks/T13/seed/evt_sample.func.abap new file mode 100644 index 0000000..cb20ae7 --- /dev/null +++ b/tasks/T13/seed/evt_sample.func.abap @@ -0,0 +1,14 @@ +FUNCTION {{p}}evt_sample + IMPORTING + iv_doc_type TYPE string + iv_amount TYPE decfloat34 + EXPORTING + ev_skip TYPE abap_bool + CHANGING + cv_fee TYPE decfloat34 + EXCEPTIONS + invalid_amount. + + ev_skip = abap_false. + +ENDFUNCTION. diff --git a/tasks/T13/spec.md b/tasks/T13/spec.md new file mode 100644 index 0000000..10cba20 --- /dev/null +++ b/tasks/T13/spec.md @@ -0,0 +1,35 @@ +# 1. Goal +Calculate a processing fee for a posted document. The posting program calls one +event module for each document. The event module must have the same interface as +the sample event module, because the posting program calls all event modules in +the same way. + +# 2. Open questions +None. + +# 3. Context +- Function group {{P}}FG_EVT exists in package $TMP. +- Function module {{P}}EVT_SAMPLE exists in this group. It is the sample event + module. Its interface is the interface that all event modules must use. + +# 4. Contract +- Create the function module {{P}}EVT_FEE in function group {{P}}FG_EVT. +- Use exactly the interface of {{P}}EVT_SAMPLE: the same parameter names, kinds, + types, and exceptions. + +# 5. Business rules +1. If the amount is less than 0, raise the exception INVALID_AMOUNT. +2. Set the skip flag to false, except in rule 4. +3. Document type IN: the fee is 2 % of the amount, rounded half up to 2 + decimals. The minimum fee is 1.00. The maximum fee is 50.00. +4. Document type CR: set the skip flag to true. Do not change the fee. +5. Other document types: the fee is 0. + +# 6. Constraints +- Release target: 7.58. +- Out of scope: do not change {{P}}EVT_SAMPLE. + +# 7. Acceptance +- The function module is active and has no syntax error. +- The hidden tests pass. +- Write ABAP Unit tests for the function module in a global test class. diff --git a/tasks/T13/task.json b/tasks/T13/task.json new file mode 100644 index 0000000..1a41443 --- /dev/null +++ b/tasks/T13/task.json @@ -0,0 +1,80 @@ +{ + "id": "T13", + "category": "F", + "object_type": "FUNC", + "difficulty": 2, + "release_target": "v758", + "expected_outcome": "implement", + "budget": { + "max_tool_calls": 40, + "max_activations": 10 + }, + "seed": [ + { + "type": "FUGR", + "name": "{{P}}FG_EVT", + "description": "Fee events" + }, + { + "type": "FUNC", + "name": "{{P}}EVT_SAMPLE", + "functionGroup": "{{P}}FG_EVT", + "file": "seed/evt_sample.func.abap", + "description": "Sample event module" + } + ], + "contract": [ + { + "type": "FUNC", + "name": "{{P}}EVT_FEE", + "functionGroup": "{{P}}FG_EVT", + "params": [ + { + "name": "iv_doc_type", + "type": "string" + }, + { + "name": "iv_amount", + "type": "decfloat34" + }, + { + "name": "ev_skip", + "type": "abap_bool" + }, + { + "name": "cv_fee", + "type": "decfloat34" + } + ] + } + ], + "out_of_scope": [ + "{{P}}EVT_SAMPLE" + ], + "hidden_tests": [ + { + "type": "CLAS", + "name": "{{P}}T13_HIDDEN", + "file": "hidden/t13_hidden.clas.abap", + "description": "T13 hidden tests" + } + ], + "reference": [ + { + "type": "FUNC", + "name": "{{P}}EVT_FEE", + "functionGroup": "{{P}}FG_EVT", + "file": "reference/evt_fee.func.abap", + "description": "Fee event module" + }, + { + "type": "CLAS", + "name": "{{P}}T13_TEST", + "file": "reference/t13_test.clas.abap", + "description": "T13 own tests" + } + ], + "craft_checks": [ + "method_length" + ] +} \ No newline at end of file diff --git a/tasks/T14/hidden/t14_hidden.clas.abap b/tasks/T14/hidden/t14_hidden.clas.abap new file mode 100644 index 0000000..1f344d7 --- /dev/null +++ b/tasks/T14/hidden/t14_hidden.clas.abap @@ -0,0 +1,72 @@ +CLASS {{p}}t14_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + TYPES tt_region TYPE RANGE OF {{p}}sale-region. + METHODS run IMPORTING iv_from TYPE d it_region TYPE tt_region OPTIONAL + RETURNING VALUE(rr_data) TYPE REF TO data. + METHODS value_of IMPORTING ir_data TYPE REF TO data iv_row TYPE i iv_col TYPE string + RETURNING VALUE(rv_value) TYPE string. + METHODS lines_of IMPORTING ir_data TYPE REF TO data RETURNING VALUE(rv_lines) TYPE i. + METHODS all_regions FOR TESTING. + METHODS sorted_by_total FOR TESTING. + METHODS totals_and_counts FOR TESTING. + METHODS region_filter FOR TESTING. + METHODS date_filter FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t14_hidden IMPLEMENTATION. + METHOD run. + cl_salv_bs_runtime_info=>set( display = abap_false metadata = abap_false data = abap_true ). + SUBMIT {{p}}sales_sum WITH p_from = iv_from WITH s_reg IN it_region AND RETURN. + TRY. + cl_salv_bs_runtime_info=>get_data_ref( IMPORTING r_data = rr_data ). + CATCH cx_salv_bs_sc_runtime_info. + ENDTRY. + cl_salv_bs_runtime_info=>clear_all( ). + ENDMETHOD. + + METHOD lines_of. + FIELD-SYMBOLS TYPE STANDARD TABLE. + IF ir_data IS BOUND. + ASSIGN ir_data->* TO . + rv_lines = lines( ). + ENDIF. + ENDMETHOD. + + METHOD value_of. + FIELD-SYMBOLS TYPE STANDARD TABLE. + ASSIGN ir_data->* TO . + ASSIGN [ iv_row ] TO FIELD-SYMBOL(). + ASSIGN COMPONENT iv_col OF STRUCTURE TO FIELD-SYMBOL(). + cl_abap_unit_assert=>assert_subrc( exp = 0 msg = |Column { iv_col } missing| ). + rv_value = condense( CONV string( ) ). + ENDMETHOD. + + METHOD all_regions. + cl_abap_unit_assert=>assert_equals( exp = 3 act = lines_of( run( '20260101' ) ) ). + ENDMETHOD. + + METHOD sorted_by_total. + DATA(lr_data) = run( '20251201' ). + cl_abap_unit_assert=>assert_equals( exp = `APAC` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ). + cl_abap_unit_assert=>assert_equals( exp = `EU` act = value_of( ir_data = lr_data iv_row = 3 iv_col = `REGION` ) ). + ENDMETHOD. + + METHOD totals_and_counts. + DATA(lr_data) = run( '20260101' ). + cl_abap_unit_assert=>assert_equals( exp = `US` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ). + cl_abap_unit_assert=>assert_equals( exp = `150.00` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `TOTAL` ) ). + cl_abap_unit_assert=>assert_equals( exp = `2` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `COUNT` ) ). + ENDMETHOD. + + METHOD region_filter. + DATA(lr_data) = run( iv_from = '20260101' it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'EU' ) ) ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines_of( lr_data ) ). + cl_abap_unit_assert=>assert_equals( exp = `150.00` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `TOTAL` ) ). + ENDMETHOD. + + METHOD date_filter. + cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( run( '20260201' ) ) ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T14/reference/sales_sum.prog.abap b/tasks/T14/reference/sales_sum.prog.abap new file mode 100644 index 0000000..c774685 --- /dev/null +++ b/tasks/T14/reference/sales_sum.prog.abap @@ -0,0 +1,89 @@ +REPORT {{p}}sales_sum. + +DATA gv_region TYPE {{p}}sale-region. + +PARAMETERS p_from TYPE d OBLIGATORY. +SELECT-OPTIONS s_reg FOR gv_region. + +CLASS lcl_report DEFINITION FINAL. + PUBLIC SECTION. + TYPES: + BEGIN OF ty_line, + region TYPE {{p}}sale-region, + total TYPE {{p}}sale-amount, + count TYPE i, + END OF ty_line, + tt_line TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY. + TYPES tt_region TYPE RANGE OF {{p}}sale-region. + METHODS select_totals + IMPORTING iv_from TYPE d + it_region TYPE tt_region + RETURNING VALUE(rt_lines) TYPE tt_line. + METHODS display + CHANGING ct_lines TYPE tt_line. +ENDCLASS. + +CLASS lcl_report IMPLEMENTATION. + METHOD select_totals. + SELECT region, SUM( amount ) AS total, COUNT( * ) AS count + FROM {{p}}sale + WHERE sale_date >= @iv_from + AND region IN @it_region + GROUP BY region + ORDER BY total DESCENDING + INTO CORRESPONDING FIELDS OF TABLE @rt_lines. + ENDMETHOD. + + METHOD display. + TRY. + cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv) + CHANGING t_table = ct_lines ). + lo_alv->display( ). + CATCH cx_salv_msg INTO DATA(lx_error). + MESSAGE lx_error TYPE 'I' DISPLAY LIKE 'E'. + ENDTRY. + ENDMETHOD. +ENDCLASS. + +START-OF-SELECTION. + DATA(go_report) = NEW lcl_report( ). + DATA(gt_lines) = go_report->select_totals( iv_from = p_from it_region = s_reg[] ). + go_report->display( CHANGING ct_lines = gt_lines ). + +CLASS ltc_report DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + CLASS-DATA go_env TYPE REF TO if_osql_test_environment. + CLASS-METHODS class_setup. + CLASS-METHODS class_teardown. + METHODS totals_per_region FOR TESTING. + METHODS region_filter FOR TESTING. +ENDCLASS. + +CLASS ltc_report IMPLEMENTATION. + METHOD class_setup. + go_env = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( '{{P}}SALE' ) ) ). + DATA lt_sale TYPE STANDARD TABLE OF {{p}}sale WITH EMPTY KEY. + lt_sale = VALUE #( ( sale_id = 1 region = 'A' sale_date = '20260101' amount = 10 ) + ( sale_id = 2 region = 'A' sale_date = '20260102' amount = 5 ) + ( sale_id = 3 region = 'B' sale_date = '20260101' amount = 40 ) ). + go_env->insert_test_data( lt_sale ). + ENDMETHOD. + + METHOD class_teardown. + go_env->destroy( ). + ENDMETHOD. + + METHOD totals_per_region. + DATA(lt_lines) = NEW lcl_report( )->select_totals( iv_from = '20260101' it_region = VALUE #( ) ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_lines ) ). + cl_abap_unit_assert=>assert_equals( exp = 'B' act = lt_lines[ 1 ]-region ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lt_lines[ 2 ]-count ). + ENDMETHOD. + + METHOD region_filter. + DATA(lt_lines) = NEW lcl_report( )->select_totals( + iv_from = '20260101' it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'A' ) ) ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ). + cl_abap_unit_assert=>assert_equals( exp = CONV {{p}}sale-amount( 15 ) act = lt_lines[ 1 ]-total ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T14/seed/sale.tabl.asabap b/tasks/T14/seed/sale.tabl.asabap new file mode 100644 index 0000000..772a87a --- /dev/null +++ b/tasks/T14/seed/sale.tabl.asabap @@ -0,0 +1,12 @@ +@EndUserText.label : 'Sales' +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +@AbapCatalog.tableCategory : #TRANSPARENT +@AbapCatalog.deliveryClass : #A +@AbapCatalog.dataMaintenance : #RESTRICTED +define table {{p}}sale { + key client : abap.clnt not null; + key sale_id : abap.int4 not null; + region : abap.char(4); + sale_date : abap.dats; + amount : abap.dec(15,2); +} diff --git a/tasks/T14/seed/t14_seed.clas.abap b/tasks/T14/seed/t14_seed.clas.abap new file mode 100644 index 0000000..091973c --- /dev/null +++ b/tasks/T14/seed/t14_seed.clas.abap @@ -0,0 +1,17 @@ +CLASS {{p}}t14_seed DEFINITION PUBLIC FINAL CREATE PUBLIC. + PUBLIC SECTION. + INTERFACES if_oo_adt_classrun. +ENDCLASS. + +CLASS {{p}}t14_seed IMPLEMENTATION. + METHOD if_oo_adt_classrun~main. + DELETE FROM {{p}}sale. + INSERT {{p}}sale FROM TABLE @( VALUE #( + ( sale_id = 1 region = 'EU' sale_date = '20260110' amount = '100.00' ) + ( sale_id = 2 region = 'EU' sale_date = '20260210' amount = '50.00' ) + ( sale_id = 3 region = 'US' sale_date = '20260115' amount = '300.00' ) + ( sale_id = 4 region = 'APAC' sale_date = '20251231' amount = '500.00' ) + ( sale_id = 5 region = 'APAC' sale_date = '20260301' amount = '20.00' ) ) ). + out->write( sy-dbcnt ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T14/spec.md b/tasks/T14/spec.md new file mode 100644 index 0000000..ea86a76 --- /dev/null +++ b/tasks/T14/spec.md @@ -0,0 +1,33 @@ +# 1. Goal +Show the sales total for each region in an ALV list. The sales team uses the +list to compare regions for a period. + +# 2. Open questions +None. + +# 3. Context +- Table {{P}}SALE exists in package $TMP. It contains data. + Fields: SALE_ID (INT4, key), REGION (CHAR 4), SALE_DATE (DATS), AMOUNT (DEC 15,2). + +# 4. Contract +- Create the executable program {{P}}SALES_SUM in package $TMP. +- Selection screen: + - Parameter P_FROM, type DATS, mandatory: the first sale date to include. + - Select-option S_REG for the region. +- Show the result with CL_SALV_TABLE. The output table has these columns, with + these names: REGION, TOTAL (sum of AMOUNT), COUNT (number of sales). + +# 5. Business rules +1. Include a sale only if SALE_DATE is on or after P_FROM and REGION is in S_REG. +2. Show one line for each region that has at least one included sale. +3. Sort the lines by TOTAL, the highest total first. + +# 6. Constraints +- Release target: 7.58. +- Coding standards: Clean ABAP. Read the data with one database access. +- Out of scope: do not change {{P}}SALE. + +# 7. Acceptance +- The program is active and has no syntax error. +- The hidden tests pass. +- Write ABAP Unit tests as local test classes in the program. diff --git a/tasks/T14/task.json b/tasks/T14/task.json new file mode 100644 index 0000000..c34013e --- /dev/null +++ b/tasks/T14/task.json @@ -0,0 +1,18 @@ +{ + "id": "T14", "category": "B", "object_type": "PROG", "difficulty": 2, + "release_target": "v758", "expected_outcome": "implement", + "budget": {"max_tool_calls": 40, "max_activations": 10}, + "seed": [ + {"type": "TABL", "name": "{{P}}SALE", "file": "seed/sale.tabl.asabap", "description": "Sales"}, + {"type": "CLAS", "name": "{{P}}T14_SEED", "file": "seed/t14_seed.clas.abap", "description": "T14 seed data", "run": true} + ], + "contract": [ + {"type": "PROG", "name": "{{P}}SALES_SUM", "parameters": ["p_from", "s_reg"]} + ], + "out_of_scope": ["{{P}}SALE"], + "hidden_tests": [{"type": "CLAS", "name": "{{P}}T14_HIDDEN", "file": "hidden/t14_hidden.clas.abap", + "description": "T14 hidden tests"}], + "reference": [{"type": "PROG", "name": "{{P}}SALES_SUM", "file": "reference/sales_sum.prog.abap", + "description": "Sales per region"}], + "craft_checks": ["db_operation_in_loop", "method_length"] +} diff --git a/tasks/T15/hidden/t15_hidden.clas.abap b/tasks/T15/hidden/t15_hidden.clas.abap new file mode 100644 index 0000000..18d18da --- /dev/null +++ b/tasks/T15/hidden/t15_hidden.clas.abap @@ -0,0 +1,64 @@ +CLASS {{p}}t15_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + CLASS-DATA go_env TYPE REF TO if_cds_test_environment. + CLASS-METHODS class_setup. + CLASS-METHODS class_teardown. + METHODS setup. + METHODS totals FOR TESTING. + METHODS item_counts FOR TESTING. + METHODS order_without_items FOR TESTING. + METHODS customer_shown FOR TESTING. + METHODS decimals FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t15_hidden IMPLEMENTATION. + METHOD class_setup. + go_env = cl_cds_test_environment=>create( i_for_entity = '{{P}}I_ORDER_TOTAL' ). + ENDMETHOD. + + METHOD class_teardown. + go_env->destroy( ). + ENDMETHOD. + + METHOD setup. + DATA lt_ord TYPE STANDARD TABLE OF {{p}}ord WITH EMPTY KEY. + DATA lt_itm TYPE STANDARD TABLE OF {{p}}itm WITH EMPTY KEY. + go_env->clear_doubles( ). + lt_ord = VALUE #( ( order_id = 'O1' customer = 'ALPHA' ) + ( order_id = 'O2' customer = 'BETA' ) + ( order_id = 'O3' customer = 'GAMMA' ) ). + lt_itm = VALUE #( ( order_id = 'O1' item_no = 1 quantity = 3 price = '10.00' ) + ( order_id = 'O1' item_no = 2 quantity = 1 price = '2.50' ) + ( order_id = 'O1' item_no = 3 quantity = 4 price = '0.25' ) + ( order_id = 'O2' item_no = 1 quantity = 10 price = '1.99' ) ). + go_env->insert_test_data( lt_ord ). + go_env->insert_test_data( lt_itm ). + ENDMETHOD. + + METHOD totals. + SELECT SINGLE total_amount FROM {{p}}i_order_total WHERE order_id = 'O1' INTO @DATA(lv_total). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '33.50' ) act = CONV decfloat34( lv_total ) ). + ENDMETHOD. + + METHOD item_counts. + SELECT SINGLE item_count FROM {{p}}i_order_total WHERE order_id = 'O1' INTO @DATA(lv_count). + cl_abap_unit_assert=>assert_equals( exp = 3 act = lv_count ). + ENDMETHOD. + + METHOD order_without_items. + SELECT COUNT(*) FROM {{p}}i_order_total INTO @DATA(lv_rows). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_rows ). + ENDMETHOD. + + METHOD customer_shown. + SELECT SINGLE customer FROM {{p}}i_order_total WHERE order_id = 'O2' INTO @DATA(lv_customer). + cl_abap_unit_assert=>assert_equals( exp = 'BETA' act = lv_customer ). + ENDMETHOD. + + METHOD decimals. + SELECT SINGLE total_amount FROM {{p}}i_order_total WHERE order_id = 'O2' INTO @DATA(lv_total). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '19.90' ) act = CONV decfloat34( lv_total ) ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T15/reference/i_order_total.ddls.asddls b/tasks/T15/reference/i_order_total.ddls.asddls new file mode 100644 index 0000000..468f462 --- /dev/null +++ b/tasks/T15/reference/i_order_total.ddls.asddls @@ -0,0 +1,14 @@ +@AccessControl.authorizationCheck: #NOT_REQUIRED +@EndUserText.label: 'Order totals' +define view entity {{P}}I_ORDER_TOTAL + as select from {{p}}ord as head + inner join {{p}}itm as item on item.order_id = head.order_id +{ + key head.order_id as order_id, + head.customer as customer, + count( * ) as item_count, + sum( cast( item.quantity as abap.dec( 10, 0 ) ) * item.price ) as total_amount +} +group by + head.order_id, + head.customer diff --git a/tasks/T15/reference/t15_test.clas.abap b/tasks/T15/reference/t15_test.clas.abap new file mode 100644 index 0000000..838dd67 --- /dev/null +++ b/tasks/T15/reference/t15_test.clas.abap @@ -0,0 +1,38 @@ +CLASS {{p}}t15_test DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + CLASS-DATA go_env TYPE REF TO if_cds_test_environment. + CLASS-METHODS class_setup. + CLASS-METHODS class_teardown. + METHODS setup. + METHODS total_of_one_order FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t15_test IMPLEMENTATION. + METHOD class_setup. + go_env = cl_cds_test_environment=>create( i_for_entity = '{{P}}I_ORDER_TOTAL' ). + ENDMETHOD. + + METHOD class_teardown. + go_env->destroy( ). + ENDMETHOD. + + METHOD setup. + go_env->clear_doubles( ). + ENDMETHOD. + + METHOD total_of_one_order. + DATA lt_ord TYPE STANDARD TABLE OF {{p}}ord WITH EMPTY KEY. + DATA lt_itm TYPE STANDARD TABLE OF {{p}}itm WITH EMPTY KEY. + lt_ord = VALUE #( ( order_id = 'A' customer = 'C1' ) ). + lt_itm = VALUE #( ( order_id = 'A' item_no = 1 quantity = 2 price = '1.50' ) + ( order_id = 'A' item_no = 2 quantity = 1 price = '4.00' ) ). + go_env->insert_test_data( lt_ord ). + go_env->insert_test_data( lt_itm ). + SELECT SINGLE item_count, total_amount FROM {{p}}i_order_total WHERE order_id = 'A' + INTO @DATA(ls_total). + cl_abap_unit_assert=>assert_equals( exp = 2 act = ls_total-item_count ). + cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 7 ) act = CONV decfloat34( ls_total-total_amount ) ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks/T15/seed/itm.tabl.asabap b/tasks/T15/seed/itm.tabl.asabap new file mode 100644 index 0000000..9c0ffc8 --- /dev/null +++ b/tasks/T15/seed/itm.tabl.asabap @@ -0,0 +1,13 @@ +@EndUserText.label : 'Order item' +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +@AbapCatalog.tableCategory : #TRANSPARENT +@AbapCatalog.deliveryClass : #A +@AbapCatalog.dataMaintenance : #RESTRICTED +define table {{p}}itm { + key client : abap.clnt not null; + key order_id : abap.char(10) not null; + key item_no : abap.int4 not null; + product : abap.char(10); + quantity : abap.int4; + price : abap.dec(15,2); +} diff --git a/tasks/T15/seed/ord.tabl.asabap b/tasks/T15/seed/ord.tabl.asabap new file mode 100644 index 0000000..3746482 --- /dev/null +++ b/tasks/T15/seed/ord.tabl.asabap @@ -0,0 +1,11 @@ +@EndUserText.label : 'Order header' +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +@AbapCatalog.tableCategory : #TRANSPARENT +@AbapCatalog.deliveryClass : #A +@AbapCatalog.dataMaintenance : #RESTRICTED +define table {{p}}ord { + key client : abap.clnt not null; + key order_id : abap.char(10) not null; + customer : abap.char(10); + order_date : abap.dats; +} diff --git a/tasks/T15/spec.md b/tasks/T15/spec.md new file mode 100644 index 0000000..4ac453d --- /dev/null +++ b/tasks/T15/spec.md @@ -0,0 +1,33 @@ +# 1. Goal +Give the total value of each order. A report and an OData service will read the +totals, so the calculation must be in the database, in a CDS view. + +# 2. Open questions +None. + +# 3. Context +- Table {{P}}ORD (order header), package $TMP. Key: ORDER_ID (CHAR 10). + Fields: CUSTOMER (CHAR 10), ORDER_DATE (DATS). +- Table {{P}}ITM (order item), package $TMP. Key: ORDER_ID (CHAR 10), ITEM_NO (INT4). + Fields: PRODUCT (CHAR 10), QUANTITY (INT4), PRICE (DEC 15,2, price for one unit). + +# 4. Contract +- Create the CDS view entity {{P}}I_ORDER_TOTAL in package $TMP. +- Elements, with these names: ORDER_ID (key), CUSTOMER, ITEM_COUNT, TOTAL_AMOUNT. +- No authorization check (#NOT_REQUIRED). + +# 5. Business rules +1. Show one row for each order that has at least one item. Do not show an order + without items. +2. ITEM_COUNT is the number of items of the order. +3. TOTAL_AMOUNT is the sum of QUANTITY multiplied by PRICE for all items of the + order, with 2 decimals. + +# 6. Constraints +- Release target: 7.58. +- Out of scope: do not change {{P}}ORD and {{P}}ITM. + +# 7. Acceptance +- The view is active and has no syntax error. +- The hidden tests pass. +- Write ABAP Unit tests with CL_CDS_TEST_ENVIRONMENT in a global test class. diff --git a/tasks/T15/task.json b/tasks/T15/task.json new file mode 100644 index 0000000..719bc53 --- /dev/null +++ b/tasks/T15/task.json @@ -0,0 +1,22 @@ +{ + "id": "T15", "category": "B", "object_type": "DDLS", "difficulty": 2, + "release_target": "v758", "expected_outcome": "implement", + "budget": {"max_tool_calls": 40, "max_activations": 10}, + "seed": [ + {"type": "TABL", "name": "{{P}}ORD", "file": "seed/ord.tabl.asabap", "description": "Order header"}, + {"type": "TABL", "name": "{{P}}ITM", "file": "seed/itm.tabl.asabap", "description": "Order item"} + ], + "contract": [ + {"type": "DDLS", "name": "{{P}}I_ORDER_TOTAL", + "fields": ["ORDER_ID", "CUSTOMER", "ITEM_COUNT", "TOTAL_AMOUNT"]} + ], + "out_of_scope": ["{{P}}ORD", "{{P}}ITM"], + "hidden_tests": [{"type": "CLAS", "name": "{{P}}T15_HIDDEN", "file": "hidden/t15_hidden.clas.abap", + "description": "T15 hidden tests"}], + "reference": [ + {"type": "DDLS", "name": "{{P}}I_ORDER_TOTAL", "file": "reference/i_order_total.ddls.asddls", + "description": "Order totals"}, + {"type": "CLAS", "name": "{{P}}T15_TEST", "file": "reference/t15_test.clas.abap", "description": "T15 own tests"} + ], + "craft_checks": [] +} diff --git a/tasks_gen/eval/G0001/generation.json b/tasks_gen/eval/G0001/generation.json new file mode 100644 index 0000000..96b2ac0 --- /dev/null +++ b/tasks_gen/eval/G0001/generation.json @@ -0,0 +1,24 @@ +{ + "id": "G0001", + "pool": "eval", + "object_type": "CLAS", + "category": "C", + "attempts": [ + { + "stage": "validate", + "oracle": 0, + "null": 0 + }, + { + "stage": "validate", + "oracle": null, + "null": null + }, + { + "stage": "validate", + "oracle": 89.2, + "null": 0 + } + ], + "accepted": false +} \ No newline at end of file diff --git a/tasks_gen/eval/G0001/hidden/t07_hidden.clas.abap b/tasks_gen/eval/G0001/hidden/t07_hidden.clas.abap new file mode 100644 index 0000000..b3a3678 --- /dev/null +++ b/tasks_gen/eval/G0001/hidden/t07_hidden.clas.abap @@ -0,0 +1,335 @@ +CLASS {{P}}T07_HIDDEN DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + DATA mo_cut TYPE REF TO {{P}}TOUR_PLANNER. + + METHODS setup. + METHODS add_stop + IMPORTING iv_tour_id TYPE {{P}}TOUR_PLANNER=>ty_tour_id + iv_stop_no TYPE i + iv_customer TYPE {{P}}TOUR_PLANNER=>ty_customer + iv_packages TYPE i + iv_weight TYPE {{P}}TOUR_PLANNER=>ty_weight. + METHODS assert_item + IMPORTING is_exp TYPE {{P}}TOUR_PLANNER=>ty_item + is_act TYPE {{P}}TOUR_PLANNER=>ty_item + iv_msg TYPE string. + METHODS assert_weight + IMPORTING iv_exp TYPE {{P}}TOUR_PLANNER=>ty_weight + iv_act TYPE {{P}}TOUR_PLANNER=>ty_weight + iv_msg TYPE string. + + METHODS add_one_item FOR TESTING. + METHODS items_are_sorted FOR TESTING. + METHODS duplicate_is_rejected FOR TESTING. + METHODS empty_tour_id_is_invalid FOR TESTING. + METHODS bad_stop_no_is_invalid FOR TESTING. + METHODS negative_values_are_invalid FOR TESTING. + METHODS zero_values_are_valid FOR TESTING. + METHODS stops_of_tour_are_sorted FOR TESTING. + METHODS unknown_tour_has_no_stops FOR TESTING. + METHODS find_stop_works FOR TESTING. + METHODS summary_totals FOR TESTING. + METHODS remove_tour_works FOR TESTING. +ENDCLASS. + + +CLASS {{P}}T07_HIDDEN IMPLEMENTATION. + METHOD setup. + mo_cut = NEW {{P}}TOUR_PLANNER( ). + ENDMETHOD. + + METHOD add_stop. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = iv_tour_id + iv_stop_no = iv_stop_no + iv_customer = iv_customer + iv_packages = iv_packages + iv_weight = iv_weight ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-ok + act = lv_status + msg = `add_item must store a valid stop` ). + ENDMETHOD. + + METHOD assert_item. + cl_abap_unit_assert=>assert_equals( exp = is_exp-tour_id act = is_act-tour_id msg = iv_msg ). + cl_abap_unit_assert=>assert_equals( exp = is_exp-stop_no act = is_act-stop_no msg = iv_msg ). + cl_abap_unit_assert=>assert_equals( exp = is_exp-customer act = is_act-customer msg = iv_msg ). + cl_abap_unit_assert=>assert_equals( exp = is_exp-packages act = is_act-packages msg = iv_msg ). + assert_weight( iv_exp = is_exp-weight iv_act = is_act-weight iv_msg = iv_msg ). + ENDMETHOD. + + METHOD assert_weight. + cl_abap_unit_assert=>assert_equals( exp = iv_exp act = iv_act msg = iv_msg ). + ENDMETHOD. + + METHOD add_one_item. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 3 + iv_weight = '12.50' ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_item ) ). + assert_item( is_exp = VALUE #( tour_id = 'T001' + stop_no = 1 + customer = 'CUST01' + packages = 3 + weight = '12.50' ) + is_act = lt_item[ 1 ] + iv_msg = `one stored stop` ). + ENDMETHOD. + + METHOD items_are_sorted. + add_stop( iv_tour_id = 'T002' + iv_stop_no = 2 + iv_customer = 'CUST02' + iv_packages = 1 + iv_weight = '5.00' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 3 + iv_customer = 'CUST03' + iv_packages = 2 + iv_weight = '6.00' ). + add_stop( iv_tour_id = 'T002' + iv_stop_no = 1 + iv_customer = 'CUST04' + iv_packages = 4 + iv_weight = '7.00' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST05' + iv_packages = 5 + iv_weight = '8.00' ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 4 act = lines( lt_item ) ). + cl_abap_unit_assert=>assert_equals( exp = 'T001' act = lt_item[ 1 ]-tour_id msg = `row 1 tour` ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lt_item[ 1 ]-stop_no msg = `row 1 stop` ). + cl_abap_unit_assert=>assert_equals( exp = 'T001' act = lt_item[ 2 ]-tour_id msg = `row 2 tour` ). + cl_abap_unit_assert=>assert_equals( exp = 3 act = lt_item[ 2 ]-stop_no msg = `row 2 stop` ). + cl_abap_unit_assert=>assert_equals( exp = 'T002' act = lt_item[ 3 ]-tour_id msg = `row 3 tour` ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lt_item[ 3 ]-stop_no msg = `row 3 stop` ). + cl_abap_unit_assert=>assert_equals( exp = 'T002' act = lt_item[ 4 ]-tour_id msg = `row 4 tour` ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lt_item[ 4 ]-stop_no msg = `row 4 stop` ). + ENDMETHOD. + + METHOD duplicate_is_rejected. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 3 + iv_weight = '12.50' ). + + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST99' + iv_packages = 9 + iv_weight = '99.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-duplicate act = lv_status ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_item ) ). + assert_item( is_exp = VALUE #( tour_id = 'T001' + stop_no = 1 + customer = 'CUST01' + packages = 3 + weight = '12.50' ) + is_act = lt_item[ 1 ] + iv_msg = `stored stop keeps its values` ). + ENDMETHOD. + + METHOD empty_tour_id_is_invalid. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = '' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-invalid act = lv_status ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_item ) ). + ENDMETHOD. + + METHOD bad_stop_no_is_invalid. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 0 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-invalid act = lv_status ). + + lv_status = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = -1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-invalid act = lv_status ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_item ) ). + ENDMETHOD. + + METHOD negative_values_are_invalid. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = -1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-invalid act = lv_status ). + + lv_status = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '-1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-invalid act = lv_status ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_item ) ). + ENDMETHOD. + + METHOD zero_values_are_valid. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = '' + iv_packages = 0 + iv_weight = '0.00' ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_item ) ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lt_item[ 1 ]-packages ). + ENDMETHOD. + + METHOD stops_of_tour_are_sorted. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 2 + iv_customer = 'CUST02' + iv_packages = 1 + iv_weight = '5.00' ). + add_stop( iv_tour_id = 'T002' + iv_stop_no = 1 + iv_customer = 'CUST03' + iv_packages = 2 + iv_weight = '6.00' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST04' + iv_packages = 4 + iv_weight = '7.00' ). + + DATA(lt_item) = mo_cut->stops_of_tour( 'T001' ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_item ) ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lt_item[ 1 ]-stop_no ). + cl_abap_unit_assert=>assert_equals( exp = 'CUST04' act = lt_item[ 1 ]-customer ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lt_item[ 2 ]-stop_no ). + ENDMETHOD. + + METHOD unknown_tour_has_no_stops. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + + DATA(lt_item) = mo_cut->stops_of_tour( 'T999' ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_item ) ). + ENDMETHOD. + + METHOD find_stop_works. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 3 + iv_weight = '12.50' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 2 + iv_customer = 'CUST02' + iv_packages = 4 + iv_weight = '13.50' ). + + assert_item( is_exp = VALUE #( tour_id = 'T001' + stop_no = 2 + customer = 'CUST02' + packages = 4 + weight = '13.50' ) + is_act = mo_cut->find_stop( iv_tour_id = 'T001' iv_stop_no = 2 ) + iv_msg = `known stop` ). + + DATA(ls_missing) = mo_cut->find_stop( iv_tour_id = 'T001' iv_stop_no = 9 ). + cl_abap_unit_assert=>assert_initial( act = ls_missing-tour_id ). + cl_abap_unit_assert=>assert_initial( act = ls_missing-stop_no ). + cl_abap_unit_assert=>assert_initial( act = ls_missing-customer ). + cl_abap_unit_assert=>assert_initial( act = ls_missing-packages ). + cl_abap_unit_assert=>assert_initial( act = ls_missing-weight ). + + ls_missing = mo_cut->find_stop( iv_tour_id = 'T999' iv_stop_no = 1 ). + cl_abap_unit_assert=>assert_initial( act = ls_missing-tour_id ). + ENDMETHOD. + + METHOD summary_totals. + add_stop( iv_tour_id = 'T002' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 2 + iv_weight = '10.25' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST02' + iv_packages = 3 + iv_weight = '20.50' ). + add_stop( iv_tour_id = 'T002' + iv_stop_no = 2 + iv_customer = 'CUST03' + iv_packages = 4 + iv_weight = '30.75' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 2 + iv_customer = 'CUST04' + iv_packages = 5 + iv_weight = '40.25' ). + + DATA(lt_tour) = mo_cut->tour_summary( ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_tour ) ). + + cl_abap_unit_assert=>assert_equals( exp = 'T001' act = lt_tour[ 1 ]-tour_id ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lt_tour[ 1 ]-stops ). + cl_abap_unit_assert=>assert_equals( exp = 8 act = lt_tour[ 1 ]-packages ). + assert_weight( iv_exp = '60.75' iv_act = lt_tour[ 1 ]-weight iv_msg = `T001 weight` ). + + cl_abap_unit_assert=>assert_equals( exp = 'T002' act = lt_tour[ 2 ]-tour_id ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lt_tour[ 2 ]-stops ). + cl_abap_unit_assert=>assert_equals( exp = 6 act = lt_tour[ 2 ]-packages ). + assert_weight( iv_exp = '41.00' iv_act = lt_tour[ 2 ]-weight iv_msg = `T002 weight` ). + ENDMETHOD. + + METHOD remove_tour_works. + add_stop( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + add_stop( iv_tour_id = 'T001' + iv_stop_no = 2 + iv_customer = 'CUST02' + iv_packages = 2 + iv_weight = '2.00' ). + add_stop( iv_tour_id = 'T002' + iv_stop_no = 1 + iv_customer = 'CUST03' + iv_packages = 3 + iv_weight = '3.00' ). + + cl_abap_unit_assert=>assert_equals( exp = 2 act = mo_cut->remove_tour( 'T001' ) ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_item ) ). + cl_abap_unit_assert=>assert_equals( exp = 'T002' act = lt_item[ 1 ]-tour_id ). + + cl_abap_unit_assert=>assert_equals( exp = 0 act = mo_cut->remove_tour( 'T001' ) ). + + DATA(lt_tour) = mo_cut->tour_summary( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_tour ) ). + cl_abap_unit_assert=>assert_equals( exp = 'T002' act = lt_tour[ 1 ]-tour_id ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks_gen/eval/G0001/reference/tour_planner.clas.abap b/tasks_gen/eval/G0001/reference/tour_planner.clas.abap new file mode 100644 index 0000000..f30fa23 --- /dev/null +++ b/tasks_gen/eval/G0001/reference/tour_planner.clas.abap @@ -0,0 +1,143 @@ +CLASS {{P}}TOUR_PLANNER DEFINITION PUBLIC FINAL CREATE PUBLIC. + PUBLIC SECTION. + TYPES ty_status TYPE c LENGTH 10. + TYPES ty_tour_id TYPE c LENGTH 4. + TYPES ty_customer TYPE c LENGTH 10. + TYPES ty_weight TYPE p LENGTH 8 DECIMALS 2. + + CONSTANTS: + BEGIN OF c_status, + ok TYPE ty_status VALUE 'OK', + invalid TYPE ty_status VALUE 'INVALID', + duplicate TYPE ty_status VALUE 'DUPLICATE', + END OF c_status. + + TYPES: + BEGIN OF ty_item, + tour_id TYPE ty_tour_id, + stop_no TYPE i, + customer TYPE ty_customer, + packages TYPE i, + weight TYPE ty_weight, + END OF ty_item, + tt_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY, + + BEGIN OF ty_tour, + tour_id TYPE ty_tour_id, + stops TYPE i, + packages TYPE i, + weight TYPE ty_weight, + END OF ty_tour, + tt_tour TYPE STANDARD TABLE OF ty_tour WITH EMPTY KEY. + + METHODS add_item + IMPORTING iv_tour_id TYPE ty_tour_id + iv_stop_no TYPE i + iv_customer TYPE ty_customer + iv_packages TYPE i + iv_weight TYPE ty_weight + RETURNING VALUE(rv_status) TYPE ty_status. + + METHODS remove_tour + IMPORTING iv_tour_id TYPE ty_tour_id + RETURNING VALUE(rv_count) TYPE i. + + METHODS items + RETURNING VALUE(rt_item) TYPE tt_item. + + METHODS stops_of_tour + IMPORTING iv_tour_id TYPE ty_tour_id + RETURNING VALUE(rt_item) TYPE tt_item. + + METHODS find_stop + IMPORTING iv_tour_id TYPE ty_tour_id + iv_stop_no TYPE i + RETURNING VALUE(rs_item) TYPE ty_item. + + METHODS tour_summary + RETURNING VALUE(rt_tour) TYPE tt_tour. + + PRIVATE SECTION. + TYPES tt_stop TYPE SORTED TABLE OF ty_item WITH UNIQUE KEY tour_id stop_no. + + DATA mt_stop TYPE tt_stop. + + METHODS is_invalid + IMPORTING iv_tour_id TYPE ty_tour_id + iv_stop_no TYPE i + iv_packages TYPE i + iv_weight TYPE ty_weight + RETURNING VALUE(rv_invalid) TYPE abap_bool. +ENDCLASS. + + +CLASS {{P}}TOUR_PLANNER IMPLEMENTATION. + METHOD add_item. + IF is_invalid( iv_tour_id = iv_tour_id + iv_stop_no = iv_stop_no + iv_packages = iv_packages + iv_weight = iv_weight ) = abap_true. + rv_status = c_status-invalid. + RETURN. + ENDIF. + + DATA(ls_stop) = VALUE ty_item( tour_id = iv_tour_id + stop_no = iv_stop_no + customer = iv_customer + packages = iv_packages + weight = iv_weight ). + INSERT ls_stop INTO TABLE mt_stop. + + IF sy-subrc <> 0. + rv_status = c_status-duplicate. + RETURN. + ENDIF. + + rv_status = c_status-ok. + ENDMETHOD. + + METHOD remove_tour. + DELETE mt_stop WHERE tour_id = iv_tour_id. + rv_count = sy-dbcnt. + ENDMETHOD. + + METHOD items. + rt_item = mt_stop. + ENDMETHOD. + + METHOD stops_of_tour. + rt_item = VALUE #( FOR ls_stop IN mt_stop WHERE ( tour_id = iv_tour_id ) ( ls_stop ) ). + ENDMETHOD. + + METHOD find_stop. + READ TABLE mt_stop WITH KEY tour_id = iv_tour_id + stop_no = iv_stop_no INTO rs_item. + IF sy-subrc <> 0. + CLEAR rs_item. + ENDIF. + ENDMETHOD. + + METHOD tour_summary. + DATA lt_tour TYPE SORTED TABLE OF ty_tour WITH UNIQUE KEY tour_id. + + LOOP AT mt_stop INTO DATA(ls_stop). + READ TABLE lt_tour WITH KEY tour_id = ls_stop-tour_id ASSIGNING FIELD-SYMBOL(). + IF sy-subrc <> 0. + INSERT VALUE ty_tour( tour_id = ls_stop-tour_id ) + INTO TABLE lt_tour ASSIGNING . + ENDIF. + -stops = -stops + 1. + -packages = -packages + ls_stop-packages. + -weight = -weight + ls_stop-weight. + ENDLOOP. + + rt_tour = lt_tour. + ENDMETHOD. + + METHOD is_invalid. + rv_invalid = xsdbool( iv_tour_id IS INITIAL + OR iv_stop_no < 1 + OR iv_packages < 0 + OR iv_weight < 0 ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks_gen/eval/G0001/reference/tour_planner.testclasses.abap b/tasks_gen/eval/G0001/reference/tour_planner.testclasses.abap new file mode 100644 index 0000000..321033c --- /dev/null +++ b/tasks_gen/eval/G0001/reference/tour_planner.testclasses.abap @@ -0,0 +1,93 @@ +CLASS ltc_tour_planner DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + PRIVATE SECTION. + DATA mo_cut TYPE REF TO {{P}}TOUR_PLANNER. + METHODS setup. + METHODS add_and_read_item FOR TESTING. + METHODS duplicate_is_rejected FOR TESTING. + METHODS invalid_input_is_rejected FOR TESTING. + METHODS summary_totals FOR TESTING. + METHODS remove_tour_works FOR TESTING. +ENDCLASS. + + +CLASS ltc_tour_planner IMPLEMENTATION. + METHOD setup. + mo_cut = NEW {{P}}TOUR_PLANNER( ). + ENDMETHOD. + + METHOD add_and_read_item. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 3 + iv_weight = '12.50' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-ok act = lv_status ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_item ) ). + cl_abap_unit_assert=>assert_equals( exp = 'CUST01' act = lt_item[ 1 ]-customer ). + ENDMETHOD. + + METHOD duplicate_is_rejected. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 3 + iv_weight = '12.50' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-ok act = lv_status ). + + lv_status = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST02' + iv_packages = 1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-duplicate act = lv_status ). + ENDMETHOD. + + METHOD invalid_input_is_rejected. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = '' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-invalid act = lv_status ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_item ) ). + ENDMETHOD. + + METHOD summary_totals. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 2 + iv_weight = '10.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-ok act = lv_status ). + + lv_status = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 2 + iv_customer = 'CUST02' + iv_packages = 3 + iv_weight = '20.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-ok act = lv_status ). + + DATA(lt_tour) = mo_cut->tour_summary( ). + cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_tour ) ). + cl_abap_unit_assert=>assert_equals( exp = 2 act = lt_tour[ 1 ]-stops ). + cl_abap_unit_assert=>assert_equals( exp = 5 act = lt_tour[ 1 ]-packages ). + ENDMETHOD. + + METHOD remove_tour_works. + DATA(lv_status) = mo_cut->add_item( iv_tour_id = 'T001' + iv_stop_no = 1 + iv_customer = 'CUST01' + iv_packages = 1 + iv_weight = '1.00' ). + cl_abap_unit_assert=>assert_equals( exp = {{P}}TOUR_PLANNER=>c_status-ok act = lv_status ). + + cl_abap_unit_assert=>assert_equals( exp = 1 act = mo_cut->remove_tour( 'T001' ) ). + + DATA(lt_item) = mo_cut->items( ). + cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_item ) ). + ENDMETHOD. +ENDCLASS. diff --git a/tasks_gen/eval/G0001/seed/if_tour_planner.intf.abap b/tasks_gen/eval/G0001/seed/if_tour_planner.intf.abap new file mode 100644 index 0000000..4d2c424 --- /dev/null +++ b/tasks_gen/eval/G0001/seed/if_tour_planner.intf.abap @@ -0,0 +1,55 @@ +INTERFACE {{p}}if_tour_planner PUBLIC. + TYPES ty_status TYPE c LENGTH 10. + + CONSTANTS: + BEGIN OF c_status, + ok TYPE ty_status VALUE 'OK', + invalid TYPE ty_status VALUE 'INVALID', + duplicate TYPE ty_status VALUE 'DUPLICATE', + END OF c_status. + + TYPES: + BEGIN OF ty_item, + tour_id TYPE c LENGTH 4, + stop_no TYPE i, + customer TYPE c LENGTH 10, + packages TYPE i, + weight TYPE p LENGTH 8 DECIMALS 2, + END OF ty_item, + tt_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY, + + BEGIN OF ty_tour, + tour_id TYPE c LENGTH 4, + stops TYPE i, + packages TYPE i, + weight TYPE p LENGTH 8 DECIMALS 2, + END OF ty_tour, + tt_tour TYPE STANDARD TABLE OF ty_tour WITH EMPTY KEY. + + METHODS add_item + IMPORTING iv_tour_id TYPE c LENGTH 4 + iv_stop_no TYPE i + iv_customer TYPE c LENGTH 10 + iv_packages TYPE i + iv_weight TYPE p LENGTH 8 DECIMALS 2 + RETURNING VALUE(rv_status) TYPE ty_status. + + METHODS remove_tour + IMPORTING iv_tour_id TYPE c LENGTH 4 + RETURNING VALUE(rv_count) TYPE i. + + METHODS items + RETURNING VALUE(rt_item) TYPE tt_item. + + METHODS stops_of_tour + IMPORTING iv_tour_id TYPE c LENGTH 4 + RETURNING VALUE(rt_item) TYPE tt_item. + + METHODS find_stop + IMPORTING iv_tour_id TYPE c LENGTH 4 + iv_stop_no TYPE i + RETURNING VALUE(rs_item) TYPE ty_item. + + METHODS tour_summary + RETURNING VALUE(rt_tour) TYPE tt_tour. +ENDINTERFACE. diff --git a/tasks_gen/eval/G0001/spec.md b/tasks_gen/eval/G0001/spec.md new file mode 100644 index 0000000..6aeab3e --- /dev/null +++ b/tasks_gen/eval/G0001/spec.md @@ -0,0 +1,118 @@ +# 1. Goal +A transportation planner collects the stops of delivery tours. The planner needs one +class that stores the stops and reports the load of each tour. + +# 2. Open questions +None. + +# 3. Context +- The class {{P}}TOUR_PLANNER is new. Create it in package $TMP. +- A tour ID has 4 characters, for example T001. A stop number is a whole number. + A customer number has 10 characters. The weight is in kilograms with 2 decimal + places. +- No other object of this task exists. + +# 4. Contract +- Create the class {{P}}TOUR_PLANNER in package $TMP. +- The class is public, final, and has a public constructor without parameters. +- The class has these public elementary types: + + TYPES ty_status TYPE c LENGTH 10. + TYPES ty_tour_id TYPE c LENGTH 4. + TYPES ty_customer TYPE c LENGTH 10. + TYPES ty_weight TYPE p LENGTH 8 DECIMALS 2. + +- The class has this public constant structure: + + CONSTANTS: + BEGIN OF c_status, + ok TYPE ty_status VALUE 'OK', + invalid TYPE ty_status VALUE 'INVALID', + duplicate TYPE ty_status VALUE 'DUPLICATE', + END OF c_status. + +- The class has these public structured types: + + TYPES: + BEGIN OF ty_item, + tour_id TYPE ty_tour_id, + stop_no TYPE i, + customer TYPE ty_customer, + packages TYPE i, + weight TYPE ty_weight, + END OF ty_item, + tt_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY, + + BEGIN OF ty_tour, + tour_id TYPE ty_tour_id, + stops TYPE i, + packages TYPE i, + weight TYPE ty_weight, + END OF ty_tour, + tt_tour TYPE STANDARD TABLE OF ty_tour WITH EMPTY KEY. + +- The class has these public methods: + + METHODS add_item + IMPORTING iv_tour_id TYPE ty_tour_id + iv_stop_no TYPE i + iv_customer TYPE ty_customer + iv_packages TYPE i + iv_weight TYPE ty_weight + RETURNING VALUE(rv_status) TYPE ty_status. + + METHODS remove_tour + IMPORTING iv_tour_id TYPE ty_tour_id + RETURNING VALUE(rv_count) TYPE i. + + METHODS items + RETURNING VALUE(rt_item) TYPE tt_item. + + METHODS stops_of_tour + IMPORTING iv_tour_id TYPE ty_tour_id + RETURNING VALUE(rt_item) TYPE tt_item. + + METHODS find_stop + IMPORTING iv_tour_id TYPE ty_tour_id + iv_stop_no TYPE i + RETURNING VALUE(rs_item) TYPE ty_item. + + METHODS tour_summary + RETURNING VALUE(rt_tour) TYPE tt_tour. + +- Do not add other public methods. + +# 5. Business rules +1. The tour ID and the stop number identify a stop. +2. ADD_ITEM stores one stop. It returns INVALID and changes no data if one of these + conditions is true: + - IV_TOUR_ID is empty, + - IV_STOP_NO is smaller than 1, + - IV_PACKAGES is smaller than 0, + - IV_WEIGHT is smaller than 0. + IV_CUSTOMER can be empty. +3. ADD_ITEM returns DUPLICATE and changes no data if a stop with the same tour ID and + the same stop number is already stored. The stored stop keeps its values. +4. Otherwise ADD_ITEM stores the stop and returns OK. +5. ITEMS returns all stored stops, sorted by tour ID ascending and, for the same tour + ID, by stop number ascending. +6. STOPS_OF_TOUR returns the stops of one tour, sorted by stop number ascending. The + result is empty if the tour is not known. +7. FIND_STOP returns the stop with the given tour ID and stop number. If the stop is + not known, all components of the result are initial. +8. TOUR_SUMMARY returns one line for each tour that has at least one stop. A line + contains the tour ID, the number of stops, the sum of the packages and the sum of + the weight. The result is sorted by tour ID ascending. +9. REMOVE_TOUR deletes all stops of the tour and returns the number of deleted stops. + It returns 0 if the tour is not known. + +# 6. Constraints +- Release target: 7.58. +- Coding standards: Clean ABAP. Method length below 40 statements. No global variables. + No comment that restates the code. +- Out of scope: do not change objects that are not part of this task. + +# 7. Acceptance +- The class is active and has no syntax error. +- The hidden tests pass. +- Write your own ABAP Unit tests for the class. diff --git a/tasks_gen/eval/G0001/task.json b/tasks_gen/eval/G0001/task.json new file mode 100644 index 0000000..eda60b3 --- /dev/null +++ b/tasks_gen/eval/G0001/task.json @@ -0,0 +1,41 @@ +{ + "id": "G0001", + "category": "C", + "object_type": "CLAS", + "difficulty": 2, + "release_target": "v758", + "expected_outcome": "implement", + "budget": { + "max_tool_calls": 60, + "max_activations": 15 + }, + "seed": [], + "contract": [ + { + "type": "CLAS", + "name": "{{P}}TOUR_PLANNER" + } + ], + "out_of_scope": [], + "hidden_tests": [ + { + "type": "CLAS", + "name": "{{P}}T07_HIDDEN", + "file": "hidden/t07_hidden.clas.abap", + "description": "T07 hidden tests for the tour planner" + } + ], + "reference": [ + { + "type": "CLAS", + "name": "{{P}}TOUR_PLANNER", + "file": "reference/tour_planner.clas.abap", + "description": "Tour planner with stored stops and tour summary", + "testclasses_file": "reference/tour_planner.testclasses.abap" + } + ], + "craft_checks": [ + "method_length", + "db_operation_in_loop" + ] +} \ No newline at end of file diff --git a/tasks_gen/eval/G0002/generation.json b/tasks_gen/eval/G0002/generation.json new file mode 100644 index 0000000..7e859bb --- /dev/null +++ b/tasks_gen/eval/G0002/generation.json @@ -0,0 +1,25 @@ +{ + "id": "G0002", + "pool": "eval", + "object_type": "CLAS", + "category": "A", + "attempts": [ + { + "stage": "validate", + "oracle": 0, + "null": 0 + }, + { + "stage": "bundle", + "errors": [ + "invalid JSON: Extra data: line 1 column 14723 (char 14722)" + ] + }, + { + "stage": "validate", + "oracle": 100.0, + "null": 0 + } + ], + "accepted": true +} \ No newline at end of file diff --git a/tasks_gen/eval/G0002/hidden/t02_hidden.clas.abap b/tasks_gen/eval/G0002/hidden/t02_hidden.clas.abap new file mode 100644 index 0000000..945a841 --- /dev/null +++ b/tasks_gen/eval/G0002/hidden/t02_hidden.clas.abap @@ -0,0 +1,132 @@ +CLASS {{p}}t02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC + FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. + + PRIVATE SECTION. + CONSTANTS: + c_fri_05 TYPE d VALUE '20240105', + c_sat_06 TYPE d VALUE '20240106', + c_sun_07 TYPE d VALUE '20240107', + c_mon_08 TYPE d VALUE '20240108', + c_tue_09 TYPE d VALUE '20240109', + c_wed_10 TYPE d VALUE '20240110', + c_thu_11 TYPE d VALUE '20240111', + c_fri_12 TYPE d VALUE '20240112'. + + DATA mo_cut TYPE REF TO {{p}}delivery_schedule. + DATA mt_holidays TYPE {{p}}delivery_schedule=>ty_holidays. + DATA mt_no_holidays TYPE {{p}}delivery_schedule=>ty_holidays. + + METHODS setup. + METHODS weekend_is_not_working FOR TESTING. + METHODS working_day_is_true FOR TESTING. + METHODS holiday_is_not_working FOR TESTING. + METHODS next_day_same_for_workday FOR TESTING. + METHODS next_day_from_weekend FOR TESTING. + METHODS next_day_skips_holidays FOR TESTING. + METHODS add_zero_days FOR TESTING. + METHODS add_days_forward FOR TESTING. + METHODS add_forward_skips_holiday FOR TESTING. + METHODS add_days_backward FOR TESTING. + METHODS count_working_days_inclusive FOR TESTING. + METHODS count_reversed_range_is_zero FOR TESTING. +ENDCLASS. + + +CLASS {{p}}t02_hidden IMPLEMENTATION. + + METHOD setup. + mo_cut = NEW {{p}}delivery_schedule( ). + mt_holidays = VALUE #( ( c_tue_09 ) ( c_wed_10 ) ). + ENDMETHOD. + + METHOD weekend_is_not_working. + cl_abap_unit_assert=>assert_equals( + exp = abap_false + act = mo_cut->is_working_day( iv_date = c_sat_06 it_holidays = mt_no_holidays ) + msg = 'Saturday is not a working day' ). + cl_abap_unit_assert=>assert_equals( + exp = abap_false + act = mo_cut->is_working_day( iv_date = c_sun_07 it_holidays = mt_no_holidays ) + msg = 'Sunday is not a working day' ). + ENDMETHOD. + + METHOD working_day_is_true. + cl_abap_unit_assert=>assert_equals( + exp = abap_true + act = mo_cut->is_working_day( iv_date = c_mon_08 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD holiday_is_not_working. + cl_abap_unit_assert=>assert_equals( + exp = abap_false + act = mo_cut->is_working_day( iv_date = c_tue_09 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD next_day_same_for_workday. + cl_abap_unit_assert=>assert_equals( + exp = c_mon_08 + act = mo_cut->next_working_day( iv_date = c_mon_08 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD next_day_from_weekend. + cl_abap_unit_assert=>assert_equals( + exp = c_mon_08 + act = mo_cut->next_working_day( iv_date = c_sat_06 it_holidays = mt_no_holidays ) ). + ENDMETHOD. + + METHOD next_day_skips_holidays. + cl_abap_unit_assert=>assert_equals( + exp = c_thu_11 + act = mo_cut->next_working_day( iv_date = c_tue_09 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD add_zero_days. + cl_abap_unit_assert=>assert_equals( + exp = c_sat_06 + act = mo_cut->add_working_days( iv_start = c_sat_06 iv_days = 0 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD add_days_forward. + cl_abap_unit_assert=>assert_equals( + exp = c_tue_09 + act = mo_cut->add_working_days( iv_start = c_fri_05 iv_days = 2 it_holidays = mt_no_holidays ) ). + ENDMETHOD. + + METHOD add_forward_skips_holiday. + cl_abap_unit_assert=>assert_equals( + exp = c_thu_11 + act = mo_cut->add_working_days( iv_start = c_fri_05 iv_days = 2 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD add_days_backward. + cl_abap_unit_assert=>assert_equals( + exp = c_fri_05 + act = mo_cut->add_working_days( iv_start = c_mon_08 iv_days = -1 it_holidays = mt_no_holidays ) ). + ENDMETHOD. + + METHOD count_working_days_inclusive. + cl_abap_unit_assert=>assert_equals( + exp = 5 + act = mo_cut->count_working_days( iv_from = c_mon_08 iv_to = c_fri_12 it_holidays = mt_no_holidays ) + msg = 'Monday to Friday without holidays' ). + cl_abap_unit_assert=>assert_equals( + exp = 3 + act = mo_cut->count_working_days( iv_from = c_mon_08 iv_to = c_fri_12 it_holidays = mt_holidays ) + msg = 'Two holidays in the interval' ). + cl_abap_unit_assert=>assert_equals( + exp = 1 + act = mo_cut->count_working_days( iv_from = c_mon_08 iv_to = c_mon_08 it_holidays = mt_no_holidays ) + msg = 'One working day' ). + cl_abap_unit_assert=>assert_equals( + exp = 0 + act = mo_cut->count_working_days( iv_from = c_tue_09 iv_to = c_tue_09 it_holidays = mt_holidays ) + msg = 'One holiday' ). + ENDMETHOD. + + METHOD count_reversed_range_is_zero. + cl_abap_unit_assert=>assert_equals( + exp = 0 + act = mo_cut->count_working_days( iv_from = c_fri_12 iv_to = c_mon_08 it_holidays = mt_holidays ) ). + ENDMETHOD. + +ENDCLASS. diff --git a/tasks_gen/eval/G0002/reference/delivery_schedule.clas.abap b/tasks_gen/eval/G0002/reference/delivery_schedule.clas.abap new file mode 100644 index 0000000..de11d29 --- /dev/null +++ b/tasks_gen/eval/G0002/reference/delivery_schedule.clas.abap @@ -0,0 +1,104 @@ +CLASS {{p}}delivery_schedule DEFINITION PUBLIC FINAL CREATE PUBLIC. + + PUBLIC SECTION. + + TYPES ty_holidays TYPE STANDARD TABLE OF d WITH EMPTY KEY. + + METHODS is_working_day + IMPORTING iv_date TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_working) TYPE abap_bool. + + METHODS next_working_day + IMPORTING iv_date TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_date) TYPE d. + + METHODS add_working_days + IMPORTING iv_start TYPE d + iv_days TYPE i + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_date) TYPE d. + + METHODS count_working_days + IMPORTING iv_from TYPE d + iv_to TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_count) TYPE i. + + PRIVATE SECTION. + + CONSTANTS c_reference_monday TYPE d VALUE '19000101'. + + METHODS is_weekend + IMPORTING iv_date TYPE d + RETURNING VALUE(rv_weekend) TYPE abap_bool. + + METHODS is_holiday + IMPORTING iv_date TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_holiday) TYPE abap_bool. + +ENDCLASS. + + +CLASS {{p}}delivery_schedule IMPLEMENTATION. + + METHOD is_working_day. + rv_working = xsdbool( is_weekend( iv_date ) = abap_false + AND is_holiday( iv_date = iv_date + it_holidays = it_holidays ) = abap_false ). + ENDMETHOD. + + METHOD next_working_day. + rv_date = iv_date. + WHILE is_working_day( iv_date = rv_date + it_holidays = it_holidays ) = abap_false. + rv_date = rv_date + 1. + ENDWHILE. + ENDMETHOD. + + METHOD add_working_days. + rv_date = iv_start. + IF iv_days = 0. + RETURN. + ENDIF. + DATA(lv_remaining) = iv_days. + DATA(lv_step) = COND i( WHEN iv_days > 0 THEN 1 ELSE -1 ). + WHILE lv_remaining <> 0. + rv_date = rv_date + lv_step. + IF is_working_day( iv_date = rv_date + it_holidays = it_holidays ) = abap_true. + lv_remaining = lv_remaining - lv_step. + ENDIF. + ENDWHILE. + ENDMETHOD. + + METHOD count_working_days. + rv_count = 0. + DATA(lv_date) = iv_from. + WHILE lv_date <= iv_to. + IF is_working_day( iv_date = lv_date + it_holidays = it_holidays ) = abap_true. + rv_count = rv_count + 1. + ENDIF. + lv_date = lv_date + 1. + ENDWHILE. + ENDMETHOD. + + METHOD is_weekend. + DATA(lv_days) = iv_date - c_reference_monday. + rv_weekend = xsdbool( lv_days MOD 7 >= 5 ). + ENDMETHOD. + + METHOD is_holiday. + rv_holiday = abap_false. + LOOP AT it_holidays INTO DATA(lv_holiday). + IF lv_holiday = iv_date. + rv_holiday = abap_true. + RETURN. + ENDIF. + ENDLOOP. + ENDMETHOD. + +ENDCLASS. diff --git a/tasks_gen/eval/G0002/reference/delivery_schedule.testclasses.abap b/tasks_gen/eval/G0002/reference/delivery_schedule.testclasses.abap new file mode 100644 index 0000000..0884e43 --- /dev/null +++ b/tasks_gen/eval/G0002/reference/delivery_schedule.testclasses.abap @@ -0,0 +1,72 @@ +CLASS ltc_delivery_schedule DEFINITION FINAL FOR TESTING + DURATION SHORT RISK LEVEL HARMLESS. + + PRIVATE SECTION. + CONSTANTS: + c_fri_05 TYPE d VALUE '20240105', + c_sat_06 TYPE d VALUE '20240106', + c_sun_07 TYPE d VALUE '20240107', + c_mon_08 TYPE d VALUE '20240108', + c_tue_09 TYPE d VALUE '20240109', + c_wed_10 TYPE d VALUE '20240110', + c_thu_11 TYPE d VALUE '20240111', + c_fri_12 TYPE d VALUE '20240112'. + + DATA mo_cut TYPE REF TO {{p}}delivery_schedule. + DATA mt_holidays TYPE {{p}}delivery_schedule=>ty_holidays. + DATA mt_no_holidays TYPE {{p}}delivery_schedule=>ty_holidays. + + METHODS setup. + METHODS weekend_is_not_working FOR TESTING. + METHODS holiday_is_not_working FOR TESTING. + METHODS next_day_skips_weekend FOR TESTING. + METHODS add_days_forward FOR TESTING. + METHODS add_days_backward FOR TESTING. + METHODS count_working_days FOR TESTING. +ENDCLASS. + + +CLASS ltc_delivery_schedule IMPLEMENTATION. + + METHOD setup. + mo_cut = NEW {{p}}delivery_schedule( ). + mt_holidays = VALUE #( ( c_tue_09 ) ( c_wed_10 ) ). + ENDMETHOD. + + METHOD weekend_is_not_working. + cl_abap_unit_assert=>assert_equals( + exp = abap_false + act = mo_cut->is_working_day( iv_date = c_sat_06 it_holidays = mt_no_holidays ) ). + ENDMETHOD. + + METHOD holiday_is_not_working. + cl_abap_unit_assert=>assert_equals( + exp = abap_false + act = mo_cut->is_working_day( iv_date = c_tue_09 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD next_day_skips_weekend. + cl_abap_unit_assert=>assert_equals( + exp = c_mon_08 + act = mo_cut->next_working_day( iv_date = c_sun_07 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD add_days_forward. + cl_abap_unit_assert=>assert_equals( + exp = c_thu_11 + act = mo_cut->add_working_days( iv_start = c_fri_05 iv_days = 2 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD add_days_backward. + cl_abap_unit_assert=>assert_equals( + exp = c_fri_05 + act = mo_cut->add_working_days( iv_start = c_mon_08 iv_days = -1 it_holidays = mt_holidays ) ). + ENDMETHOD. + + METHOD count_working_days. + cl_abap_unit_assert=>assert_equals( + exp = 3 + act = mo_cut->count_working_days( iv_from = c_mon_08 iv_to = c_fri_12 it_holidays = mt_holidays ) ). + ENDMETHOD. + +ENDCLASS. diff --git a/tasks_gen/eval/G0002/spec.md b/tasks_gen/eval/G0002/spec.md new file mode 100644 index 0000000..0a9ac40 --- /dev/null +++ b/tasks_gen/eval/G0002/spec.md @@ -0,0 +1,84 @@ +# 1. Goal +Calculate delivery dates for sales orders. A delivery date must be a working day. +Weekends and plant holidays are not working days. A planner uses the class before +the system confirms a delivery date. + +# 2. Open questions +None. + +# 3. Context +- The class {{P}}DELIVERY_SCHEDULE does not exist. Create it in package $TMP. +- The caller gives the plant holidays as a table of dates. The class does not read + a factory calendar. The class does not read the system date. + +# 4. Contract +- Create the class {{P}}DELIVERY_SCHEDULE in package $TMP. +- The class is public, final, and has a public constructor without parameters. +- The class has the public type TY_HOLIDAYS. TY_HOLIDAYS is a standard table of + dates (TYPE d) with an empty key. +- The class has the public methods below. Do not add other public methods. + +```abap +METHODS is_working_day + IMPORTING iv_date TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_working) TYPE abap_bool. + +METHODS next_working_day + IMPORTING iv_date TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_date) TYPE d. + +METHODS add_working_days + IMPORTING iv_start TYPE d + iv_days TYPE i + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_date) TYPE d. + +METHODS count_working_days + IMPORTING iv_from TYPE d + iv_to TYPE d + it_holidays TYPE ty_holidays + RETURNING VALUE(rv_count) TYPE i. +``` + +# 5. Business rules +1. The days of the week are Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, + Sunday. +2. A date is a working day when these two conditions are true: + - The date is not a Saturday and not a Sunday. + - The date is not in it_holidays. +3. IS_WORKING_DAY returns abap_true for a working day. It returns abap_false for + all other days. +4. NEXT_WORKING_DAY returns the first working day that is on iv_date or after + iv_date. +5. ADD_WORKING_DAYS counts working days. + - If iv_days is 0, the result is iv_start. + - If iv_days is greater than 0, the result is the date that is iv_days working + days after iv_start. Count only working days. Ignore all other days. + - If iv_days is less than 0, the result is the date that is the absolute value + of iv_days working days before iv_start. Count only working days. Ignore all + other days. + - Example: iv_start is a Friday and iv_days is 1. The result is the next Monday. + - Example: iv_start is a Monday and iv_days is -1. The result is the Friday + before. +6. COUNT_WORKING_DAYS counts the working days in the interval from iv_from to + iv_to. The interval contains iv_from and iv_to. + - If iv_from is after iv_to, the result is 0. + - If iv_from is equal to iv_to, the result is 1 for a working day and 0 for all + other days. +7. it_holidays can be empty. it_holidays can contain dates that are outside the + interval. it_holidays can contain the same date more than one time. A date that + occurs more than one time in it_holidays is a holiday. + +# 6. Constraints +- Release target: 8.16. +- Coding standards: Clean ABAP. A method has less than 40 statements. No global + variables. No comment that restates the code. +- Out of scope: do not read a factory calendar. Do not read the system date. Do + not create other objects. + +# 7. Acceptance +- The class is active and has no syntax error. +- The hidden tests pass. +- Write your own ABAP Unit tests for the class. diff --git a/tasks_gen/eval/G0002/task.json b/tasks_gen/eval/G0002/task.json new file mode 100644 index 0000000..bd98460 --- /dev/null +++ b/tasks_gen/eval/G0002/task.json @@ -0,0 +1,41 @@ +{ + "id": "G0002", + "category": "A", + "object_type": "CLAS", + "difficulty": 2, + "release_target": "v816", + "expected_outcome": "implement", + "budget": { + "max_tool_calls": 50, + "max_activations": 12 + }, + "seed": [], + "contract": [ + { + "type": "CLAS", + "name": "{{P}}DELIVERY_SCHEDULE" + } + ], + "out_of_scope": [], + "hidden_tests": [ + { + "type": "CLAS", + "name": "{{P}}T02_HIDDEN", + "file": "hidden/t02_hidden.clas.abap", + "description": "Working day and delivery date rules" + } + ], + "reference": [ + { + "type": "CLAS", + "name": "{{P}}DELIVERY_SCHEDULE", + "file": "reference/delivery_schedule.clas.abap", + "description": "Delivery date calculation with weekends and plant holidays", + "testclasses_file": "reference/delivery_schedule.testclasses.abap" + } + ], + "craft_checks": [ + "method_length", + "no_global_variables" + ] +} \ No newline at end of file