Harness: runner, generator, tasks T01 T13 T14 T15, CLAUDE.md
This commit is contained in:
7
.env.example
Normal file
7
.env.example
Normal file
@@ -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
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
runs/
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
90
CLAUDE.md
Normal file
90
CLAUDE.md
Normal file
@@ -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/<run_dir>
|
||||||
|
python3 -m harness.cli cleanup-list <PREFIX> runs/<dir> && python3 -m harness.cli teardown runs/<dir>
|
||||||
|
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).
|
||||||
0
harness/__init__.py
Normal file
0
harness/__init__.py
Normal file
72
harness/adt_client.py
Normal file
72
harness/adt_client.py
Normal file
@@ -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"<del:object adtcore:uri={quoteattr(u)}><del:transportNumber/></del:object>"
|
||||||
|
for u in uris)
|
||||||
|
body = ('<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
'<del:deletionRequest xmlns:del="http://www.sap.com/adt/deletion" '
|
||||||
|
'xmlns:adtcore="http://www.sap.com/adt/core">' + objs + '</del:deletionRequest>')
|
||||||
|
_, _, text = self._req("POST", "/sap/bc/adt/deletion/delete", body,
|
||||||
|
{"Content-Type": DEL_REQ, "Accept": DEL_RES})
|
||||||
|
out = {}
|
||||||
|
for m in re.finditer(r'<del:object\b([^>]*)>(.*?)</del:object>', 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'<del:text>(.*?)</del:text>', 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
|
||||||
139
harness/agents.py
Normal file
139
harness/agents.py
Normal file
@@ -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
|
||||||
89
harness/cli.py
Normal file
89
harness/cli.py
Normal file
@@ -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/<run_dir>
|
||||||
|
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()
|
||||||
242
harness/generator.py
Normal file
242
harness/generator.py
Normal file
@@ -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": <task.json object>, "files": {"<relative path>": "<file content>", ...}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
42
harness/ledger.py
Normal file
42
harness/ledger.py
Normal file
@@ -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
|
||||||
96
harness/mcp_client.py
Normal file
96
harness/mcp_client.py
Normal file
@@ -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()
|
||||||
48
harness/pilot.py
Normal file
48
harness/pilot.py
Normal file
@@ -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()
|
||||||
80
harness/proxy.py
Normal file
80
harness/proxy.py
Normal file
@@ -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()
|
||||||
394
harness/runner.py
Normal file
394
harness/runner.py
Normal file
@@ -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'<?xml version="1.0" encoding="utf-8"?><abapGit version="v1.0.0" serializer="LCL_OBJECT_PROG" '
|
||||||
|
f'serializer_version="v1.0.0"><asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">'
|
||||||
|
f'<asx:values><PROGDIR><NAME>{name}</NAME><SUBC>1</SUBC><RLOAD>E</RLOAD><FIXPT>X</FIXPT>'
|
||||||
|
f'<UCCHECK>X</UCCHECK></PROGDIR></asx:values></asx:abap></abapGit>')
|
||||||
|
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'<?xml version="1.0" encoding="utf-8"?><abapGit version="v1.0.0" serializer="{ser}" '
|
||||||
|
f'serializer_version="v1.0.0"><asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">'
|
||||||
|
f'<asx:values><{tag}><CLSNAME>{name}</CLSNAME><LANGU>E</LANGU><DESCRIPT>x</DESCRIPT>'
|
||||||
|
f'<STATE>1</STATE><UNICODE>X</UNICODE></{tag}></asx:values></asx:abap></abapGit>')
|
||||||
|
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 <run_dir>"}
|
||||||
|
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()}
|
||||||
62
harness/task.py
Normal file
62
harness/task.py
Normal file
@@ -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", [])]
|
||||||
31
package-lock.json
generated
Normal file
31
package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
package.json
Normal file
16
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
tasks/T01/hidden/t01_hidden.clas.abap
Normal file
93
tasks/T01/hidden/t01_hidden.clas.abap
Normal file
@@ -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.
|
||||||
84
tasks/T01/reference/iban_validator.clas.abap
Normal file
84
tasks/T01/reference/iban_validator.clas.abap
Normal file
@@ -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(<ls_country>).
|
||||||
|
IF sy-subrc <> 0.
|
||||||
|
rv_reason = {{p}}if_iban_check=>c_reason-country.
|
||||||
|
RETURN.
|
||||||
|
ENDIF.
|
||||||
|
IF strlen( lv_iban ) <> <ls_country>-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.
|
||||||
48
tasks/T01/reference/iban_validator.testclasses.abap
Normal file
48
tasks/T01/reference/iban_validator.testclasses.abap
Normal file
@@ -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.
|
||||||
15
tasks/T01/seed/if_iban_check.intf.abap
Normal file
15
tasks/T01/seed/if_iban_check.intf.abap
Normal file
@@ -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.
|
||||||
45
tasks/T01/spec.md
Normal file
45
tasks/T01/spec.md
Normal file
@@ -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.
|
||||||
50
tasks/T01/task.json
Normal file
50
tasks/T01/task.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
70
tasks/T13/hidden/t13_hidden.clas.abap
Normal file
70
tasks/T13/hidden/t13_hidden.clas.abap
Normal file
@@ -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.
|
||||||
30
tasks/T13/reference/evt_fee.func.abap
Normal file
30
tasks/T13/reference/evt_fee.func.abap
Normal file
@@ -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.
|
||||||
52
tasks/T13/reference/t13_test.clas.abap
Normal file
52
tasks/T13/reference/t13_test.clas.abap
Normal file
@@ -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.
|
||||||
14
tasks/T13/seed/evt_sample.func.abap
Normal file
14
tasks/T13/seed/evt_sample.func.abap
Normal file
@@ -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.
|
||||||
35
tasks/T13/spec.md
Normal file
35
tasks/T13/spec.md
Normal file
@@ -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.
|
||||||
80
tasks/T13/task.json
Normal file
80
tasks/T13/task.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
72
tasks/T14/hidden/t14_hidden.clas.abap
Normal file
72
tasks/T14/hidden/t14_hidden.clas.abap
Normal file
@@ -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 <lt_data> TYPE STANDARD TABLE.
|
||||||
|
IF ir_data IS BOUND.
|
||||||
|
ASSIGN ir_data->* TO <lt_data>.
|
||||||
|
rv_lines = lines( <lt_data> ).
|
||||||
|
ENDIF.
|
||||||
|
ENDMETHOD.
|
||||||
|
|
||||||
|
METHOD value_of.
|
||||||
|
FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
|
||||||
|
ASSIGN ir_data->* TO <lt_data>.
|
||||||
|
ASSIGN <lt_data>[ iv_row ] TO FIELD-SYMBOL(<ls_line>).
|
||||||
|
ASSIGN COMPONENT iv_col OF STRUCTURE <ls_line> TO FIELD-SYMBOL(<lv_value>).
|
||||||
|
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = |Column { iv_col } missing| ).
|
||||||
|
rv_value = condense( CONV string( <lv_value> ) ).
|
||||||
|
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.
|
||||||
89
tasks/T14/reference/sales_sum.prog.abap
Normal file
89
tasks/T14/reference/sales_sum.prog.abap
Normal file
@@ -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.
|
||||||
12
tasks/T14/seed/sale.tabl.asabap
Normal file
12
tasks/T14/seed/sale.tabl.asabap
Normal file
@@ -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);
|
||||||
|
}
|
||||||
17
tasks/T14/seed/t14_seed.clas.abap
Normal file
17
tasks/T14/seed/t14_seed.clas.abap
Normal file
@@ -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.
|
||||||
33
tasks/T14/spec.md
Normal file
33
tasks/T14/spec.md
Normal file
@@ -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.
|
||||||
18
tasks/T14/task.json
Normal file
18
tasks/T14/task.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
64
tasks/T15/hidden/t15_hidden.clas.abap
Normal file
64
tasks/T15/hidden/t15_hidden.clas.abap
Normal file
@@ -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.
|
||||||
14
tasks/T15/reference/i_order_total.ddls.asddls
Normal file
14
tasks/T15/reference/i_order_total.ddls.asddls
Normal file
@@ -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
|
||||||
38
tasks/T15/reference/t15_test.clas.abap
Normal file
38
tasks/T15/reference/t15_test.clas.abap
Normal file
@@ -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.
|
||||||
13
tasks/T15/seed/itm.tabl.asabap
Normal file
13
tasks/T15/seed/itm.tabl.asabap
Normal file
@@ -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);
|
||||||
|
}
|
||||||
11
tasks/T15/seed/ord.tabl.asabap
Normal file
11
tasks/T15/seed/ord.tabl.asabap
Normal file
@@ -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;
|
||||||
|
}
|
||||||
33
tasks/T15/spec.md
Normal file
33
tasks/T15/spec.md
Normal file
@@ -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.
|
||||||
22
tasks/T15/task.json
Normal file
22
tasks/T15/task.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
24
tasks_gen/eval/G0001/generation.json
Normal file
24
tasks_gen/eval/G0001/generation.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
335
tasks_gen/eval/G0001/hidden/t07_hidden.clas.abap
Normal file
335
tasks_gen/eval/G0001/hidden/t07_hidden.clas.abap
Normal file
@@ -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.
|
||||||
143
tasks_gen/eval/G0001/reference/tour_planner.clas.abap
Normal file
143
tasks_gen/eval/G0001/reference/tour_planner.clas.abap
Normal file
@@ -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(<ls_tour>).
|
||||||
|
IF sy-subrc <> 0.
|
||||||
|
INSERT VALUE ty_tour( tour_id = ls_stop-tour_id )
|
||||||
|
INTO TABLE lt_tour ASSIGNING <ls_tour>.
|
||||||
|
ENDIF.
|
||||||
|
<ls_tour>-stops = <ls_tour>-stops + 1.
|
||||||
|
<ls_tour>-packages = <ls_tour>-packages + ls_stop-packages.
|
||||||
|
<ls_tour>-weight = <ls_tour>-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.
|
||||||
93
tasks_gen/eval/G0001/reference/tour_planner.testclasses.abap
Normal file
93
tasks_gen/eval/G0001/reference/tour_planner.testclasses.abap
Normal file
@@ -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.
|
||||||
55
tasks_gen/eval/G0001/seed/if_tour_planner.intf.abap
Normal file
55
tasks_gen/eval/G0001/seed/if_tour_planner.intf.abap
Normal file
@@ -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.
|
||||||
118
tasks_gen/eval/G0001/spec.md
Normal file
118
tasks_gen/eval/G0001/spec.md
Normal file
@@ -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.
|
||||||
41
tasks_gen/eval/G0001/task.json
Normal file
41
tasks_gen/eval/G0001/task.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tasks_gen/eval/G0002/generation.json
Normal file
25
tasks_gen/eval/G0002/generation.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
132
tasks_gen/eval/G0002/hidden/t02_hidden.clas.abap
Normal file
132
tasks_gen/eval/G0002/hidden/t02_hidden.clas.abap
Normal file
@@ -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.
|
||||||
104
tasks_gen/eval/G0002/reference/delivery_schedule.clas.abap
Normal file
104
tasks_gen/eval/G0002/reference/delivery_schedule.clas.abap
Normal file
@@ -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.
|
||||||
@@ -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.
|
||||||
84
tasks_gen/eval/G0002/spec.md
Normal file
84
tasks_gen/eval/G0002/spec.md
Normal file
@@ -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.
|
||||||
41
tasks_gen/eval/G0002/task.json
Normal file
41
tasks_gen/eval/G0002/task.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user