Compare commits

...

10 Commits

Author SHA1 Message Date
Kral
6984fc7917 Snapshot: new training tasks (balanced generation), docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 22:08:59 +02:00
Kral
eb13453097 Series A: local Qwen on the MacBook (remote server), 12 h window with hard stop, clean pause on server loss, dashboard card, docs/remote-model.md
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 21:57:01 +02:00
Kral
dc8d99a913 Dashboard: ignore an old STOPPED.txt while a controller runs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 21:32:23 +02:00
Kral
301c221d8e Budget guard from panel 50.00: limit 128
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 21:31:15 +02:00
Kral
59496cb08b Lock leak: controlled reproduction attempts and method (not reproduced), enqueue reader; restart plan for 12 October
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 19:47:16 +02:00
Kral
4432ac896c Only kinds below their target share are generated and run; one-controller flock guard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 19:39:37 +02:00
Kral
976274d48a Balanced generation under the pipeline, per-kind brake; STATE: type mix fix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 19:27:43 +02:00
Kral
55f4068330 STRU and exception tasks accepted; G6 cascade fix, exception class mutants, RTTI unit notes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 19:25:09 +02:00
Kral
b119f1afac Object type mix: INTF, TABL, STRU, MSAG, exception tasks (harness G2, mutants, generator notes), balanced generator, kind-deficit job order, dashboard mix card
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 18:52:29 +02:00
Kral
454db6626c Review notes: token length for bf16 test (p95 48k), DDLS reject reasons, G1034 lock analysis (not reproduced)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 18:21:44 +02:00
657 changed files with 38905 additions and 68 deletions

61
docs/epod-lock-leak.md Normal file
View File

@@ -0,0 +1,61 @@
# EPOD: stale lock after a write (2 cases, not reproduced under control)
Status 2026-10-05 evening. For Kral, to fix in the EPOD server (or to decide that it is not worth it).
## What happened (the two real cases)
| | Case 1 | Case 2 |
|---|---|---|
| When | 15:09, run 200102, task G1034 | 19:25, run 200273, task G1091 |
| Object | class `Z4AEE0SQ_STORAGE_PRICER` | table `Z4AJ50UB_PO_HEAD` (seed table of the task) |
| Situation | normal run; the second `sap_push_source includeType=testclasses` returned success (two activation warnings); the next `sap_push_element` failed `[LOCK] ... User KESELI is currently editing`, twice; teardown through ADT deletion said `You are already editing` | I started a second controller by mistake (same run number, same prefix, same seed table) and then killed both controller processes while the seed was installed. Teardown/deletion of the table said `You are already editing` |
| Lock gone | only after SM12 (Kral) | only after SM12 (Kral) |
In both cases the lock outlived the MCP session and the run (hours). `ENQUEUE_READ` (see below) showed nothing after the SM12 delete.
## What we tried to reproduce (A4H, probe objects, no cloud, 2026-10-05)
Every test: write through MCP, interfere, wait 3 s, read the enqueue table, write the same object again, delete it.
None left a lock. Scripts: `scripts_probe/` (`killtest.py`, `killtabl.py`, `killtwo.py`, `killcreate.py`, `deltest.py`, `lockprobe.py`).
1. Client killed (`SIGKILL`) 0.03 to 0.8 s after `sap_push_source includeType=testclasses` was sent (the write needs about 0.7 s): 9 kills, no lock. The server finishes the write.
2. Client killed during `sap_push_source TABL` (DDL, activation): 6 kills at 0.5 to 4.5 s (the write takes about 0.7 s): no lock.
3. Two clients write the same table at the same time and both are killed: 8 delays (0.05 to 0.7 s): no lock.
4. Two clients both `sap_create_object` + `sap_push_source` the same table and both are killed (Case 2 as exactly as possible): 8 delays (0.05 to 1.0 s): no lock.
5. ADT deletion of the object while a write on it runs: the deletion is refused (`You are already editing`) or wins the race, no lock stays (6 delays).
6. Earlier: 25 write cycles alone; 60 cycles overlapping 2690 `sap_run_unit_test` calls of another session; 40 cycles of testclasses write + `sap_check_object` (ATC, unit tests, coverage) + `sap_object_members` + second testclasses write + `sap_push_element` with a second session on `sap_check_object`; `sap_push_element` on a method of the test class (error text only, no lock).
So neither "client killed during a write" nor "two writers on one object" nor "delete during a write" leaks a lock on A4H in a short test. The two real cases may need a long running or slow call (the real runs were under load from 3 generators and 1 to 2 trajectory workers, calls then take seconds), or a state of the ADT stateful session that the probes did not reach.
## How to see the enqueue state (exact method)
A class with `IF_OO_ADT_CLASSRUN` calls `ENQUEUE_READ` and is run with `sap_run_class` (class `ZPROBE0EQ_LOCKS`, in `$TMP`; source in `scripts_probe/lockprobe.py`):
```abap
CALL FUNCTION 'ENQUEUE_READ'
EXPORTING gclient = sy-mandt gname = '' garg = '' guname = ''
IMPORTING subrc = lv_subrc
TABLES enq = lt_enq "TYPE STANDARD TABLE OF seqg3
EXCEPTIONS communication_failure = 1 system_failure = 2 OTHERS = 3.
```
A normal lock of a running write looks like this (seen in the 0.6 s test while a pipeline run was writing):
`GNAME=SEOCLSENQ GARG=Z4AJW0UK_CL_BERTH_FEE_TEST====...` (lock object of the class include, owner user KESELI).
After every probe the table was empty. A stale lock would show up as an entry that stays after the MCP session is closed:
read the table, close the session, read again.
There is no function module on A4H to delete an enqueue entry (`TFDIR`: only `ENQUEUE_READ`, no `ENQUEUE_DELETE`): SM12 is the only way to release a stale lock, so every reproduction costs one SM12 delete.
## What the harness does about it today
- A write result with `[LOCK]` ("currently editing") is an infrastructure event: the run is not accepted, the trajectory workers go from 2 to 1, three equal events stop the pipeline.
- A failed teardown (`You are already editing`) is listed as a leftover (dashboard); the object needs an SM12 delete, then the harness deletes it.
- One controller only (flock on `runs/pipeline/controller.lock`), so two controllers cannot install the same run number twice any more (this was Case 2).
- Never kill a controller while it runs: it is drained through the budget guard or a STOP flag.
## Wish for EPOD
1. Release the lock in a `finally` path of every write tool (also after a failed or warned activation).
2. Release all locks of an MCP session when the session ends (`DELETE /mcp`) and when the connection breaks.
3. Return a clear error with the lock owner and the age of the lock, and a tool to release the locks of the caller's own session.
4. A short idle timeout for the stateful ADT session (the lock lasted hours).

87
docs/remote-model.md Normal file
View File

@@ -0,0 +1,87 @@
# Local Qwen on the MacBook, harness on the Mac mini (series A)
Roles: the **MacBook only serves the model** (MLX, 4-bit). The harness, A4H, the MCP server, the records and the dashboard
stay on the **Mac mini**. The series is `python3 -m harness.localqwen` (see below). Written 2026-10-05.
## 1. Start the server on the MacBook (step by step)
1. **Plug the MacBook in and keep the lid open.** Close all heavy apps (the model needs about 16 GB, the prompt cache up to 6 GB).
2. **Check that this is the official baseline build** (same 4-bit weights as the baseline of 2026-10-04):
```sh
cd ~/models/Qwen3.8-27B-4bit
shasum -a 256 config.json model.safetensors.index.json tokenizer.json chat_template.jinja
```
The values must be:
```
14b65a0ee06517060a6bbd979bb1a8ff54e7b304b1a1f01d54344b88b8285e85 config.json
13b840162b4cb35c66fef7df072f7dbb4717908204364f5e5d9f9655a2758fa8 model.safetensors.index.json
06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523 tokenizer.json
c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041 chat_template.jinja
```
Full check (about 2 minutes, the weights): `shasum -a 256 model-0000*-of-00003.safetensors`
```
6cc1508e96fb5d0865dfd5753a79f4ec60651bf3e2a82844a7e8ae9c60528c0d model-00001-of-00003.safetensors
83f2a20ca8058f486a3634a27faf99587f4cd3c156a83dee34fb99e6ac178670 model-00002-of-00003.safetensors
31b8c91ef899f79efaaa69e3d2c096f6e2ebeb2ff20e29222abbd9ebc79e560a model-00003-of-00003.safetensors
```
(These are the hashes of `mlx-community/Qwen3.8-27B-4bit` on the Mac mini, `~/models/Qwen3.8-27B-4bit`.) If one differs, stop.
3. **Get the start script** `train/serve_remote.sh` onto the MacBook. Either, if the repo is on the MacBook, `cd ~/projects/abap-llm/harness && git pull`
(path `train/serve_remote.sh`), or copy it: `scp erhankeseli@192.168.178.40:~/projects/abap-llm/harness/train/serve_remote.sh ~/serve_remote.sh`
(192.168.178.40 is the Mac mini; use `.29` if that is its other address, check with `ifconfig` on the mini), then `chmod +x ~/serve_remote.sh`.
The script uses the same flags as the baseline (`train/serve.sh`): temp 0.2, top-p 0.95, top-k 20, min-p 0, max tokens 32768, prompt cache 4 / 6 GB;
only the host is `0.0.0.0` (port 8080), and `caffeinate -dimsu -w <server pid>` keeps the MacBook awake as long as the server lives.
4. **Start it** (a terminal window on the MacBook, leave it open; or in the background):
```sh
nohup ~/projects/abap-llm/harness/train/serve_remote.sh > ~/qwen_server.log 2>&1 &
```
(use `~/serve_remote.sh` if you copied it). The first start loads the model for about 1 minute. macOS may ask
"Do you want the application Python to accept incoming network connections?": **Allow**.
5. **Check on the MacBook:** `tail -f ~/qwen_server.log` shows `Starting httpd at 0.0.0.0 on port 8080`, and
`curl -s http://127.0.0.1:8080/v1/models` returns the model path.
6. **Find the MacBook IP** (the harness needs it): `ipconfig getifaddr en0` (Wi-Fi; if empty try `en1`, or look at System Settings > Wi-Fi > Details).
Both Macs must be in the same network (the mini is 192.168.178.x).
7. **Check from the Mac mini** (replace the IP): `curl -s http://<MacBook IP>:8080/v1/models`. It must show
`/Users/I301710/models/Qwen3.8-27B-4bit`.
8. **Start the series on the Mac mini:**
```sh
cd ~/projects/abap-llm/harness
python3 -m harness.localqwen --base-url http://<MacBook IP>:8080/v1 --wait 600
```
(`--wait 600`: waits up to 10 minutes for the server at the start. Run it with `nohup ... > runs/local_qwen.log 2>&1 &`.)
The 12 hour window starts with the first run, not with the command.
## 2. Stop
- **The series** (Mac mini): `pkill -f harness.localqwen` stops it, but the run in progress is cut and its objects stay in A4H. Better: wait for the
window to end (hard stop, clean) or ask Claude to stop it cleanly.
- **The server** (MacBook): `kill $(cat ~/qwen_server.pid)` (`caffeinate -w` ends when the server ends), or `pkill -f mlx_lm.server`.
Check: `curl -s -m 3 http://127.0.0.1:8080/v1/models` gives no answer.
- Closing the lid or a sleeping MacBook stops the server: the series then pauses by itself and resumes when the server answers (below).
## 3. What the harness does (series A)
- **Before the first run:** the server must answer, the served model path must end in `Qwen3.8-27B-4bit`, and a 16-token probe must work.
The answer is saved in `runs/local_qwen/preflight.json` (model path, speed of the probe). The weights themselves cannot be
checked remotely: the shasum comparison in step 2 is the proof of the same build.
- **Tasks and order** (`--plan-only` prints them): 3 x INTF, 3 x TABL, 3 x STRU, 3 x MSAG, 3 x exception, then 5 x DDLS (accepted
training tasks, no K variants, lowest ids). One task at a time.
- **Settings as the official baseline:** thinking off (`enable_thinking=false` per request), max_tokens 16384, loop guard 3,
tool budget 60 (CDS too), temperature 0.2, same system prompt, at most 80 turns.
- **Window:** 12 hours from the first run. At the end there is a hard stop, also in the middle of a run: the model request is dropped,
the run is not scored (no failure), its objects are deleted on A4H (the teardown needs about 20 s).
- **Server does not answer:** during a request the server is pinged every 30 s (3 misses in a row); between requests a failed
request is checked with a ping. Then the run ends cleanly (objects deleted, no scoring, folder `runs/local_qwen/_paused/`), the series
waits (a check every 30 s), and when the server answers again the same task starts again. The time of the pause counts in the 12 hours.
This is not counted as a failure.
- **Output** (`runs/local_qwen/`): `state.json` (the dashboard card), `results.jsonl` (one line per task), `summary.json` (at the end: pass rate and
failure types per kind), `accepted.jsonl` (accepted trajectories, **not for the first SFT**: `metadata.use_for_sft = false`, `series = local_qwen_A`),
`runs/` (one folder per run with `record.json`, trajectory, report). Resumable: start it again with the same arguments.
- **Acceptance** is the same filter as for the DeepSeek trajectories: score at least 80, end reason `report`, no harness error text.
Failure types per run: `loop`, `tool_budget`, `empty_response`, `model_error`, `not_active`, `contract`, `hidden_tests_not_run`,
`hidden_tests_failed`, `out_of_scope_changed`, `atc_priority1`, `release_syntax`, `low_score`.
- **Dashboard:** the card "Series A: local Qwen on the MacBook" in `runs/dashboard/index.html` (5-minute refresh).
## 4. Speed and cost
About 12 tokens per second with the 4-bit model (`train/README.md`); a task needs 20 to 40 minutes, so the 20 tasks take 7 to 13 hours:
at the slow end the window ends before the last DDLS tasks. No cloud cost: the ledger is not touched (the model is not a `:cloud` model).

View File

@@ -0,0 +1,62 @@
# Restart plan for 12 October 2026 (cloud work)
Decision (Kral + Opus, 2026-10-05): when the budget guard stops, no cloud work (generation, trajectories) before the
Ollama reset on 12 October. Until then only no-cloud work. This is the plan for the restart.
## 1. Before the start (5 minutes, no cloud call)
1. Panel value after the reset (usage USD). Then:
`python3 -m harness.restart_plan --panel <value>`
It prints the `.env` values and the state below with live numbers. Put them in `.env`:
`BUDGET_CYCLE_START=2026-10-12`, `BUDGET_LIMIT_USD=<printed>`, `BUDGET_RESERVE_USD=8` (the 3 usage reserve for the
second teacher test stays). Ratio for the guard: 1.2 ledger per usage (trajectory runs; generation is about 2, so the
guard is on the safe side). Give the panel value again after about 2 hours and recompute (`python3 -m harness.dashboard panel <v>`).
2. A4H up (`docker ps`), MCP answers, no stale lock: `python3 scripts_probe/lockprobe.py` must print 0 entries.
3. One controller only: `python3 -m harness.pipeline` refuses to start a second one (`runs/pipeline/controller.lock`).
Remove `runs/pipeline/STOP` and `STOPPED.txt` if they exist.
4. Dashboard: `python3 -m harness.dashboard` (5-minute page, `runs/dashboard/index.html`).
## 2. Order of work (what the controller does by itself)
Only kinds below their target share are generated and run (`harness/mix.py`, `below_target`); the kind with the biggest
deficit goes first. Target (percent of accepted tasks and of accepted trajectories): CLAS 28, INTF 7, DDLS 25, FUNC 15,
PROG 10, TABL 8, STRU 2, MSAG 2.5, exception 2.5.
1. Trajectories for the new-type tasks that wait (INTF, TABL, STRU, MSAG, exception, PROG), then DDLS.
2. Second attempts: a task whose first attempt failed, or was accepted without a repair, gets one more attempt (at most two
attempts, at most two accepted trajectories per task). Failed DDLS and failed new-type tasks come first because they are
below target.
3. Generation (3 workers): the kind with the biggest deficit; a kind with more than 8 waiting tasks is not generated; a kind with
6 or more tries and under 20 % accepted is skipped. 20 % error-targeted slots, 30 % hard slots.
4. CLAS and FUNC generation and trajectories wait until they are at or below their target share (CLAS is far above).
5. K variants (free text) resume at 10 % of the other accepted tasks when their kinds are below target.
## 3. How much is needed (numbers of 2026-10-05 20:00, live: `restart_plan`)
CLAS has 49 accepted trajectories. At a 28 % share that is a total of 175 accepted trajectories, so about 105 more are
needed, all on other kinds: INTF 12, DDLS about 35, FUNC about 15, PROG about 17, TABL 14, STRU 3, MSAG 4, exception 4.
Tasks needed (accepted, with about 1.3 trajectory runs per accepted trajectory): TABL +9, INTF +10, STRU +3, MSAG +4,
DDLS +20, PROG +5. About 130 trajectory runs at 0.25 ledger = 33 ledger = 27 usage, plus the generation (about 6 usage).
Second attempts are part of the 130 runs. If the new reset gives 60 usage, this fits with room for the second teacher test.
## 4. Checks on the first runs of each new type (do not skip)
- The first 5 runs of INTF, TABL, STRU, MSAG, exception: records complete (the proxy must pass `sap_push_message`),
Qwen conversion works (`train/to_qwen.py` on `accepted.jsonl`: tool call round trip), no harness event.
- Acceptance per kind in the summary; a kind under 20 % after 6 tries is skipped automatically: read why (prompt, harness, or the
teacher cannot do it) before starting it again.
- DDLS: 8 of 17 runs accepted on 2026-10-05; the rejections were budget (60 calls, now 100) and empty responses (32k output limit).
If empty responses stay high, lower the output limit for CDS runs or retry once more.
## 5. Settings that stay
- Trajectory workers 2 (a harness event sets 1); stop rules: acceptance under 50 % over the last 30 runs, the same harness
error three times, the budget guard. Generation deadline 2026-10-10 18:00 has passed: set `--gen-deadline` and
`--traj-deadline` on the controller command (for example `--gen-deadline 2026-10-20T18:00 --traj-deadline 2026-10-21T23:30`).
- Tool budget 100 for CDS tasks (eval stays 60). 20 tool schemas in every sample. Token note: p95 48k, max 72k.
- Summary every 50 accepted trajectories in `train/STATE.md` and `docs/yol-haritasi.md`, with a commit.
## 6. After the restart
Open decisions: second A4H (not started, multi-host code later), bf16 memory test at 48k, the second teacher test (the
reserve), the EPOD requests in `docs/epod-lock-leak.md` and `docs/epod-syntax-hint.md`.

View File

@@ -4,9 +4,20 @@ import os
import time import time
import urllib.request import urllib.request
import threading
import urllib.error
from .ledger import add_usage, check_budget from .ledger import add_usage, check_budget
from .proxy import BudgetExceeded from .proxy import BudgetExceeded
class ServerDown(Exception):
"""The model server does not answer (remote local model). The run ends cleanly and is not a result."""
class WindowEnd(Exception):
"""The time window of the series is over (hard stop, also in the middle of a request)."""
SYSTEM_PROMPT = """You are an ABAP developer. You implement a plan on an SAP system with the tools. SYSTEM_PROMPT = """You are an ABAP developer. You implement a plan on an SAP system with the tools.
Rules: Rules:
@@ -53,6 +64,8 @@ class OracleAgent:
ident["functionGroup"] = o["functionGroup"] ident["functionGroup"] = o["functionGroup"]
proxy.call("sap_create_object", dict(ident, packageName="$TMP", proxy.call("sap_create_object", dict(ident, packageName="$TMP",
description=o.get("description", o["name"])[:60])) description=o.get("description", o["name"])[:60]))
if o.get("messages"): # message class: messages are written with sap_push_message
proxy.call("sap_push_message", {"objectName": o["name"], "messages": o["messages"]})
if o.get("source"): if o.get("source"):
proxy.call("sap_push_source", dict(ident, source=o["source"])) proxy.call("sap_push_source", dict(ident, source=o["source"]))
if o.get("testclasses_source"): if o.get("testclasses_source"):
@@ -70,7 +83,8 @@ class LlmAgent:
""" """
def __init__(self, model, base_url=None, api_key=None, max_turns=80, temperature=0.2, def __init__(self, model, base_url=None, api_key=None, max_turns=80, temperature=0.2,
max_seconds=None, max_tokens=None, chat_template_kwargs=None, loop_guard=None): max_seconds=None, max_tokens=None, chat_template_kwargs=None, loop_guard=None,
deadline=None, watch=False):
self.model = model self.model = model
self.name = f"llm:{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.base_url = (base_url or os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")).rstrip("/")
@@ -84,6 +98,8 @@ class LlmAgent:
self.max_tokens = max_tokens or (32000 if ":cloud" in (model or "") else None) self.max_tokens = max_tokens or (32000 if ":cloud" in (model or "") else None)
self.empty_retries = 2 self.empty_retries = 2
self.loop_guard = loop_guard # end the run after this many identical pushes in a row (None = off) self.loop_guard = loop_guard # end the run after this many identical pushes in a row (None = off)
self.deadline = deadline # absolute time (time.time()) of the hard stop, or None
self.watch = watch # remote model: ping the server during a request, end the run when it is gone
self.end_reason = None self.end_reason = None
self.messages, self.tools, self.reasoning, self.turn_usage = [], [], [], [] # for the trajectory record self.messages, self.tools, self.reasoning, self.turn_usage = [], [], [], [] # for the trajectory record
self.chat_template_kwargs = chat_template_kwargs # local server only, e.g. {"enable_thinking": False} self.chat_template_kwargs = chat_template_kwargs # local server only, e.g. {"enable_thinking": False}
@@ -101,17 +117,60 @@ class LlmAgent:
last = None last = None
for attempt in range(4): # model server errors (HTTP 5xx, timeouts): retry with backoff for attempt in range(4): # model server errors (HTTP 5xx, timeouts): retry with backoff
try: try:
with urllib.request.urlopen(req, timeout=self.request_timeout) as r: if self.watch or self.deadline:
data = json.loads(r.read().decode()) data = self._post_watched(req)
return data["choices"][0]["message"], data.get("usage", {}) else:
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 (ServerDown, WindowEnd):
raise
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
last = e last = e
code = getattr(e, "code", None) code = getattr(e, "code", None)
if code is not None and code < 500 and code != 429: if code is not None and code < 500 and code != 429:
raise raise
if self.deadline and time.time() >= self.deadline:
raise WindowEnd()
if self.watch and not self._ping():
raise ServerDown(str(e)[:200])
time.sleep(10 * (attempt + 1)) time.sleep(10 * (attempt + 1))
raise RuntimeError(f"model request failed after retries: {last}") raise RuntimeError(f"model request failed after retries: {last}")
def _ping(self, timeout=10):
try:
urllib.request.urlopen(urllib.request.Request(f"{self.base_url}/models"), timeout=timeout).read()
return True
except Exception: # noqa: BLE001
return False
def _post_watched(self, req):
"""The request runs in a thread; this thread watches the deadline and (remote model) the server: a hard stop
or a dead server ends the wait at once, also in the middle of a long generation."""
box = {}
def work():
try:
with urllib.request.urlopen(req, timeout=self.request_timeout) as r:
box["data"] = json.loads(r.read().decode())
except BaseException as e: # noqa: BLE001
box["err"] = e
th = threading.Thread(target=work, daemon=True)
th.start()
last_ping, fails = time.time(), 0
while th.is_alive():
th.join(5)
if self.deadline and time.time() >= self.deadline:
raise WindowEnd()
if self.watch and th.is_alive() and time.time() - last_ping >= 30:
last_ping = time.time()
fails = 0 if self._ping() else fails + 1
if fails >= 3:
raise ServerDown("no answer to 3 pings in a row during a request")
if "err" in box:
raise box["err"]
return box["data"]
def run(self, task, proxy): def run(self, task, proxy):
tools = [{"type": "function", "function": {"name": t["name"], tools = [{"type": "function", "function": {"name": t["name"],
"description": t.get("description", ""), "description": t.get("description", ""),
@@ -127,6 +186,10 @@ class LlmAgent:
self.end_reason = "max_turns" self.end_reason = "max_turns"
start = time.time() start = time.time()
for _ in range(self.max_turns): for _ in range(self.max_turns):
if self.deadline and time.time() >= self.deadline:
final = "Stopped: time window over."
self.end_reason = "window_end"
break
if self.max_seconds and time.time() - start > self.max_seconds: if self.max_seconds and time.time() - start > self.max_seconds:
final = f"Stopped: time budget exceeded ({self.max_seconds} s)." final = f"Stopped: time budget exceeded ({self.max_seconds} s)."
self.end_reason = "time_budget" self.end_reason = "time_budget"
@@ -135,6 +198,14 @@ class LlmAgent:
msg, usage = self._chat(messages, tools) msg, usage = self._chat(messages, tools)
add_usage(self.model, usage, kind="run", ref=proxy.prefix) add_usage(self.model, usage, kind="run", ref=proxy.prefix)
self.turn_usage.append(usage) self.turn_usage.append(usage)
except WindowEnd:
final = "Stopped: time window over."
self.end_reason = "window_end"
break
except ServerDown as e:
final = f"Stopped: model server not reachable ({e})."
self.end_reason = "server_down"
break
except RuntimeError as e: except RuntimeError as e:
final = f"Stopped: {e}" final = f"Stopped: {e}"
self.end_reason = "model_error" self.end_reason = "model_error"

View File

@@ -17,6 +17,7 @@ import time
import urllib.request import urllib.request
from .adt_client import load_env from .adt_client import load_env
from . import mix
from .ledger import _env_budget, spent from .ledger import _env_budget, spent
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -242,6 +243,88 @@ def agg_table(title, evs, rows, key, note=""):
% (E(title), E(key.replace("_", " ")), trs, "<div class=note>%s</div>" % E(note) if note else "")) % (E(title), E(key.replace("_", " ")), trs, "<div class=note>%s</div>" % E(note) if note else ""))
def local_card():
"""Card for series A (local Qwen on the MacBook): state.json of harness.localqwen. Empty string without a series."""
path = os.path.join(ROOT, "runs", "local_qwen", "state.json")
if not os.path.exists(path):
return ""
try:
st = json.load(open(path))
except ValueError:
return ""
now = time.time()
url = st.get("base_url") or ""
up = ping(url + "/models", timeout=4) if url else False # the dashboard asks the MacBook itself
status = st.get("status", "?")
left = st.get("window_start", now) + st.get("window_hours", 12) * 3600 - now
cur = st.get("current")
plan, res = st.get("plan", []), st.get("results", [])
chips = "<span class='chip %s'>server %s</span> <span class='chip %s'>%s</span>" % (
"ok" if up else "er", "answers" if up else "does not answer",
{"running": "ok", "paused": "wa", "finished": "gr", "window_over": "gr"}.get(status, "gr"), E(status))
rows = ""
per = {}
for r in res:
k = per.setdefault(r["kind"], {"runs": 0, "ok": 0, "scores": [], "fail": {}})
k["runs"] += 1
k["ok"] += r["accepted"]
if r.get("score") is not None:
k["scores"].append(r["score"])
for f in r.get("failure_types", []):
k["fail"][f] = k["fail"].get(f, 0) + 1
planned = {}
for p_ in plan:
planned[p_["kind"]] = planned.get(p_["kind"], 0) + 1
for kind in [x for x in ("INTF", "TABL", "STRU", "MSAG", "EXC", "DDLS") if x in planned or x in per]:
k = per.get(kind, {"runs": 0, "ok": 0, "scores": [], "fail": {}})
mean = "%.0f" % (sum(k["scores"]) / len(k["scores"])) if k["scores"] else "-"
rate = "%.0f%%" % (100.0 * k["ok"] / k["runs"]) if k["runs"] else "-"
fails = ", ".join("%s %d" % (f, n) for f, n in sorted(k["fail"].items(), key=lambda x: -x[1])) or "-"
rows += ("<tr><td>%s</td><td class=n>%d/%d</td><td class=n>%d</td><td class=n>%s</td><td class=n>%s</td><td>%s</td></tr>"
% (kind, k["runs"], planned.get(kind, 0), k["ok"], rate, mean, E(fails)))
if cur:
elapsed = (now - cur["start"]) / 60
curtxt = "%s (%s, %s), run %d min%s" % (cur["task"], cur["kind"], cur["category"], elapsed,
", restart %d" % cur["restarts"] if cur.get("restarts") else "")
else:
curtxt = "none (%s)" % status
evs = "".join("<div class=note>%s %s %s: %s</div>" % (time.strftime("%H:%M", time.localtime(e["t"])), E(e["kind"]),
E(e["task"]), E(e["text"])) for e in st.get("events", [])[-3:])
hdr = ("<div class=card style='grid-column:1/-1'><h2>Series A: local Qwen 3.8 27B on the MacBook (thinking off, 16384 tokens, loop guard 3, budget 60)</h2>"
"<div class=b>%s &nbsp; model <span class=mono>%s</span></div>"
"<div class=b><table><tr><td>Current task</td><td>%s</td><td>Done / planned</td><td class=n>%d / %d</td>"
"<td>Time left in the window</td><td class=n>%s</td><td>Paused</td><td class=n>%.0f min</td></tr></table></div>"
% (chips, E(os.path.basename(str(st.get("model_id", "")))), E(curtxt), len(res), len(plan),
("%dh %02dm" % (left // 3600, (left % 3600) // 60)) if left > 0 else "over", st.get("paused_seconds", 0) / 60))
tbl = ("<div class=b><table><tr><th>kind</th><th class=n>runs/planned</th><th class=n>accepted</th><th class=n>pass rate</th>"
"<th class=n>mean score</th><th>failure types</th></tr>%s</table></div>%s"
"<div class=note>Accepted trajectories of this series are kept apart (runs/local_qwen/accepted.jsonl), not for the first SFT.</div></div>" % (rows, evs))
return hdr + tbl
def mix_table(rows, evs):
kt = mix.accepted_task_counts()
kr = {}
for r, e in zip(rows, evs):
k = mix.kind_of_task_dir(r["task"])
a = kr.setdefault(k, [0, 0])
a[0] += 1
a[1] += e["accepted"]
tt, tr = max(sum(kt.values()), 1), max(sum(v[1] for v in kr.values()), 1)
ss = sum(mix.TYPE_SHARE.values())
trs = ""
for k, v in mix.TYPE_SHARE.items():
tgt = 100.0 * v / ss
pt, pr = 100.0 * kt.get(k, 0) / tt, 100.0 * kr.get(k, [0, 0])[1] / tr
cls = "g" if abs(pt - tgt) < 4 else ("w" if abs(pt - tgt) < 10 else "r")
trs += ("<tr><td>%s</td><td class=n>%.0f%%</td><td class=n>%d (%.0f%%)</td><td style='width:25%%'>%s</td>"
"<td class=n>%d/%d (%.0f%%)</td></tr>" % (k, tgt, kt.get(k, 0), pt, bar(pt, max(tgt * 2, 1), cls),
kr.get(k, [0, 0])[1], kr.get(k, [0, 0])[0], pr))
return ("<div class=card><h2>Object type mix vs target</h2><div class=b><table><tr><th>kind</th><th class=n>target</th>"
"<th class=n>tasks</th><th>vs target</th><th class=n>trajectories acc/runs</th></tr>%s</table></div>"
"<div class=note>CLAS/INTF 35, CDS 25, FUNC 15, PROG 10, DDIC 10, MSAG + exception 5 (percent of accepted tasks).</div></div>" % trs)
def gen_table(logs, key, title): def gen_table(logs, key, title):
agg = {} agg = {}
for l in logs: for l in logs:
@@ -270,7 +353,9 @@ def render(d):
tasks_with_run = {r["task"] for r in rows} tasks_with_run = {r["task"] for r in rows}
backlog = sum(1 for l in logs if l.get("accepted") and l["id"] not in tasks_with_run) backlog = sum(1 for l in logs if l.get("accepted") and l["id"] not in tasks_with_run)
alive = any("harness.pipeline" in p for p in d["procs"]) alive = any("harness.pipeline" in p for p in d["procs"])
status, scls = ("RUNNING", "ok") if alive and not d["stopped"] else (("STOPPED", "er") if d["stopped"] else ("NOT RUNNING", "er")) if alive: # a STOPPED.txt of an earlier run does not count while a controller runs
d["stopped"] = None
status, scls = ("RUNNING", "ok") if alive else (("STOPPED", "er") if d["stopped"] else ("NOT RUNNING", "er"))
hints = sum(e["hints"] for e in evs) hints = sum(e["hints"] for e in evs)
ctx = sorted(e["ctx"] for e in acc if e["ctx"]) ctx = sorted(e["ctx"] for e in acc if e["ctx"])
next50 = (n_acc // 50 + 1) * 50 next50 = (n_acc // 50 + 1) * 50
@@ -350,8 +435,8 @@ def render(d):
% ("ok" if d["a4h"] else "er", "up" if d["a4h"] else "down", "ok" if d["mcp"] else "er", "up" if d["mcp"] else "down", scls, status, % ("ok" if d["a4h"] else "er", "up" if d["a4h"] else "down", "ok" if d["mcp"] else "er", "up" if d["mcp"] else "down", scls, status,
E("\n".join(d["procs"])))) E("\n".join(d["procs"]))))
logc = "<div class=card><h2>Pipeline log</h2><div class=b><div class='log mono'>%s</div></div></div>" % E("\n".join(d["log_tail"])) logc = "<div class=card><h2>Pipeline log</h2><div class=b><div class='log mono'>%s</div></div></div>" % E("\n".join(d["log_tail"]))
body = (banner + "<div class=tiles>" + tiles + "</div><div class=grid>" + budget + prog + stops + sysc body = (banner + "<div class=tiles>" + tiles + "</div><div class=grid>" + local_card() + budget + prog + stops + sysc
+ agg_table("Trajectories by category", evs, rows, "category") + agg_table("Trajectories by object type", evs, rows, "object_type") + mix_table(rows, evs) + agg_table("Trajectories by category", evs, rows, "category") + agg_table("Trajectories by object type", evs, rows, "object_type")
+ gen_table(logs, "category", "Task generation by category") + gen_table(logs, "object_type", "Task generation by object type") + gen_table(logs, "category", "Task generation by category") + gen_table(logs, "object_type", "Task generation by object type")
+ tok + recent + logc + "</div>") + tok + recent + logc + "</div>")
stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(d["now"])) stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(d["now"]))

View File

@@ -24,7 +24,8 @@ ABAPLINT = os.path.join(ROOT, "node_modules", ".bin", "abaplint")
MAX_OUT_TOKENS = 80000 MAX_OUT_TOKENS = 80000
RUNS_PER_ATTEMPT = 10 RUNS_PER_ATTEMPT = 10
ABAPLINT_VERSIONS = {"v702", "v740sp05", "v750", "v758"} ABAPLINT_VERSIONS = {"v702", "v740sp05", "v750", "v758"}
EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15"} EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15",
"STRU": "T15", "MSAG": "T13"}
CATEGORIES = { CATEGORIES = {
"A": "pure logic in a new class (language, OO design, Clean ABAP)", "A": "pure logic in a new class (language, OO design, Clean ABAP)",
"B": "database access (ABAP SQL, CDS) with test doubles", "B": "database access (ABAP SQL, CDS) with test doubles",
@@ -50,6 +51,59 @@ CATEGORIES = {
"hidden_tests is []. Do not write the gap in Open questions (write \"None.\")", "hidden_tests is []. Do not write the gap in Open questions (write \"None.\")",
} }
# Extra format notes for object types that the main prompt does not describe (training mode, 2026-10-05).
# The example bundle is of another type; these notes say how this type differs.
TYPE_NOTES = {
"INTF": """The model must create an INTERFACE (type INTF) as the main contract object: the methods, types and constants
named in the spec. Contract entry: {"type": "INTF", "name": "{{P}}IF_X"}. Reference: the interface source
("reference/x.intf.abap") and ONE global own-test class (type CLAS, source with a global class DEFINITION FOR TESTING;
an interface has no test include). Hidden tests: one global test class with a local class that IMPLEMENTS the
interface (it only compiles when every signature matches), calls the methods through the interface, reads the
constants (values), and checks type lengths with RTTI (cl_abap_typedescr=>describe_by_name). The business rules give the
exact constant values, parameter names and types. No other object is needed.""",
"TABL": """The model must create a transparent DATABASE TABLE (type TABL) as the main contract object. The table name has at
most 16 characters including the prefix: use a short name, for example {{P}}ORD (7 characters after the prefix at most).
Source is DDL: annotations @EndUserText.label, @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE,
@AbapCatalog.tableCategory : #TRANSPARENT, @AbapCatalog.deliveryClass : #A, @AbapCatalog.dataMaintenance : #RESTRICTED,
then "define table {{p}}ord { key client : abap.clnt not null; key item_id : abap.char(10) not null; ... }".
Contract entry: {"type": "TABL", "name": "{{P}}ORD", "fields": ["client", "item_id", ...]} (all fields, lower case).
The spec (Business rules) lists every field with its data type and length, the key fields, and not-null fields. Use
only built-in types (abap.char, abap.numc, abap.dec, abap.int4, abap.dats). Avoid abap.curr and abap.quan; if the spec
needs an amount or a quantity, the reference field needs a reference annotation in the form 'tablename.fieldname'
(for example @Semantics.amount.currencyCode : '{{p}}ord.currency_code' on the amount and a field currency_code :
abap.cuky in the same table; @Semantics.quantity.unitOfMeasure : '{{p}}ord.unit' with unit : abap.unit(3)); a
reference without the table name fails with "Annotation with reference to unit code ... is uncomplete". Reference: the DDL file and ONE global own-test class
(type CLAS). Hidden tests (one global class): RTTI on the table: cast cl_abap_typedescr=>describe_by_name( '{{P}}ORD' ) to
cl_abap_structdescr and check get_ddic_field_list( ) (field names, key flags, LENG in characters, decimals, types; cl_abap_elemdescr->length is in BYTES: 2 per character), and a
cl_osql_test_environment test (create( i_dependency_list = VALUE #( ( '{{P}}ORD' ) ) ), insert, select back; a second
INSERT with the same key gives sy-subrc = 4). No seed is needed.""",
"STRU": """The model must create a DDIC STRUCTURE (type STRU) as the main contract object. DDL source: annotations
@EndUserText.label and @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE, then
"define structure {{p}}name { code : abap.char(4); amount : abap.dec(9,2); ... }" (include another structure with
"include {{p}}other;" when the spec asks). Contract entry: {"type": "STRU", "name": "{{P}}NAME", "fields": [...]}
(the field names in lower case). The Business rules list every field with data type and length. Reference: the DDL file
and ONE global own-test class (type CLAS). Hidden tests (one global class): RTTI: cl_abap_typedescr=>describe_by_name(
'{{P}}NAME' ) cast to cl_abap_structdescr; check components (names, length, decimals, type kind) and the order. RTTI units: cl_abap_elemdescr->length is in BYTES
(2 bytes per character in Unicode), so a char(10) field has length 20; compare characters with output_length or with
LENG of cl_abap_structdescr=>get_ddic_field_list( ) (characters), and decimals with ->decimals.""",
"MSAG": """The model must create a MESSAGE CLASS (type MSAG) as the main contract object. The name has at most 20
characters including the prefix. A message class has NO source file. In task.json the reference entry has no "file":
{"type": "MSAG", "name": "{{P}}MSG", "description": "...", "messages": [{"msgno": "001", "text": "Order &1 is blocked"}, ...]}
and the contract entry is {"type": "MSAG", "name": "{{P}}MSG", "messages": [{"msgno": "001"}, ...]} (the numbers that
must exist). The Business rules give for each message the number, the exact text with the placeholders &1 to &4 (at
most 72 characters), and when it is used (error, warning, information). Reference: the message class entry and ONE global
own-test class (type CLAS) that reads the messages. Hidden tests (one global class): for each message use
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'A' 'B' INTO DATA(lv_text) and assert the final text; also check one
placeholder substitution. The model may also be asked for an exception class that uses the message class (then both
are contract entries and the exception class is a reference file).""",
"EXC": """The main contract object is a class-based EXCEPTION class (CLAS, name {{P}}CX_...) that inherits from
CX_STATIC_CHECK, CX_DYNAMIC_CHECK or CX_NO_CHECK. The spec asks for constants for the error cases, attributes with
context data, a constructor with these parameters, and a message text (through IF_T100_DYN_MSG / IF_T100_MESSAGE with
a message class that is a seed object, or through a redefined get_text). Hidden tests raise the exception from a small
seed class, catch it and check the attributes and the text (get_text( )). Reference: the exception class with its
testclasses_file or ONE global own-test class (an exception class has few methods).""",
}
SYSTEM = """You write evaluation tasks for an ABAP developer model. Each task is a bundle of files. 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) 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 through ADT tools. The harness installs the seed objects, runs the model, then checks the result with
@@ -220,7 +274,7 @@ def lint_files(b):
m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), "")) m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), ""))
if not m: if not m:
continue continue
want = {"table": "TABL", "structure": "TABL", "view": "DDLS"}[m.group(1).lower()] want = {"table": "TABL", "structure": "STRU", "view": "DDLS"}[m.group(1).lower()]
if o.get("type") != want: if o.get("type") != want:
errs.append(f"{k}: {o.get('name')} has type {o.get('type')}, but its source is " errs.append(f"{k}: {o.get('name')} has type {o.get('type')}, but its source is "
f"'define {m.group(1)}'; use type {want}") f"'define {m.group(1)}'; use type {want}")
@@ -294,9 +348,24 @@ def check_bundle(b):
errs.append(f"{k}: file {o[fk]} missing") errs.append(f"{k}: file {o[fk]} missing")
if not o.get("name", "").startswith("{{P}}"): if not o.get("name", "").startswith("{{P}}"):
errs.append(f"{k}: name {o.get('name')} does not start with {{{{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 limit = {"TABL": 16, "FUGR": 26, "MSAG": 20}.get(o.get("type"), 30)
if len(o.get("name", "").replace("{{P}}", "Z0000000_")) > limit: if len(o.get("name", "").replace("{{P}}", "Z0000000_")) > limit:
errs.append(f"{k}: name {o.get('name')} too long (max {limit})") errs.append(f"{k}: name {o.get('name')} too long (max {limit})")
for k in ("seed", "reference"):
for o in t.get(k, []):
if o.get("type") == "MSAG":
msgs = o.get("messages") or []
if k == "reference" and not msgs:
errs.append(f"reference message class {o.get('name')} has no 'messages' list")
for m in msgs:
if not re.fullmatch(r"\d{1,3}", str(m.get("msgno", ""))) or not m.get("text") or len(m["text"]) > 72:
errs.append(f"message class {o.get('name')}: message {m.get('msgno')} needs a 1 to 3 digit "
"msgno and a text of at most 72 characters")
for c in t.get("contract", []):
if c.get("type") in ("TABL", "STRU") and not c.get("fields"):
errs.append(f"contract {c.get('name')}: a {c['type']} entry needs the list 'fields'")
if c.get("type") == "MSAG" and not c.get("messages"):
errs.append(f"contract {c.get('name')}: a MSAG entry needs the list 'messages'")
if not t.get("hidden_tests") and not stop: if not t.get("hidden_tests") and not stop:
errs.append("no hidden test class") errs.append("no hidden test class")
return errs return errs
@@ -399,9 +468,11 @@ def generate(task_id, pool, object_type, category, difficulty, model, base_url,
pool_root = os.path.join(ROOT, "tasks_gen", pool) pool_root = os.path.join(ROOT, "tasks_gen", pool)
task_dir = os.path.join(pool_root, task_id) task_dir = os.path.join(pool_root, task_id)
example = bundle_of(os.path.join(ROOT, "tasks", EXAMPLE_FOR[object_type])) example = bundle_of(os.path.join(ROOT, "tasks", EXAMPLE_FOR[object_type]))
note_key = "EXC" if (object_type == "CLAS" and topic and "exception class" in topic.lower()) else object_type
ask = (f"Write one new task.\nObject type of the main contract object: {object_type}.\n" 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"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") + (f"Topic idea: {topic}\n" if topic else "Choose a new, realistic business topic.\n")
+ (("Format notes for this object type:\n" + TYPE_NOTES[note_key] + "\n") if note_key in TYPE_NOTES else "")
+ "Here is an example bundle of a different task (same format):\n" + json.dumps(example)) + "Here is an example bundle of a different task (same format):\n" + json.dumps(example))
messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": ask}] messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": ask}]
log = {"id": task_id, "pool": pool, "object_type": object_type, "category": category, "attempts": []} log = {"id": task_id, "pool": pool, "object_type": object_type, "category": category, "attempts": []}

320
harness/localqwen.py Normal file
View File

@@ -0,0 +1,320 @@
"""Series A: the local Qwen 3.8 27B (4-bit, served on the MacBook) on training tasks of the new kinds (2026-10-05).
Harness, A4H, MCP server and records stay on the Mac mini; the MacBook only serves the model (docs/remote-model.md).
python3 -m harness.localqwen --base-url http://<MacBook LAN IP>:8080/v1 [--window-hours 12] [--plan-only]
Order: 3 tasks each of INTF, TABL, STRU, MSAG, exception, then 5 DDLS; one task at a time. Settings as the official
baseline (docs/stage1-baseline.md): thinking off, max_tokens 16384, loop guard 3, tool budget 60, temperature 0.2, same
system prompt. Time window: 12 hours from the first run, hard stop at the end also in the middle of a run (the run is
not scored, its objects are deleted on A4H). If the server does not answer, the run ends cleanly, the series pauses and
resumes when it answers again; such a run is not a result and not a failure.
Output (runs/local_qwen/): state.json (dashboard), runs/, accepted.jsonl (accepted trajectories, NOT for the first SFT),
results.jsonl, summary.json. Resumable: call again with the same arguments.
"""
import argparse
import json
import os
import shutil
import sys
import threading
import time
import urllib.request
from .adt_client import load_env
from .agents import LlmAgent
from . import mix
from .runner import Runner
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "train"))
import accept as acc # noqa: E402
POOL = os.path.join(ROOT, "tasks_gen", "train")
OUT = os.path.join(ROOT, "runs", "local_qwen")
STATE = os.path.join(OUT, "state.json")
RUN_BASE = 410000 # + task index * 10 + restart number (a digit must lead the 4-char base36 run: < 466560)
EXPECTED_MODEL = "Qwen3.8-27B-4bit" # basename of the served model path (same build as the official baseline)
PLAN_ORDER = [("INTF", 3), ("TABL", 3), ("STRU", 3), ("MSAG", 3), ("EXC", 3), ("DDLS", 5)]
SETTINGS = {"max_tokens": 16384, "enable_thinking": False, "temperature": 0.2, "tool_call_budget": 60, "loop_guard": 3,
"max_turns": 80, "one_task_at_a_time": True}
MIN_SCORE = 80
def now():
return time.time()
def http_json(url, timeout=10, body=None):
req = urllib.request.Request(url, json.dumps(body).encode() if body else None,
{"Content-Type": "application/json", "Authorization": "Bearer none"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode())
def preflight(base_url, wait=0):
"""Server answers, serves the expected build, can generate. Returns a dict (ok, model id, speed, models json)."""
deadline = now() + wait
while True:
out = {"ok": False, "base_url": base_url, "time": time.strftime("%F %T")}
try:
models = http_json(base_url + "/models", timeout=10)
ids = [m.get("id", "") for m in models.get("data", [])]
out["models"] = ids
out["model_id"] = ids[0] if ids else ""
out["build_ok"] = bool(ids) and os.path.basename(ids[0].rstrip("/")) == EXPECTED_MODEL
if out["build_ok"]:
t0 = now()
r = http_json(base_url + "/chat/completions", timeout=180,
body={"model": out["model_id"], "messages": [{"role": "user", "content": "Say OK."}],
"max_tokens": 16, "temperature": 0, "chat_template_kwargs": {"enable_thinking": False}})
out["probe_seconds"] = round(now() - t0, 1)
out["probe_reply"] = (r["choices"][0]["message"].get("content") or "")[:40]
out["ok"] = True
else:
out["error"] = f"served model {ids} is not {EXPECTED_MODEL} (the official baseline build)"
except Exception as e: # noqa: BLE001
out["error"] = repr(e)[:200]
if out["ok"] or now() >= deadline:
return out
time.sleep(15)
def build_plan():
"""3 x INTF, TABL, STRU, MSAG, EXC, then 5 DDLS: accepted training tasks, no K variants, lowest ids first."""
import glob
by_kind = {}
for f in sorted(glob.glob(os.path.join(POOL, "G*", "generation.json"))):
tid = os.path.basename(os.path.dirname(f))
try:
if not json.load(open(f)).get("accepted"):
continue
meta = json.load(open(os.path.join(POOL, tid, "task.json")))
except (OSError, ValueError):
continue
if meta.get("category") == "K":
continue
k = mix.kind_of_task_dir(tid)
by_kind.setdefault(k, []).append((tid, meta.get("category")))
plan = []
for kind, n in PLAN_ORDER:
for tid, cat in by_kind.get(kind, [])[:n]:
plan.append({"task": tid, "kind": kind, "category": cat})
return plan
def failure_types(rep, why):
"""Why a run is not accepted (list of short labels)."""
out = []
er = rep.get("end_reason")
if er and er != "report":
out.append(er) # loop, tool_budget, empty_response, model_error, max_turns, ...
g = rep.get("gates") or {}
names = {"G1_active": "not_active", "G2_contract": "contract", "G3_hidden_runs": "hidden_tests_not_run",
"G4_out_of_scope": "out_of_scope_changed", "G5_no_p1": "atc_priority1", "G6_release": "release_syntax"}
out += [names[k] for k, v in g.items() if not v and k in names]
h = rep.get("hidden_tests") or {}
if h.get("total") and h.get("passed", 0) < h["total"]:
out.append("hidden_tests_failed")
if not out and any(w.startswith("score=") for w in why):
out.append("low_score")
if "no_final_report" in why and "no_report" not in out and er == "report":
out.append("no_report")
return out or ["other"]
class Series:
def __init__(self, a):
self.a = a
os.makedirs(OUT, exist_ok=True)
self.state = json.load(open(STATE)) if os.path.exists(STATE) else {}
self.stop_hb = threading.Event()
self.lock = threading.Lock()
# ---- state -------------------------------------------------------------------------------------------------
def save(self):
with self.lock:
self.state["updated"] = now()
tmp = STATE + ".tmp"
json.dump(self.state, open(tmp, "w"), indent=1, default=str)
os.replace(tmp, STATE)
def heartbeat(self):
while not self.stop_hb.wait(30):
try:
ok = True
http_json(self.a.base_url + "/models", timeout=8)
except Exception: # noqa: BLE001
ok = False
self.state["server_ok"] = ok
self.state["server_checked"] = now()
self.save()
def window_end(self):
return self.state["window_start"] + self.a.window_hours * 3600
# ---- one run ---------------------------------------------------------------------------------------------
def one(self, item, idx, model_id):
restarts = 0
while True:
run_no = RUN_BASE + idx * 10 + restarts
self.state["current"] = {"task": item["task"], "kind": item["kind"], "category": item["category"], "start": now(),
"run": run_no, "restarts": restarts}
self.state["status"] = "running"
self.save()
agent = LlmAgent(model_id, self.a.base_url, max_tokens=SETTINGS["max_tokens"],
chat_template_kwargs={"enable_thinking": False}, loop_guard=SETTINGS["loop_guard"],
deadline=self.window_end(), watch=True)
runner = Runner(POOL, os.path.join(OUT, "runs"))
try:
rep, run_dir = runner.run(item["task"], agent, run_no, teardown=True, tool_budget=SETTINGS["tool_call_budget"])
except Exception as e: # noqa: BLE001 a harness exception is not a model result
self.event("harness_exception", item, repr(e)[:200])
return {"task": item["task"], "kind": item["kind"], "category": item["category"], "harness_error": repr(e)[:200]}
er = rep.get("end_reason")
if er == "server_down":
self.pause(item, run_dir)
if now() >= self.window_end():
return {"window_over": True}
restarts = min(restarts + 1, 9)
continue
if er == "window_end":
self.event("window_end_mid_run", item, "run stopped and cleaned up; not scored")
self.park(run_dir, "_window_end")
return {"window_over": True}
return self.finish(item, rep, run_dir, agent)
def park(self, run_dir, folder):
dst = os.path.join(OUT, folder)
os.makedirs(dst, exist_ok=True)
shutil.move(run_dir, os.path.join(dst, os.path.basename(run_dir) + "_" + str(int(now()))))
def event(self, kind, item, text):
self.state.setdefault("events", []).append({"t": now(), "kind": kind, "task": item["task"], "text": text})
self.save()
print(time.strftime("%F %T"), kind, item["task"], text, flush=True)
def pause(self, item, run_dir):
self.event("server_down", item, "run ended cleanly (objects deleted); series paused")
self.park(run_dir, "_paused")
self.state["status"] = "paused"
self.state["pause_start"] = now()
self.save()
while now() < self.window_end():
pf = preflight(self.a.base_url)
if pf["ok"]:
break
time.sleep(30)
paused = now() - self.state.pop("pause_start", now())
self.state["paused_seconds"] = self.state.get("paused_seconds", 0) + paused
self.event("server_back", item, f"paused {paused / 60:.1f} min; the task is run again (no failure counted)")
def finish(self, item, rep, run_dir, agent):
rec_path = os.path.join(run_dir, "record.json")
rec = json.load(open(rec_path)) if os.path.exists(rec_path) else None
row = {"harness_error": False, "setup_failed": bool(rep.get("setup_failed"))}
ok, why = acc.judge(rec, row, MIN_SCORE) if rec else (False, ["no_record"])
if rep.get("setup_failed"):
ok, why = False, ["setup_failed"]
res = {"task": item["task"], "kind": item["kind"], "category": item["category"], "accepted": ok,
"score": (rep.get("score") or {}).get("total"), "end_reason": rep.get("end_reason"),
"tool_calls": rep.get("tool_calls"), "seconds": rep.get("seconds"), "agent_seconds": rep.get("agent_seconds"),
"hidden": "%s/%s" % ((rep.get("hidden_tests") or {}).get("passed"), (rep.get("hidden_tests") or {}).get("total")),
"activation_failures": rep.get("activation_failures"), "syntax_hints": rep.get("syntax_hints"),
"failure_types": [] if ok else failure_types(rep, why), "reasons": why,
"run_dir": os.path.relpath(run_dir, ROOT), "teardown_ok": isinstance(rep.get("teardown"), dict)
and all(v.get("deleted") for v in rep["teardown"].values())}
if ok:
rec["metadata"]["use_for_sft"] = False # not for the first SFT (Kral 2026-10-05)
rec["metadata"]["series"] = "local_qwen_A"
rec["metadata"]["served_model"] = agent.model
with open(os.path.join(OUT, "accepted.jsonl"), "a") as f:
f.write(json.dumps({"id": f"{item['task']}_r{rep.get('run')}", "task": rec["task"], "teacher": agent.model,
"metadata": rec["metadata"], "messages": rec["messages"], "tools": rec["tools"]}) + "\n")
with open(os.path.join(OUT, "results.jsonl"), "a") as f:
f.write(json.dumps(res) + "\n")
return res
# ---- series ------------------------------------------------------------------------------------------------
def run(self):
if self.a.plan_only:
for p in self.state.get("plan") or build_plan():
print(p)
return
pf = preflight(self.a.base_url, wait=self.a.wait)
json.dump(pf, open(os.path.join(OUT, "preflight.json"), "w"), indent=1)
print("preflight:", json.dumps(pf)[:400], flush=True)
if not pf["ok"]:
sys.exit("the model server is not usable: %s" % pf.get("error"))
plan = self.state.get("plan") or build_plan()
if self.a.only:
plan = [p for p in plan if p["task"] in self.a.only]
self.state.update(plan=plan, settings=SETTINGS, base_url=self.a.base_url, model_id=pf["model_id"],
window_hours=self.a.window_hours, preflight=pf, server_ok=True, results=self.state.get("results", []))
self.state.setdefault("window_start", now()) # 12 hours from the first run
self.save()
threading.Thread(target=self.heartbeat, daemon=True).start()
done = {r["task"] for r in self.state["results"]}
for idx, item in enumerate(plan):
if item["task"] in done:
continue
if now() >= self.window_end():
break
res = self.one(item, idx, pf["model_id"])
if res.get("window_over"):
break
if res.get("harness_error"):
continue
self.state["results"].append(res)
self.state["current"] = None
self.save()
print(json.dumps({k: res[k] for k in ("task", "kind", "accepted", "score", "end_reason", "failure_types")}), flush=True)
left = self.window_end() - now()
self.state["status"] = "window_over" if left <= 0 else "finished"
self.state["current"] = None
self.save()
self.stop_hb.set()
self.summary()
def summary(self):
per = {}
for r in self.state["results"]:
k = per.setdefault(r["kind"], {"runs": 0, "accepted": 0, "scores": [], "failures": {}})
k["runs"] += 1
k["accepted"] += r["accepted"]
if r["score"] is not None:
k["scores"].append(r["score"])
for ft in r["failure_types"]:
k["failures"][ft] = k["failures"].get(ft, 0) + 1
for k in per.values():
k["mean_score"] = round(sum(k["scores"]) / len(k["scores"]), 1) if k["scores"] else None
k["pass_rate"] = round(k["accepted"] / k["runs"], 2) if k["runs"] else None
out = {"per_kind": per, "runs": len(self.state["results"]), "accepted": sum(r["accepted"] for r in self.state["results"]),
"window_hours": self.a.window_hours, "paused_seconds": self.state.get("paused_seconds", 0),
"events": self.state.get("events", []), "settings": SETTINGS, "model_id": self.state.get("model_id")}
json.dump(out, open(os.path.join(OUT, "summary.json"), "w"), indent=1)
print("summary:", json.dumps({k: (v["accepted"], v["runs"]) for k, v in per.items()}), flush=True)
def main():
load_env(os.path.join(ROOT, ".env"))
ap = argparse.ArgumentParser()
ap.add_argument("--base-url", default=os.environ.get("LOCAL_MODEL_URL"), help="http://<MacBook LAN IP>:8080/v1")
ap.add_argument("--window-hours", type=float, default=12.0)
ap.add_argument("--wait", type=int, default=0, help="seconds to wait for the server at the start")
ap.add_argument("--plan-only", action="store_true")
ap.add_argument("--only", nargs="*", help="test: only these tasks")
ap.add_argument("--out-dir", help="test: another output directory")
a = ap.parse_args()
if a.out_dir:
global OUT, STATE
OUT = os.path.abspath(a.out_dir)
STATE = os.path.join(OUT, "state.json")
if not a.base_url:
sys.exit("--base-url (or LOCAL_MODEL_URL) is required, for example http://192.168.1.20:8080/v1")
a.base_url = a.base_url.rstrip("/")
Series(a).run()
if __name__ == "__main__":
main()

87
harness/mix.py Normal file
View File

@@ -0,0 +1,87 @@
"""Object type mix of the training data (CLAUDE.md section 1, Opus review 2026-10-05).
Target share of the accepted tasks (and of the accepted trajectories) per "kind":
CLAS/INTF 35 % (CLAS 28, INTF 7), CDS (DDLS) 25 %, FUNC 15 %, PROG 10 %, DDIC 10 % (TABL 8, STRU 2),
MSAG + exception 5 % (MSAG 2.5, EXC 2.5).
A task has the kind of its main contract object; a CLAS whose reference inherits from CX_ is EXC.
DTEL / DOMA tasks are not made yet (the DDIC share is covered by TABL and STRU).
"""
import json
import os
import re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
POOL = os.path.join(ROOT, "tasks_gen", "train")
TYPE_SHARE = {"CLAS": 28.0, "INTF": 7.0, "DDLS": 25.0, "FUNC": 15.0, "PROG": 10.0, "TABL": 8.0, "STRU": 2.0,
"MSAG": 2.5, "EXC": 2.5}
# categories a kind supports (docs/faz1-tasarim.md 8): the generator picks the one with the biggest deficit
KIND_CATEGORIES = {"CLAS": "ACDEFGHI", "FUNC": "ACDEFGHI", "PROG": "BCEGH", "DDLS": "BEFHI", "INTF": "A",
"TABL": "B", "STRU": "B", "MSAG": "D", "EXC": "D"}
CATEGORY_SHARE = {"A": 10, "B": 15, "C": 15, "D": 10, "E": 15, "F": 10, "G": 10, "I": 5, "H": 10}
_CACHE = {}
def kind_of_task_dir(task_id):
"""Kind of an accepted training task (cached)."""
if task_id in _CACHE:
return _CACHE[task_id]
d = os.path.join(POOL, task_id)
try:
t = json.load(open(os.path.join(d, "task.json")))
except (OSError, ValueError):
return None
otype = t.get("object_type") or "CLAS"
for c in t.get("contract", []): # K variants and old tasks: the contract object is the truth
otype = c.get("type", otype)
break
kind = otype
if otype == "CLAS":
for o in t.get("reference", []):
if o.get("file") and o.get("name") == (t.get("contract") or [{}])[0].get("name"):
try:
src = open(os.path.join(d, o["file"])).read()
except OSError:
src = ""
if re.search(r"INHERITING\s+FROM\s+\S*CX_", src, re.I):
kind = "EXC"
_CACHE[task_id] = kind
return kind
def deficit_pick(counts, share=TYPE_SHARE, allowed=None):
"""Kind with the largest (target - actual) for the next item. counts: {kind: n}."""
total = sum(counts.values()) + 1
best, best_def = None, None
tot_share = sum(share.values())
for k, s in share.items():
if allowed and k not in allowed:
continue
d = total * s / tot_share - counts.get(k, 0)
if best_def is None or d > best_def:
best, best_def = k, d
return best
def accepted_task_counts(logs_dir=None):
"""{kind: accepted tasks} from the generation logs of the training pool (K variants count by their kind)."""
import glob
logs_dir = logs_dir or os.path.join(POOL, "_logs")
out = {}
for f in glob.glob(os.path.join(logs_dir, "G*.json")):
try:
l = json.load(open(f))
except (OSError, ValueError):
continue
if l.get("accepted"):
k = kind_of_task_dir(l["id"])
if k:
out[k] = out.get(k, 0) + 1
return out
def below_target(counts, share=TYPE_SHARE):
"""Kinds whose share of `counts` is below the target for the next item (deficit > 0).
Since 2026-10-05 (Kral + Opus): only these kinds are generated and run; CLAS and FUNC wait until they are at or
below their target share."""
total, ss = sum(counts.values()) + 1, sum(share.values())
return {k for k, v in share.items() if total * v / ss - counts.get(k, 0) > 0}

View File

@@ -148,6 +148,74 @@ def mutants(src, otype, seed, n=MAX_MUTANTS):
return out return out
def decl_mutants(src, otype, seed, n=MAX_MUTANTS):
"""Mutants of declarations that carry the behavior of DDIC objects and interfaces (no executable code):
TABL / STRU: field length, decimals, data type, key flag. INTF: constant values, type lengths."""
sites = [] # (kind, start, end, new, line, old)
def add(kind, m, new):
sites.append((kind, m.start(), m.end(), new, src.count("\n", 0, m.start()) + 1, m.group(0)))
if otype in ("TABL", "STRU"):
for m in re.finditer(r"abap\.(char|numc|dec|curr|quan|lang|cuky|unit)\((\d+)(?:,\s*(\d+))?\)", src, re.I):
kind_, ln, dec = m.group(1).lower(), int(m.group(2)), m.group(3)
if kind_ in ("char", "numc") and ln > 1:
add("length", m, f"abap.{kind_}({ln - 1})")
if dec is not None and int(dec) < ln - 1:
add("decimals", m, f"abap.{kind_}({ln},{int(dec) + 1})")
if kind_ == "char" and ln > 1:
add("type", m, f"abap.numc({ln})")
for m in re.finditer(r"abap\.(int4|int8|timestamp|dats|tims)\b", src, re.I):
add("type", m, {"int4": "abap.int8", "int8": "abap.int4", "timestamp": "abap.dats",
"dats": "abap.tims", "tims": "abap.dats"}[m.group(1).lower()])
for m in re.finditer(r"^(\s*)key(\s+)(?!client\b)(\w+\s*:)", src, re.I | re.M):
add("key", m, f"{m.group(1)}{m.group(3)}")
if otype == "INTF":
for m in re.finditer(r"(\bmsgno\s*=\s*')(\d+)(')", src, re.I): # message number of an exception text id
add("msgno", m, f"{m.group(1)}{str(int(m.group(2)) + 1).zfill(len(m.group(2)))}{m.group(3)}")
for m in re.finditer(r"(\bVALUE\s+)(\d+)(?=\s*\.)", src, re.I):
add("const", m, f"{m.group(1)}{int(m.group(2)) + 1}")
for m in re.finditer(r"(\bVALUE\s+)'([^'\n]{1,20})'", src, re.I):
v = m.group(2)
add("lit", m, f"{m.group(1)}'" + ("Z" if v[0] != "Z" else "Y") + v[1:] + "'")
for m in re.finditer(r"(\bVALUE\s+)(abap_true|abap_false)\b", src, re.I):
add("bool", m, m.group(1) + ("abap_false" if m.group(2).lower() == "abap_true" else "abap_true"))
for m in re.finditer(r"\bLENGTH\s+(\d+)", src, re.I):
if int(m.group(1)) > 1:
add("length", m, f"LENGTH {int(m.group(1)) - 1}")
rnd = random.Random(seed)
rnd.shuffle(sites)
chosen, kinds = [], set()
for prefer_new in (True, False):
for st in sites:
if len(chosen) >= n:
break
if st in chosen or (prefer_new and st[0] in kinds):
continue
chosen.append(st)
kinds.add(st[0])
return [(f"line {line}: {old.strip()} -> {new.strip()} ({kind})", src[:a] + new + src[b:])
for kind, a, b, new, line, old in sorted(chosen, key=lambda x: x[1])]
def msag_mutants(messages, seed, n=MAX_MUTANTS):
"""Mutants of a message class: changed text, changed placeholder, a message moved to another number."""
out = []
for i, m in enumerate(messages):
t = m.get("text", "")
mut = [dict(x) for x in messages]
mut[i]["text"] = t + " x" if len(t) < 70 else t[:-1]
out.append((f"message {m['msgno']}: text + ' x' (text)", mut))
if "&1" in t:
mut = [dict(x) for x in messages]
mut[i]["text"] = t.replace("&1", "&2", 1)
out.append((f"message {m['msgno']}: &1 -> &2 (placeholder)", mut))
if len(messages) > 1:
mut = [dict(x) for x in messages]
mut[i]["msgno"] = str(int(m["msgno"]) + 50).zfill(3)
out.append((f"message {m['msgno']}: number + 50 (number)", mut))
random.Random(seed).shuffle(out)
return out[:n]
def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False): def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
"""Run the mutants of one task. Returns a summary dict; writes it to <task>/mutation.json.""" """Run the mutants of one task. Returns a summary dict; writes it to <task>/mutation.json."""
task_dir = os.path.join(pool_root, task_id) task_dir = os.path.join(pool_root, task_id)
@@ -156,14 +224,25 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
def is_test(o): def is_test(o):
return o["type"] == "CLAS" and re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING", return o["type"] == "CLAS" and re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING",
open(os.path.join(task_dir, o["file"])).read(), re.I | re.M) open(os.path.join(task_dir, o["file"])).read(), re.I | re.M)
targets = [o for o in meta["reference"] if o.get("file") and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS") targets = [o for o in meta["reference"] if o.get("file") and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS", "INTF",
"TABL", "STRU")
and not is_test(o)] and not is_test(o)]
targets += [o for o in meta["reference"] if o["type"] == "MSAG" and o.get("messages")]
work = os.path.join(ROOT, "runs", "gen", "mut") work = os.path.join(ROOT, "runs", "gen", "mut")
os.makedirs(work, exist_ok=True) os.makedirs(work, exist_ok=True)
runner = Runner(os.path.join(work, "pool"), os.path.join(work, "runs")) runner = Runner(os.path.join(work, "pool"), os.path.join(work, "runs"))
# candidates per object, then round robin: objects without mutation sites (exception classes) give their share # candidates per object, then round robin: objects without mutation sites (exception classes) give their share
cand = [[(o, d, m) for d, m in mutants(open(os.path.join(task_dir, o["file"])).read(), o["type"], def _muts(o):
f"{task_id}:{o['name']}", n)] for o in targets] if o["type"] == "MSAG":
return msag_mutants(o["messages"], f"{task_id}:{o['name']}", n)
src = open(os.path.join(task_dir, o["file"])).read()
if o["type"] in ("TABL", "STRU", "INTF"):
return decl_mutants(src, o["type"], f"{task_id}:{o['name']}", n)
ms = mutants(src, o["type"], f"{task_id}:{o['name']}", n)
if o["type"] == "CLAS" and len(ms) < n: # exception classes: constants, message numbers, texts
ms += decl_mutants(src, "INTF", f"{task_id}:{o['name']}", n - len(ms))
return ms
cand = [[(o, d, m) for d, m in _muts(o)] for o in targets]
plan = [] plan = []
while len(plan) < n and any(cand): while len(plan) < n and any(cand):
for c in cand: for c in cand:
@@ -174,7 +253,14 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
mdir = os.path.join(work, "pool", task_id) mdir = os.path.join(work, "pool", task_id)
shutil.rmtree(mdir, ignore_errors=True) shutil.rmtree(mdir, ignore_errors=True)
shutil.copytree(task_dir, mdir) shutil.copytree(task_dir, mdir)
open(os.path.join(mdir, o["file"]), "w").write(msrc) if o["type"] == "MSAG": # the mutant changes the messages of the reference in task.json
tj = json.load(open(os.path.join(mdir, "task.json")))
for r in tj["reference"]:
if r["name"] == o["name"]:
r["messages"] = msrc
json.dump(tj, open(os.path.join(mdir, "task.json"), "w"), indent=2)
else:
open(os.path.join(mdir, o["file"]), "w").write(msrc)
rep, _ = runner.run(task_id, OracleAgent(), run_base + k) rep, _ = runner.run(task_id, OracleAgent(), run_base + k)
h = rep.get("hidden_tests") or {} h = rep.get("hidden_tests") or {}
g = rep.get("gates") or {} g = rep.get("gates") or {}
@@ -187,7 +273,7 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
results.append({"object": o["name"], "mutant": desc, "status": status, results.append({"object": o["name"], "mutant": desc, "status": status,
"hidden": f"{h.get('passed')}/{h.get('total')}", "hidden": f"{h.get('passed')}/{h.get('total')}",
"failed_tests": [d["method"] for d in h.get("detail", []) if not d["ok"]]}) "failed_tests": [d["method"] for d in h.get("detail", []) if not d["ok"]]})
if keep and status == "killed": # candidate faulty reference for own-test scoring if keep and status == "killed" and o["type"] != "MSAG": # candidate faulty reference for own-test scoring
fdir = os.path.join(task_dir, "faulty") fdir = os.path.join(task_dir, "faulty")
os.makedirs(fdir, exist_ok=True) os.makedirs(fdir, exist_ok=True)
open(os.path.join(fdir, f"m{k}_{os.path.basename(o['file'])}"), "w").write(msrc) open(os.path.join(fdir, f"m{k}_{os.path.basename(o['file'])}"), "w").write(msrc)

View File

@@ -25,7 +25,7 @@ import threading
import time import time
from .adt_client import load_env from .adt_client import load_env
from . import trainset, trajectories from . import mix, trainset, trajectories
from .ledger import spent from .ledger import spent
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -129,13 +129,27 @@ class Pipeline:
done = {(r["task"], r["attempt"]) for r in rows} done = {(r["task"], r["attempt"]) for r in rows}
ev0 = {r["task"]: self.evaluate(r) for r in rows if r["attempt"] == 0} ev0 = {r["task"]: self.evaluate(r) for r in rows if r["attempt"] == 0}
tasks = trajectories.accepted_tasks() tasks = trajectories.accepted_tasks()
for t in tasks: # first attempt for every task first counts = {} # accepted trajectories (and runs in flight) per kind
if (t, 0) not in done and (t, 0) not in self.inflight: for r in rows:
self.inflight.add((t, 0)) if self.evaluate(r)["accepted"]:
return t, 0 k = mix.kind_of_task_dir(r["task"])
counts[k] = counts.get(k, 0) + 1
for (t, _a) in self.inflight:
k = mix.kind_of_task_dir(t)
counts[k] = counts.get(k, 0) + 1
below = mix.below_target(counts) # only kinds below their target share run (Kral + Opus 2026-10-05)
cands = [t for t in tasks if (t, 0) not in done and (t, 0) not in self.inflight
and mix.kind_of_task_dir(t) in below]
if cands: # first attempts: the kind with the biggest deficit against the target mix goes first
total, tot_share = sum(counts.values()) + 1, sum(mix.TYPE_SHARE.values())
best = max(cands, key=lambda t: (total * mix.TYPE_SHARE.get(mix.kind_of_task_dir(t), 0) / tot_share
- counts.get(mix.kind_of_task_dir(t), 0), -int(t[1:])))
self.inflight.add((best, 0))
return best, 0
for t in tasks: # second attempt: first failed, or accepted without a repair for t in tasks: # second attempt: first failed, or accepted without a repair
e = ev0.get(t) e = ev0.get(t)
if e and (t, 1) not in done and (t, 1) not in self.inflight and (not e["accepted"] or not e["repair"]): if e and (t, 1) not in done and (t, 1) not in self.inflight and (not e["accepted"] or not e["repair"]) \
and mix.kind_of_task_dir(t) in below:
self.inflight.add((t, 1)) self.inflight.add((t, 1))
return t, 1 return t, 1
return None return None
@@ -210,7 +224,7 @@ class Pipeline:
def old_generators(self): def old_generators(self):
out = subprocess.run(["pgrep", "-fl", r"harness\.trainset run --part"], capture_output=True, text=True).stdout out = subprocess.run(["pgrep", "-fl", r"harness\.trainset run --part"], capture_output=True, text=True).stdout
return [l for l in out.splitlines() if "--plan plan2" not in l] return [l for l in out.splitlines() if "--plan balanced" not in l and "--plan plan2" not in l]
def supervisor(self): def supervisor(self):
launched = False launched = False
@@ -221,15 +235,14 @@ class Pipeline:
if time.time() > self.gen_deadline or os.path.exists(trainset.STOP_FLAG): if time.time() > self.gen_deadline or os.path.exists(trainset.STOP_FLAG):
continue continue
if not launched and not self.old_generators(): if not launched and not self.old_generators():
trainset.ensure_plan("plan2")
dl = self.a.gen_deadline dl = self.a.gen_deadline
for i in range(3): for i in range(3):
f = open(os.path.join(ROOT, "runs", "gen_train", f"p2_w{i}.log"), "a") f = open(os.path.join(ROOT, "runs", "gen_train", f"p2_w{i}.log"), "a")
self.children.append(subprocess.Popen( self.children.append(subprocess.Popen(
[sys.executable, "-m", "harness.trainset", "run", "--plan", "plan2", "--part", str(i), [sys.executable, "-m", "harness.trainset", "run", "--plan", "balanced", "--part", str(i),
"--parts", "3", "--deadline", dl], cwd=ROOT, stdout=f, stderr=f)) "--parts", "3", "--deadline", dl], cwd=ROOT, stdout=f, stderr=f))
launched = True launched = True
log("plan 2 generators started (3 workers, deadline", dl + ")") log("balanced generators started (3 workers, deadline", dl + ")")
elif launched: elif launched:
for i, c in enumerate(self.children): for i, c in enumerate(self.children):
if c.poll() not in (None, 0) and crashes < 3: if c.poll() not in (None, 0) and crashes < 3:
@@ -237,7 +250,7 @@ class Pipeline:
log("generator", i, "exited with", c.returncode, "- restarted") log("generator", i, "exited with", c.returncode, "- restarted")
f = open(os.path.join(ROOT, "runs", "gen_train", f"p2_w{i}.log"), "a") f = open(os.path.join(ROOT, "runs", "gen_train", f"p2_w{i}.log"), "a")
self.children[i] = subprocess.Popen( self.children[i] = subprocess.Popen(
[sys.executable, "-m", "harness.trainset", "run", "--plan", "plan2", "--part", str(i), [sys.executable, "-m", "harness.trainset", "run", "--plan", "balanced", "--part", str(i),
"--parts", "3", "--deadline", self.a.gen_deadline], cwd=ROOT, stdout=f, stderr=f) "--parts", "3", "--deadline", self.a.gen_deadline], cwd=ROOT, stdout=f, stderr=f)
# K variants: free text, about 10 % of the other accepted training tasks # K variants: free text, about 10 % of the other accepted training tasks
if k_proc is None or k_proc.poll() is not None: if k_proc is None or k_proc.poll() is not None:
@@ -275,6 +288,25 @@ class Pipeline:
a[1] += e["accepted"] a[1] += e["accepted"]
a[2] += e["accepted"] and e["repair"] a[2] += e["accepted"] and e["repair"]
return "; ".join(f"{k} {v[1]}/{v[0]} (repair {v[2]})" for k, v in sorted(agg.items())) return "; ".join(f"{k} {v[1]}/{v[0]} (repair {v[2]})" for k, v in sorted(agg.items()))
kinds_t, kinds_r = mix.accepted_task_counts(), {}
for r, e in zip(rows, evs):
k = mix.kind_of_task_dir(r["task"])
a = kinds_r.setdefault(k, [0, 0])
a[0] += 1
a[1] += e["accepted"]
tt, tr_ = max(sum(kinds_t.values()), 1), max(sum(v[1] for v in kinds_r.values()), 1)
share_sum = sum(mix.TYPE_SHARE.values())
kind_line = "; ".join("%s tasks %d (%.0f%%) traj %d/%d (%.0f%%) target %.0f%%" % (
k, kinds_t.get(k, 0), 100.0 * kinds_t.get(k, 0) / tt, kinds_r.get(k, [0, 0])[1], kinds_r.get(k, [0, 0])[0],
100.0 * kinds_r.get(k, [0, 0])[1] / tr_, 100.0 * v / share_sum) for k, v in mix.TYPE_SHARE.items())
ddls = {}
for r, e in zip(rows, evs):
if mix.kind_of_task_dir(r["task"]) == "DDLS" and not e["accepted"]:
for w in (e["reasons"] or ["?"]):
w = w.split("=")[0] if w.startswith(("end_reason", "score")) else w
ddls[w] = ddls.get(w, 0) + 1
ddls_n = sum(1 for r in rows if mix.kind_of_task_dir(r["task"]) == "DDLS")
ddls_acc = sum(1 for r, e in zip(rows, evs) if mix.kind_of_task_dir(r["task"]) == "DDLS" and e["accepted"])
s = spent() s = spent()
used = (s - BASE_LEDGER) / LEDGER_TO_USAGE used = (s - BASE_LEDGER) / LEDGER_TO_USAGE
from .ledger import _env_budget from .ledger import _env_budget
@@ -290,6 +322,8 @@ trajectory runs {len(rows)}, accepted trajectories {len(accepted)} (acceptance {
- By category, accepted/runs: {table('category')}. - By category, accepted/runs: {table('category')}.
- By object type, accepted/runs: {table('object_type')}. - By object type, accepted/runs: {table('object_type')}.
- Tokens of accepted samples (20 tool schemas kept): p50 {pct(0.5)}, p90 {pct(0.9)}, p95 {pct(0.95)}, max {toks[-1] if toks else None}, n {len(toks)}. - Tokens of accepted samples (20 tool schemas kept): p50 {pct(0.5)}, p90 {pct(0.9)}, p95 {pct(0.95)}, max {toks[-1] if toks else None}, n {len(toks)}.
- Object type mix (accepted tasks; accepted/runs trajectories; target): {kind_line}.
- DDLS (CDS) runs: {ddls_acc} accepted of {ddls_n}; reject reasons {ddls or 'none'} (activation, hidden tests and ATC rejections are separate gate failures, they show as score reasons).
- Syntax hints (proxy syntaxCheck added): {sum(e['hints'] for e in evs)} in {sum(1 for e in evs if e['hints'])} runs. - Syntax hints (proxy syntaxCheck added): {sum(e['hints'] for e in evs)} in {sum(1 for e in evs if e['hints'])} runs.
- Harness events: {sum(self.events.values())} ({self.events or 'none'}); trajectory workers now {self.max_workers}. - Harness events: {sum(self.events.values())} ({self.events or 'none'}); trajectory workers now {self.max_workers}.
""" """
@@ -327,6 +361,26 @@ trajectory runs {len(rows)}, accepted trajectories {len(accepted)} (acceptance {
log("pipeline ended:", text.replace("\n", " | ")) log("pipeline ended:", text.replace("\n", " | "))
def acquire_controller_lock():
"""One controller only. An exclusive flock on runs/pipeline/controller.lock lives as long as the process (the
kernel drops it when the process dies, so a crash never leaves a stale lock); the pid is written for people."""
import fcntl
os.makedirs(PIPE, exist_ok=True)
path = os.path.join(PIPE, "controller.lock")
f = open(path, "a+")
try:
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
f.seek(0)
sys.exit("a controller is already running (pid %s, %s); not starting a second one" % (
f.read().strip() or "?", path))
f.seek(0)
f.truncate()
f.write("%d\n" % os.getpid())
f.flush()
return f
def main(): def main():
load_env(os.path.join(ROOT, ".env")) load_env(os.path.join(ROOT, ".env"))
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
@@ -348,7 +402,9 @@ def main():
break break
time.sleep(30) time.sleep(30)
os.makedirs(OUT, exist_ok=True) os.makedirs(OUT, exist_ok=True)
guard = acquire_controller_lock()
Pipeline(a).run() Pipeline(a).run()
guard.close()
if __name__ == "__main__": if __name__ == "__main__":

77
harness/restart_plan.py Normal file
View File

@@ -0,0 +1,77 @@
"""State and suggested settings for restarting the cloud work after the budget reset (12 October).
python3 -m harness.restart_plan [--panel 0.0] [--ratio 1.2]
Prints (no cloud call, no file change): ledger, the suggested BUDGET_* values from the panel value, the object
type deficits, the tasks waiting for a first run per kind, the second attempt candidates, and the commands.
"""
import argparse
import json
import os
from .adt_client import load_env
from . import mix
from .ledger import spent
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RESERVE_USAGE = 3.0
LEDGER_RESERVE = 8
def main():
load_env(os.path.join(ROOT, ".env"))
ap = argparse.ArgumentParser()
ap.add_argument("--panel", type=float, default=0.0, help="Ollama panel usage after the reset (USD)")
ap.add_argument("--ratio", type=float, default=1.2, help="ledger per usage, pessimistic (trajectory runs 1.2)")
a = ap.parse_args()
s = spent("2026-10-12")
usable = 60 - a.panel - RESERVE_USAGE
limit = round(usable * a.ratio + LEDGER_RESERVE)
print(f"ledger since 2026-10-12: {s:.2f}")
print(f"panel {a.panel} of 60, reserve {RESERVE_USAGE} usage -> usable {usable:.1f} usage x {a.ratio} = {usable * a.ratio:.0f} ledger")
print(f"set in .env: BUDGET_CYCLE_START=2026-10-12 BUDGET_LIMIT_USD={limit + int(s)} BUDGET_RESERVE_USD={LEDGER_RESERVE}")
rows = [json.loads(l) for l in open(os.path.join(ROOT, "runs", "traj", "summary.jsonl"))]
import sys
sys.path.insert(0, os.path.join(ROOT, "train"))
import accept
acc_traj, runs = {}, {}
first = {}
for r in rows:
k = mix.kind_of_task_dir(r["task"])
runs[k] = runs.get(k, 0) + 1
ok = False
p = os.path.join(ROOT, "runs", "traj", r.get("run_dir") or "-", "record.json")
if os.path.exists(p):
ok = accept.judge(json.load(open(p)), r, 80)[0]
acc_traj[k] = acc_traj.get(k, 0) + ok
if r["attempt"] == 0:
first[r["task"]] = ok
tot_t = max(sum(acc_traj.values()), 1)
tasks = mix.accepted_task_counts()
ss = sum(mix.TYPE_SHARE.values())
print("\nkind target tasks traj acc/runs (share) below target (generate + run)")
below_t, below_r = mix.below_target(tasks), mix.below_target(acc_traj)
for k, v in mix.TYPE_SHARE.items():
print(f"{k:5s} {100 * v / ss:5.1f}% {tasks.get(k, 0):6d} {acc_traj.get(k, 0):4d}/{runs.get(k, 0):<4d} ({100 * acc_traj.get(k, 0) / tot_t:4.1f}%) "
f"tasks:{'yes' if k in below_t else 'no '} trajectories:{'yes' if k in below_r else 'no'}")
done0 = {r["task"] for r in rows if r["attempt"] == 0}
done1 = {r["task"] for r in rows if r["attempt"] == 1}
waiting, second = {}, {}
import glob
for f in glob.glob(os.path.join(ROOT, "tasks_gen", "train", "G*", "generation.json")):
tid = os.path.basename(os.path.dirname(f))
if not json.load(open(f)).get("accepted"):
continue
k = mix.kind_of_task_dir(tid)
if tid not in done0:
waiting[k] = waiting.get(k, 0) + 1
elif tid not in done1 and not first.get(tid):
second[k] = second.get(k, 0) + 1
print("\nwaiting for a first run, per kind:", dict(sorted(waiting.items())))
print("second attempt candidates (first attempt failed), per kind:", dict(sorted(second.items())))
print("\nstart: python3 -m harness.pipeline (one controller only; it starts the generators, the K variants and the "
"trajectory workers; only kinds below target run; second attempts for failed tasks)")
if __name__ == "__main__":
main()

View File

@@ -21,7 +21,7 @@ CLEAN_RULES = {
} }
DELETE_ORDER = ["CLAS", "INTF", "PROG", "FUNC", "FUGR", "SRVD", "DDLX", "DCLS", "DDLS", DELETE_ORDER = ["CLAS", "INTF", "PROG", "FUNC", "FUGR", "SRVD", "DDLX", "DCLS", "DDLS",
"TTYP", "TABL", "STRU", "DTEL", "DOMA", "MSAG"] "TTYP", "TABL", "STRU", "DTEL", "DOMA", "MSAG"]
SOURCE_TYPES = ("CLAS", "INTF", "PROG", "FUNC", "DDLS", "DCLS", "DDLX", "TABL") SOURCE_TYPES = ("CLAS", "INTF", "PROG", "FUNC", "DDLS", "DCLS", "DDLX", "TABL", "STRU")
def _cds_elements(src): def _cds_elements(src):
@@ -40,6 +40,13 @@ def _cds_elements(src):
return out return out
def _ddl_fields(src):
"""Field names of a DDL table or structure source ('key name : type', 'name : type', 'include x')."""
body = re.sub(r"//[^\n]*|/\*.*?\*/", "", src or "", flags=re.S)
return {m.group(1).upper() for m in re.finditer(r"^\s*(?:key\s+)?(\w+)\s*:", body, re.I | re.M)
if not m.group(1).startswith("@") and m.group(1).lower() not in ("define",)}
def _obj_args(otype, name, fg=None): def _obj_args(otype, name, fg=None):
a = {"objectType": otype, "objectName": name} a = {"objectType": otype, "objectName": name}
if fg: if fg:
@@ -136,6 +143,9 @@ class Runner:
includeType="testclasses", includeType="testclasses",
source=o["testclasses_source"])) source=o["testclasses_source"]))
ok = ok and not e3 and (_json(t3) or {}).get("success", False) ok = ok and not e3 and (_json(t3) or {}).get("success", False)
if o.get("messages"): # message class (MSAG): created empty, messages written with sap_push_message
e5, t5 = mcp.call("sap_push_message", {"objectName": o["name"], "messages": o["messages"]})
ok = ok and not e5 and (_json(t5) or {}).get("success", False)
if o.get("run"): if o.get("run"):
e4, t4 = mcp.call("sap_run_class", {"className": o["name"]}) e4, t4 = mcp.call("sap_run_class", {"className": o["name"]})
ok = ok and not e4 and (_json(t4) or {}).get("success", False) ok = ok and not e4 and (_json(t4) or {}).get("success", False)
@@ -199,11 +209,13 @@ class Runner:
"line": i.get("start", {}).get("row"), "msg": i.get("description")} for i in issues] "line": i.get("start", {}).get("row"), "msg": i.get("description")} for i in issues]
# ---------- main ---------- # ---------- main ----------
def run(self, task_id, agent, run_no, teardown=True, rescore_dir=None, cds_calls=None): def run(self, task_id, agent, run_no, teardown=True, rescore_dir=None, cds_calls=None, tool_budget=None):
"""cds_calls: raise max_tool_calls to this value for tasks with a CDS (DDLS) contract object """cds_calls: raise max_tool_calls to this value for tasks with a CDS (DDLS) contract object
(trajectory runs, 2026-10-05: the own CDS test class needs more than 60 calls; eval stays at the task value).""" (trajectory runs, 2026-10-05: the own CDS test class needs more than 60 calls; eval stays at the task value)."""
prefix = prefix_for(run_no, task_id) prefix = prefix_for(run_no, task_id)
task = Task(os.path.join(self.tasks_root, task_id), prefix) task = Task(os.path.join(self.tasks_root, task_id), prefix)
if tool_budget: # fixed budget for every task (local model series: 60)
task.meta.setdefault("budget", {})["max_tool_calls"] = tool_budget
if cds_calls and any(c.get("type") == "DDLS" for c in task.meta.get("contract", [])): if cds_calls and any(c.get("type") == "DDLS" for c in task.meta.get("contract", [])):
b = task.meta.setdefault("budget", {}) b = task.meta.setdefault("budget", {})
b["max_tool_calls"] = max(b.get("max_tool_calls", 60), cds_calls) b["max_tool_calls"] = max(b.get("max_tool_calls", 60), cds_calls)
@@ -251,6 +263,15 @@ class Runner:
if proxy.fallbacks: if proxy.fallbacks:
rep["adt_fallbacks"] = proxy.fallbacks rep["adt_fallbacks"] = proxy.fallbacks
if rep.get("end_reason") in ("window_end", "server_down"): # not a result: no scoring, only the teardown
rep["score"] = {"total": None, "note": f"not scored: {rep['end_reason']}"}
rep["not_a_result"] = True
rep["seconds"] = round(time.time() - t0, 1)
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
# 3 collect # 3 collect
hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")} hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")}
seed_names = set(seed_src) seed_names = set(seed_src)
@@ -285,9 +306,11 @@ class Runner:
g2_detail = [] g2_detail = []
for c in contract: for c in contract:
src = sources.get((c["type"], c["name"].upper()), "") 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): impl = c.get("implements") or []
g2 = False for iname in ([impl] if isinstance(impl, str) else impl): # one interface or a list
g2_detail.append(f"{c['name']}: does not implement {c['implements']}") if not re.search(rf"INTERFACES\s+{re.escape(str(iname))}\b", src, re.I):
g2 = False
g2_detail.append(f"{c['name']}: does not implement {iname}")
if c["type"] == "FUNC": # signature: every parameter with its type in the FUNCTION header if c["type"] == "FUNC": # signature: every parameter with its type in the FUNCTION header
# the FUNCTION statement, not the first statement: local classes can come before it (G0107) # the FUNCTION statement, not the first statement: local classes can come before it (G0107)
fm = re.search(rf"^\s*FUNCTION\s+{re.escape(c['name'])}\b[^.]*\.", src, re.I | re.M) fm = re.search(rf"^\s*FUNCTION\s+{re.escape(c['name'])}\b[^.]*\.", src, re.I | re.M)
@@ -303,15 +326,25 @@ class Runner:
if not re.search(rf"(PARAMETERS|SELECT-OPTIONS)\s*:?[^.]*\b{re.escape(prm)}\b", src, re.I): if not re.search(rf"(PARAMETERS|SELECT-OPTIONS)\s*:?[^.]*\b{re.escape(prm)}\b", src, re.I):
g2 = False g2 = False
g2_detail.append(f"{c['name']}: no selection screen parameter {prm}") g2_detail.append(f"{c['name']}: no selection screen parameter {prm}")
if c["type"] == "DDLS" and c.get("fields") and g["G1_active"]: if c["type"] in ("DDLS", "TABL", "STRU") and c.get("fields") and g["G1_active"]:
_, q = mcp.call("sap_sql_query", {"query": f"SELECT * FROM {c['name']}", "maxRows": 1}) cols = set()
cols = {col.get("name", "").upper() for col in (_json(q) or {}).get("columns", [])} if c["type"] != "STRU": # a structure cannot be selected
_, 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 cols: # view with parameters: SELECT without parameters fails if not cols: # view with parameters: SELECT without parameters fails
cols = _cds_elements(src) cols = _cds_elements(src) if c["type"] == "DDLS" else _ddl_fields(src)
missing = {f.upper() for f in c["fields"]} - cols missing = {f.upper() for f in c["fields"]} - cols
if missing: if missing:
g2 = False g2 = False
g2_detail.append(f"{c['name']}: CDS elements missing: {sorted(missing)}") g2_detail.append(f"{c['name']}: {'CDS elements' if c['type'] == 'DDLS' else 'fields'} missing: {sorted(missing)}")
if c["type"] == "MSAG" and c.get("messages") and g["G1_active"]: # the numbers must exist (texts: hidden tests)
_, q = mcp.call("sap_sql_query", {"query": "SELECT msgnr FROM t100 WHERE arbgb = '%s' AND sprsl = 'E'"
% c["name"].upper(), "maxRows": 999})
have = {r.get("MSGNR") for r in (_json(q) or {}).get("rows", [])}
missing = {str(m["msgno"]).zfill(3) for m in c["messages"]} - have
if missing:
g2 = False
g2_detail.append(f"{c['name']}: message numbers missing: {sorted(missing)}")
# Categories E and I: the contract object is a seed object (refactor or fix in place). It must # Categories E and I: the contract object is a seed object (refactor or fix in place). It must
# change (else the null agent passes on the legacy code), and it is not out of scope. # change (else the null agent passes on the legacy code), and it is not out of scope.
contract_upper = {c["name"].upper() for c in contract} contract_upper = {c["name"].upper() for c in contract}
@@ -404,8 +437,15 @@ class Runner:
contract_files = {f"{c['name'].lower()}.{c['type'].lower()}.abap" for c in contract} contract_files = {f"{c['name'].lower()}.{c['type'].lower()}.abap" for c in contract}
# abaplint does not know standard superclasses (CX_STATIC_CHECK ...); an exception hierarchy # abaplint does not know standard superclasses (CX_STATIC_CHECK ...); an exception hierarchy
# then gives "Super class ... not found or contains errors". A4H activation (G1) covers this. # then gives "Super class ... not found or contains errors". A4H activation (G1) covers this.
# Cascade: a class that uses such an exception class gets "Definition for X not found in scope" (X is
# the exception class abaplint could not build). Messages that name a broken class are not counted.
broken = {re.sub(r"\.(clas|intf)\.abap$", "", i["file"]).upper() for i in issues
if re.match(r"Super class .* not found or contains errors", i["msg"] or "")}
def _cascade(msg):
return any(re.search(rf"\b{re.escape(b)}\b", msg or "", re.I) for b in broken)
g["G6_release"] = not any(i["rule"] == "check_syntax" and i["file"] in contract_files g["G6_release"] = not any(i["rule"] == "check_syntax" and i["file"] in contract_files
and not re.match(r"Super class .* not found or contains errors", i["msg"] or "") and not re.match(r"Super class .* not found or contains errors", i["msg"] or "")
and not _cascade(i["msg"])
for i in issues) for i in issues)
# 8 score # 8 score

View File

@@ -19,6 +19,7 @@ from .evalset import SLOTS, RELEASES, accepted_goals
from .generator import ROOT, generate, make_k_variant from .generator import ROOT, generate, make_k_variant
from .ledger import BudgetExceeded, spent from .ledger import BudgetExceeded, spent
from . import overlap from . import overlap
from . import mix
POOL = os.path.join(ROOT, "tasks_gen", "train") POOL = os.path.join(ROOT, "tasks_gen", "train")
PLAN = os.path.join(POOL, "plan.json") PLAN = os.path.join(POOL, "plan.json")
@@ -132,6 +133,28 @@ def backlog():
return len(acc - done) return len(acc - done)
def backlog_by_kind():
"""{kind: accepted tasks without a first trajectory run}."""
done = set()
sp = os.path.join(ROOT, "runs", "traj", "summary.jsonl")
if os.path.exists(sp):
done = {json.loads(l)["task"] for l in open(sp) if json.loads(l)["attempt"] == 0}
out = {}
for f in glob.glob(os.path.join(POOL, "G*", "generation.json")):
tid = os.path.basename(os.path.dirname(f))
try:
ok = json.load(open(f)).get("accepted")
except (OSError, ValueError):
ok = False
if ok and tid not in done:
k = mix.kind_of_task_dir(tid)
out[k] = out.get(k, 0) + 1
return out
BAL_KIND_BACKLOG = 8 # a kind with more waiting tasks than this is not generated (its trajectories come first)
def run(part, parts, target, stop_ledger, plan_name="plan", deadline=None): def run(part, parts, target, stop_ledger, plan_name="plan", deadline=None):
plan = ensure_plan(plan_name, stop_ledger, target) plan = ensure_plan(plan_name, stop_ledger, target)
stop_at = plan["ledger_at_start"] + plan["stop_ledger"] stop_at = plan["ledger_at_start"] + plan["stop_ledger"]
@@ -186,6 +209,140 @@ def run(part, parts, target, stop_ledger, plan_name="plan", deadline=None):
print(json.dumps(log), flush=True) print(json.dumps(log), flush=True)
BAL_FIRST_ID = 1910
BAL_RUN_BASE = 370000 # 40 per slot; below 466560 (a digit must lead the 4-char base36 run)
BAL_ERROR_KINDS = {"CLAS": ["named-type", "long-names"], "FUNC": ["named-type"], "DDLS": ["reserved-word"],
"TABL": ["reserved-word"], "STRU": ["reserved-word"]}
BAL_HINTS = dict(((c, t), h) for c, t, _, h in SLOTS if h)
def _claims_dir():
d = os.path.join(POOL, "_claims")
os.makedirs(d, exist_ok=True)
return d
def _claim_slot():
"""Next free balanced slot number, claimed with O_EXCL (several workers). Returns (n, claim path)."""
for n in range(BAL_FIRST_ID, BAL_FIRST_ID + 600):
sid = "G%04d" % n
if os.path.exists(os.path.join(POOL, "_logs", sid + ".json")):
continue
path = os.path.join(_claims_dir(), sid + ".json")
try:
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
except FileExistsError:
continue
os.close(fd)
return n, path
return None, None
def _kind_stats():
"""({kind: accepted}, {kind: attempted}) from the generation logs; claims of running slots count as attempted."""
acc = mix.accepted_task_counts()
att = {}
for f in glob.glob(os.path.join(POOL, "_logs", "G*.json")):
try:
l = json.load(open(f))
except (OSError, ValueError):
continue
k = l.get("kind") or l.get("object_type")
if k and l.get("category") != "K":
att[k] = att.get(k, 0) + 1
return acc, att
def run_balanced(part, parts, deadline):
"""Generation without a fixed plan: each slot takes the kind with the biggest deficit against mix.TYPE_SHARE.
A kind with 6 or more tries and an acceptance below 20 % is skipped (a harness or prompt problem: do not burn budget)."""
base_url = os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")
evals = overlap.load_pool("eval")
while True:
if os.path.exists(STOP_FLAG):
print("STOP flag", flush=True)
return
if deadline and time.time() > deadline:
print("DEADLINE", flush=True)
return
acc, att = _kind_stats()
running = {}
for f in glob.glob(os.path.join(_claims_dir(), "G*.json")):
try:
k = json.load(open(f)).get("kind")
except (OSError, ValueError):
k = None
if k:
running[k] = running.get(k, 0) + 1
counts = {k: acc.get(k, 0) + running.get(k, 0) for k in set(acc) | set(running) | set(mix.TYPE_SHARE)}
blocked = {k for k in mix.TYPE_SHARE if att.get(k, 0) >= 6 and acc.get(k, 0) < 0.2 * att.get(k, 0)}
if blocked:
print("kinds skipped (low acceptance):", sorted(blocked), flush=True)
waiting = {k for k, n in backlog_by_kind().items() if n > BAL_KIND_BACKLOG}
allowed = (set(mix.TYPE_SHARE) - blocked - waiting) & mix.below_target(counts) # only kinds below their share
if not allowed:
time.sleep(120) # every kind has a backlog: the trajectories are the slower side
continue
kind = mix.deficit_pick(counts, allowed=allowed)
n, claim = _claim_slot()
if n is None:
print("no free slot", flush=True)
return
sid = "G%04d" % n
json.dump({"kind": kind}, open(claim, "w"))
otype = "CLAS" if kind == "EXC" else kind
cats = mix.KIND_CATEGORIES[kind]
logs = [json.load(open(f)) for f in glob.glob(os.path.join(POOL, "_logs", "G*.json"))]
ccount = {c: sum(1 for l in logs if l.get("accepted") and l.get("category") == c) for c in cats}
tot = sum(ccount.values()) + 1
cat = max(cats, key=lambda c: tot * mix.CATEGORY_SHARE[c] / sum(mix.CATEGORY_SHARE[x] for x in cats) - ccount[c])
idx = n - BAL_FIRST_ID
error_kind = None
if idx % 5 == 2 and kind in BAL_ERROR_KINDS: # 20 % error-targeted slots
error_kind = BAL_ERROR_KINDS[kind][(idx // 5) % len(BAL_ERROR_KINDS[kind])]
cat = ERROR_CATEGORY[error_kind] if kind in ("CLAS", "FUNC") else cat
hard = cat != "H" and idx % 10 in (3, 6, 9) # 30 % hard
topic = None
if error_kind:
topic = dict(ERROR_HINTS)[error_kind]
elif kind == "EXC":
topic = "exception class (CX_...): " + ["a domain exception with context attributes and message texts",
"an exception hierarchy with a common super class",
"an exception that wraps a previous exception"][idx % 3]
elif (cat, otype) in BAL_HINTS:
h = BAL_HINTS[(cat, otype)]
topic = h[idx % len(h)]
if cat == "G":
topic = (topic + "; " if topic else "") + f"release target {RELEASES[idx % 2]}"
avoid = [g for g in accepted_goals() if g][-170:]
full = ((topic + ". ") if topic else "Choose a new, realistic business topic. ") + \
"Do not repeat these existing topics: " + "; ".join(avoid)
pool_now = evals + overlap.load_pool("train")
def extra(b, _pool=pool_now):
hits = overlap.check(overlap.load_bundle(b), _pool)
return [f"Too close to task {i} (similarity spec {sc['spec']:.2f}, rules {sc['core']:.2f}, "
f"names {sc['name']:.2f}). Choose a different business topic and different object names."
for i, sc in hits[:3]]
try:
log = generate(sid, "train", otype, cat, 3 if hard else 2, "deepseek-v4.1-flash:cloud", base_url,
BAL_RUN_BASE + 40 * idx, full, extra_check=extra)
except BudgetExceeded as e:
print("BUDGET", e, flush=True)
os.remove(claim)
return
except Exception as e: # noqa: BLE001
log = {"id": sid, "error": str(e)[:500]}
log.update(kind=kind, error_kind=error_kind, difficulty=3 if hard else 2, spent_total=spent())
os.makedirs(os.path.join(POOL, "_logs"), exist_ok=True)
json.dump(log, open(os.path.join(POOL, "_logs", sid + ".json"), "w"), indent=1)
stray = os.path.join(POOL, "generation.json")
if os.path.exists(stray):
os.remove(stray)
os.remove(claim)
print(json.dumps(log), flush=True)
K_FIRST_ID = 1300 K_FIRST_ID = 1300
K_RUN_BASE = 41800 # 20 per variant; above the trajectory run numbers (41000-41700) K_RUN_BASE = 41800 # 20 per variant; above the trajectory run numbers (41000-41700)
K_COUNT = 18 # K share of the eval plan: 10 of 110 (9 %); counted inside the 200 accepted tasks K_COUNT = 18 # K share of the eval plan: 10 of 110 (9 %); counted inside the 200 accepted tasks
@@ -240,7 +397,7 @@ def main():
ap.add_argument("--part", type=int, default=0) ap.add_argument("--part", type=int, default=0)
ap.add_argument("--parts", type=int, default=1) ap.add_argument("--parts", type=int, default=1)
ap.add_argument("--target", type=int, default=200) ap.add_argument("--target", type=int, default=200)
ap.add_argument("--plan", default="plan", help="plan (first 223 slots) or plan2 (hard and error share raised)") ap.add_argument("--plan", default="plan", help="plan (first 223 slots), plan2 (hard and error share raised) or balanced (kind with the biggest deficit)")
ap.add_argument("--deadline", help="YYYY-MM-DDTHH:MM local time: no new slot after it") ap.add_argument("--deadline", help="YYYY-MM-DDTHH:MM local time: no new slot after it")
ap.add_argument("--k-count", type=int, default=K_COUNT) ap.add_argument("--k-count", type=int, default=K_COUNT)
ap.add_argument("--stop-ledger", type=float, default=27.0, help="ledger USD for this phase (10 USD usage = 27)") ap.add_argument("--stop-ledger", type=float, default=27.0, help="ledger USD for this phase (10 USD usage = 27)")
@@ -255,6 +412,9 @@ def main():
run_k(a.k_count) run_k(a.k_count)
return return
dl = time.mktime(time.strptime(a.deadline, "%Y-%m-%dT%H:%M")) if a.deadline else None dl = time.mktime(time.strptime(a.deadline, "%Y-%m-%dT%H:%M")) if a.deadline else None
if a.plan == "balanced":
run_balanced(a.part, a.parts, dl)
return
run(a.part, a.parts, a.target, a.stop_ledger, a.plan, dl) run(a.part, a.parts, a.target, a.stop_ledger, a.plan, dl)

41
scripts_probe/deltest.py Normal file
View File

@@ -0,0 +1,41 @@
"""Delete an object through ADT while a write on it is running (what happened on 2026-10-05 19:25)."""
import json, sys, threading, time
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
from lockprobe import *
from killtest import MAIN, tc
def attempt(delay, n, extra=40):
name = "ZPROBE0DL_%03d" % n
with McpClient() as m:
m.call("sap_create_object", {"objectType": "CLAS", "objectName": name, "packageName": "$TMP", "description": "delete race"})
m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "source": MAIN.format(n=name.lower())})
res = {}
def writer():
with McpClient() as m:
res["t_write_start"] = time.time()
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "includeType": "testclasses", "source": tc(extra)})
res["write"] = t[:260]
res["t_write_end"] = time.time()
th = threading.Thread(target=writer); th.start()
time.sleep(delay)
res["delete_during_write"] = delete_uris(["/sap/bc/adt/oo/classes/" + name.lower()])
th.join()
time.sleep(2)
with McpClient() as m:
res["enqueue"] = read_locks(m)
e, t = m.call("sap_push_element", {"objectType": "CLAS", "objectName": name, "element": "RUN", "source": " METHOD run.\n rv = 2.\n ENDMETHOD.\n"})
res["follow_up"] = t[:200]
e, t = m.call("sap_search_object", {"query": name})
res["still_exists"] = name in t
res["delete_after"] = delete_uris(["/sap/bc/adt/oo/classes/" + name.lower()]) if res["still_exists"] else "n/a"
res["name"], res["delay"] = name, delay
return res
if __name__ == "__main__":
for i, d in enumerate(float(x) for x in sys.argv[1].split(",")):
r = attempt(d, int(sys.argv[2]) + i)
short = {k: v for k, v in r.items() if k not in ("t_write_start", "t_write_end")}
short["enqueue"] = short["enqueue"][:600]
print(json.dumps(short, indent=1))
if "[LOCK]" in r["follow_up"] or "enqueue entries matching the probe prefixes: 0" not in r["enqueue"]:
print("POSSIBLE LEAK at delay", d, r["name"]); break

View File

@@ -0,0 +1,52 @@
"""Two clients both CREATE and WRITE the same table at the same time (two controllers installing the seed of run 200273)."""
import json, os, signal, subprocess, sys, time
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
from lockprobe import *
from killtabl import DDL
VICTIM2 = r'''
import sys, json
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness")
from harness.adt_client import load_env; load_env("/Users/erhankeseli/projects/abap-llm/harness/.env")
from harness.mcp_client import McpClient
a = json.loads(sys.argv[1])
m = McpClient().open()
print("SENT", flush=True)
m.call("sap_create_object", {"objectType": "TABL", "objectName": a["objectName"], "packageName": "$TMP", "description": "seed"})
print("CREATED", flush=True)
m.call("sap_push_source", a)
print("DONE", flush=True)
'''
def attempt(delay, n):
name = "ZPROBE0K3_%03d" % n
args = {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=20 + n)}
ps = [subprocess.Popen([sys.executable, "-c", VICTIM2, json.dumps(args)], stdout=subprocess.PIPE, text=True) for _ in range(2)]
for p in ps:
assert p.stdout.readline().strip() == "SENT"
time.sleep(delay)
states = []
for p in ps:
states.append("finished" if p.poll() is not None else "killed")
try: os.kill(p.pid, signal.SIGKILL)
except ProcessLookupError: pass
p.wait()
time.sleep(3)
out = {"name": name, "delay": delay, "victims": states}
with McpClient() as m:
out["enqueue"] = read_locks(m)
e, t = m.call("sap_push_source", {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=40 + n)})
out["follow_up"] = t[:300]
e, t = m.call("sap_search_object", {"query": name}); out["exists"] = name in t
out["leak"] = "[LOCK]" in out["follow_up"] or "ZPROBE0K3" in out["enqueue"]
if out["exists"]:
out["delete"] = {k.split("/")[-1]: (v["deleted"], v["msg"]) for k, v in delete_uris(["/sap/bc/adt/ddic/tables/" + name.lower()]).items()}
return out
if __name__ == "__main__":
n = int(sys.argv[2])
for d in (float(x) for x in sys.argv[1].split(",")):
o = attempt(d, n); n += 1
o["enqueue"] = o["enqueue"][:1200]
print(json.dumps(o, indent=1))
if o["leak"]:
print("LEAK at delay", d, "object", o["name"]); break

51
scripts_probe/killtabl.py Normal file
View File

@@ -0,0 +1,51 @@
"""Controlled kill during the activation of a DDIC table (database table creation takes seconds)."""
import json, os, signal, subprocess, sys, time
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
from lockprobe import *
from killtest import VICTIM
DDL = """@EndUserText.label : 'kill test'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {n} {{
key client : abap.clnt not null;
key item_id : abap.char(10) not null;
qty : abap.int4;
name : abap.char({w});
}}"""
def attempt(delay, n):
name = "ZPROBE0KT_%03d" % n
with McpClient() as m:
print("create", m.call("sap_create_object", {"objectType": "TABL", "objectName": name, "packageName": "$TMP", "description": "kill table"})[1][:50])
# first write alone, to learn how long it takes
args = {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=20 + n)}
p = subprocess.Popen([sys.executable, "-c", VICTIM, json.dumps(args)], stdout=subprocess.PIPE, text=True)
assert p.stdout.readline().strip() == "SENT"
t0 = time.time(); time.sleep(delay)
finished = p.poll() is not None
try:
os.kill(p.pid, signal.SIGKILL)
except ProcessLookupError:
finished = True
p.wait()
print("table write: victim %s after %.2f s" % ("had finished" if finished else "killed", time.time() - t0))
time.sleep(3)
out = {"name": name, "delay": delay, "finished_before_kill": finished}
with McpClient() as m:
out["enqueue"] = read_locks(m)
e, t = m.call("sap_push_source", {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=40 + n)})
out["second_write"] = t[:300]
out["leak"] = "[LOCK]" in out["second_write"] or ("ZPROBE0KT" in out["enqueue"] and "enqueue entries matching the probe prefixes: 0" not in out["enqueue"])
out["delete"] = delete_uris(["/sap/bc/adt/ddic/tables/" + name.lower()])
return out
if __name__ == "__main__":
for i, d in enumerate(float(x) for x in sys.argv[1].split(",")):
o = attempt(d, int(sys.argv[2]) + i)
o["enqueue"] = o["enqueue"][:900]
print(json.dumps(o, indent=1))
if o["leak"]:
print("LEAK at delay", d, "object", o["name"]); break

71
scripts_probe/killtest.py Normal file
View File

@@ -0,0 +1,71 @@
"""Controlled kill: a client sends sap_push_source (testclasses include) and is killed with SIGKILL `delay` seconds
after the request left. Then the enqueue table is read and a second write is tried."""
import json, os, signal, subprocess, sys, time
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
from lockprobe import *
MAIN = """CLASS {n} DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS run RETURNING VALUE(rv) TYPE i.
ENDCLASS.
CLASS {n} IMPLEMENTATION.
METHOD run.
rv = 1.
ENDMETHOD.
ENDCLASS.
"""
def tc(extra):
body = "".join(" cl_abap_unit_assert=>assert_equals( act = %d exp = %d ).\n" % (i, i) for i in range(extra))
return ("CLASS ltc DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.\n PRIVATE SECTION.\n METHODS t1 FOR TESTING.\n"
"ENDCLASS.\nCLASS ltc IMPLEMENTATION.\n METHOD t1.\n" + body + " ENDMETHOD.\nENDCLASS.\n")
VICTIM = r'''
import sys, json
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness")
from harness.adt_client import load_env; load_env("/Users/erhankeseli/projects/abap-llm/harness/.env")
from harness.mcp_client import McpClient
args = json.loads(sys.argv[1])
m = McpClient().open()
print("SENT", flush=True)
m.call("sap_push_source", args)
print("DONE", flush=True)
'''
def attempt(delay, n, extra=40):
name = "ZPROBE0KL_%03d" % n
with McpClient() as m:
print("create", m.call("sap_create_object", {"objectType": "CLAS", "objectName": name, "packageName": "$TMP", "description": "kill test"})[1][:60])
print("main ", m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "source": MAIN.format(n=name.lower())})[1][:70])
args = {"objectType": "CLAS", "objectName": name, "includeType": "testclasses", "source": tc(extra)}
p = subprocess.Popen([sys.executable, "-c", VICTIM, json.dumps(args)], stdout=subprocess.PIPE, text=True)
assert p.stdout.readline().strip() == "SENT"
t0 = time.time()
time.sleep(delay)
done_before_kill = p.poll() is not None
try:
os.kill(p.pid, signal.SIGKILL)
except ProcessLookupError:
done_before_kill = True
p.wait()
print("victim killed %.2f s after the request was sent (finished before kill: %s)" % (time.time() - t0, done_before_kill))
time.sleep(3)
out = {"name": name, "delay": delay}
with McpClient() as m:
out["enqueue"] = read_locks(m)
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "includeType": "testclasses", "source": tc(extra + 1)})
out["second_write"] = t[:300]
e, t = m.call("sap_push_element", {"objectType": "CLAS", "objectName": name, "element": "RUN", "source": " METHOD run.\n rv = 2.\n ENDMETHOD.\n"})
out["push_element"] = t[:300]
out["leak"] = "currently editing" in out["second_write"] or "[LOCK]" in out["second_write"] or "[LOCK]" in out["push_element"]
if not out["leak"]:
out["delete"] = delete_uris(["/sap/bc/adt/oo/classes/" + name.lower()])
return out
if __name__ == "__main__":
delays = [float(x) for x in sys.argv[1].split(",")]
n0 = int(sys.argv[2]) if len(sys.argv) > 2 else 1
for i, d in enumerate(delays):
o = attempt(d, n0 + i, int(sys.argv[3]) if len(sys.argv) > 3 else 40)
print(json.dumps({k: (v if k != "enqueue" else v[:700]) for k, v in o.items()}, indent=1))
if o["leak"]:
print("LEAK at delay", d, "- stopping, object", o["name"], "needs SM12"); break

42
scripts_probe/killtwo.py Normal file
View File

@@ -0,0 +1,42 @@
"""Two clients write the same table at the same time (what two controllers did with run 200273); both are killed."""
import json, os, signal, subprocess, sys, time
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
from lockprobe import *
from killtest import VICTIM
from killtabl import DDL
def attempt(delay, n):
name = "ZPROBE0K2_%03d" % n
with McpClient() as m:
m.call("sap_create_object", {"objectType": "TABL", "objectName": name, "packageName": "$TMP", "description": "two writers"})
args = {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=20 + n)}
ps = [subprocess.Popen([sys.executable, "-c", VICTIM, json.dumps(args)], stdout=subprocess.PIPE, text=True) for _ in range(2)]
for p in ps:
assert p.stdout.readline().strip() == "SENT"
time.sleep(delay)
states = []
for p in ps:
states.append("finished" if p.poll() is not None else "killed")
try:
os.kill(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
p.wait()
time.sleep(3)
out = {"name": name, "delay": delay, "victims": states}
with McpClient() as m:
out["enqueue"] = read_locks(m)
e, t = m.call("sap_push_source", {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=40 + n)})
out["follow_up"] = t[:300]
out["leak"] = "[LOCK]" in out["follow_up"] or ("ZPROBE0K2" in out["enqueue"])
out["delete"] = {k.split("/")[-1]: v["deleted"] for k, v in delete_uris(["/sap/bc/adt/ddic/tables/" + name.lower()]).items()}
return out
if __name__ == "__main__":
n = int(sys.argv[2])
for d in (float(x) for x in sys.argv[1].split(",")):
o = attempt(d, n); n += 1
o["enqueue"] = o["enqueue"][:1200]
print(json.dumps(o, indent=1))
if o["leak"]:
print("LEAK at delay", d, "object", o["name"]); break

View File

@@ -0,0 +1,58 @@
"""Lock leak probe helpers (no cloud, A4H only): the enqueue reader class and a controlled kill of a writing client."""
import json, os, signal, subprocess, sys, time
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness")
from harness.adt_client import load_env
load_env("/Users/erhankeseli/projects/abap-llm/harness/.env")
from harness.mcp_client import McpClient
from harness.runner import delete_uris
READER = "ZPROBE0EQ_LOCKS"
READER_SRC = """CLASS zprobe0eq_locks DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zprobe0eq_locks IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA lt_enq TYPE STANDARD TABLE OF seqg3 WITH DEFAULT KEY.
DATA lv_subrc TYPE sy-subrc.
CALL FUNCTION 'ENQUEUE_READ'
EXPORTING gclient = sy-mandt gname = '' garg = '' guname = ''
IMPORTING subrc = lv_subrc
TABLES enq = lt_enq
EXCEPTIONS communication_failure = 1 system_failure = 2 OTHERS = 3.
DATA(lv_n) = 0.
LOOP AT lt_enq ASSIGNING FIELD-SYMBOL(<l>) WHERE garg CS 'ZPROBE0' OR garg CS 'Z4AJ' OR garg CS 'Z4AE'.
lv_n = lv_n + 1.
DATA(lv_line) = ||.
DO.
ASSIGN COMPONENT sy-index OF STRUCTURE <l> TO FIELD-SYMBOL(<f>).
IF sy-subrc <> 0. EXIT. ENDIF.
DATA(lo_d) = CAST cl_abap_structdescr( cl_abap_typedescr=>describe_by_data( <l> ) ).
DATA(lv_name) = lo_d->components[ sy-index ]-name.
IF <f> IS NOT INITIAL.
lv_line = lv_line && lv_name && '=' && condense( |{ <f> }| ) && ' '.
ENDIF.
ENDDO.
out->write( lv_line ).
ENDLOOP.
out->write( |enqueue entries matching the probe prefixes: { lv_n } of { lines( lt_enq ) } (subrc { lv_subrc })| ).
ENDMETHOD.
ENDCLASS.
"""
def ensure_reader(m):
e, t = m.call("sap_search_object", {"query": READER})
if READER not in t:
m.call("sap_create_object", {"objectType": "CLAS", "objectName": READER, "packageName": "$TMP", "description": "enqueue reader probe"})
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": READER, "source": READER_SRC})
if '"success":true' not in t.replace(" ", ""):
print("reader activation:", t[:600])
def read_locks(m):
e, t = m.call("sap_run_class", {"className": READER})
return t
if __name__ == "__main__":
with McpClient() as m:
ensure_reader(m)
print(read_locks(m)[:2500])

View File

@@ -0,0 +1,105 @@
REPORT {{p}}oil_report.
DATA gv_region TYPE {{p}}grove-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}}grove-region,
litres TYPE {{p}}press-litres,
kilos TYPE {{p}}press-kilos,
END OF ty_line,
tt_line TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY.
TYPES tt_region TYPE RANGE OF {{p}}grove-region.
METHODS collect
IMPORTING iv_from TYPE d
it_region TYPE tt_region
RETURNING VALUE(rt_lines) TYPE tt_line.
METHODS show
CHANGING ct_lines TYPE tt_line.
ENDCLASS.
CLASS lcl_report IMPLEMENTATION.
METHOD collect.
SELECT gr~region,
SUM( pr~litres ) AS litres,
SUM( pr~kilos ) AS kilos
FROM {{p}}press AS pr
INNER JOIN {{p}}grove AS gr ON gr~grove_id = pr~grove_id
WHERE pr~press_date < @iv_from
AND gr~region IN @it_region
GROUP BY gr~region
ORDER BY litres DESCENDING
INTO CORRESPONDING FIELDS OF TABLE @rt_lines.
ENDMETHOD.
METHOD show.
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->collect( iv_from = p_from it_region = s_reg[] ).
go_report->show( 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 litres_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}}GROVE' ) ( '{{P}}PRESS' ) ) ).
DATA lt_grove TYPE STANDARD TABLE OF {{p}}grove WITH EMPTY KEY.
lt_grove = VALUE #( ( grove_id = 1 grove_name = 'A' region = 'NORTH' variety = 'ARBEQ' )
( grove_id = 2 grove_name = 'B' region = 'NORTH' variety = 'ARBEQ' )
( grove_id = 3 grove_name = 'C' region = 'SOUTH' variety = 'PICUA' ) ).
go_env->insert_test_data( lt_grove ).
DATA lt_press TYPE STANDARD TABLE OF {{p}}press WITH EMPTY KEY.
lt_press = VALUE #( ( press_id = 1 grove_id = 1 press_date = '20260110' kilos = 1000 litres = '200.00' )
( press_id = 2 grove_id = 2 press_date = '20260111' kilos = 500 litres = '90.00' )
( press_id = 3 grove_id = 3 press_date = '20260112' kilos = 800 litres = '150.00' ) ).
go_env->insert_test_data( lt_press ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD litres_per_region.
DATA lv_expected TYPE {{p}}press-litres.
lv_expected = '290.00'.
DATA(lt_lines) = NEW lcl_report( )->collect(
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 = 'NORTH' act = lt_lines[ 1 ]-region ).
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
cl_abap_unit_assert=>assert_equals( exp = 1500 act = lt_lines[ 1 ]-kilos ).
ENDMETHOD.
METHOD region_filter.
DATA lv_expected TYPE {{p}}press-litres.
lv_expected = '150.00'.
DATA(lt_lines) = NEW lcl_report( )->collect(
iv_from = '20260101'
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ).
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,105 @@
REPORT {{p}}oil_report.
DATA gv_region TYPE {{p}}grove-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}}grove-region,
litres TYPE {{p}}press-litres,
kilos TYPE {{p}}press-kilos,
END OF ty_line,
tt_line TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY.
TYPES tt_region TYPE RANGE OF {{p}}grove-region.
METHODS collect
IMPORTING iv_from TYPE d
it_region TYPE tt_region
RETURNING VALUE(rt_lines) TYPE tt_line.
METHODS show
CHANGING ct_lines TYPE tt_line.
ENDCLASS.
CLASS lcl_report IMPLEMENTATION.
METHOD collect.
SELECT gr~region,
SUM( pr~litres ) AS litres,
SUM( pr~kilos ) AS kilos
FROM {{p}}press AS pr
INNER JOIN {{p}}grove AS gr ON gr~grove_id = pr~grove_id
WHERE pr~press_date >= @iv_from
OR gr~region IN @it_region
GROUP BY gr~region
ORDER BY litres DESCENDING
INTO CORRESPONDING FIELDS OF TABLE @rt_lines.
ENDMETHOD.
METHOD show.
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->collect( iv_from = p_from it_region = s_reg[] ).
go_report->show( 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 litres_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}}GROVE' ) ( '{{P}}PRESS' ) ) ).
DATA lt_grove TYPE STANDARD TABLE OF {{p}}grove WITH EMPTY KEY.
lt_grove = VALUE #( ( grove_id = 1 grove_name = 'A' region = 'NORTH' variety = 'ARBEQ' )
( grove_id = 2 grove_name = 'B' region = 'NORTH' variety = 'ARBEQ' )
( grove_id = 3 grove_name = 'C' region = 'SOUTH' variety = 'PICUA' ) ).
go_env->insert_test_data( lt_grove ).
DATA lt_press TYPE STANDARD TABLE OF {{p}}press WITH EMPTY KEY.
lt_press = VALUE #( ( press_id = 1 grove_id = 1 press_date = '20260110' kilos = 1000 litres = '200.00' )
( press_id = 2 grove_id = 2 press_date = '20260111' kilos = 500 litres = '90.00' )
( press_id = 3 grove_id = 3 press_date = '20260112' kilos = 800 litres = '150.00' ) ).
go_env->insert_test_data( lt_press ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD litres_per_region.
DATA lv_expected TYPE {{p}}press-litres.
lv_expected = '290.00'.
DATA(lt_lines) = NEW lcl_report( )->collect(
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 = 'NORTH' act = lt_lines[ 1 ]-region ).
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
cl_abap_unit_assert=>assert_equals( exp = 1500 act = lt_lines[ 1 ]-kilos ).
ENDMETHOD.
METHOD region_filter.
DATA lv_expected TYPE {{p}}press-litres.
lv_expected = '150.00'.
DATA(lt_lines) = NEW lcl_report( )->collect(
iv_from = '20260101'
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ).
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,34 @@
{
"id": "G1140",
"pool": "train",
"object_type": "PROG",
"category": "B",
"attempts": [
{
"stage": "bundle",
"errors": [
"Too close to task G0020 (similarity spec 0.67, rules 0.49, names 1.00). Choose a different business topic and different object names.",
"Too close to task G0183 (similarity spec 0.60, rules 0.00, names 1.00). Choose a different business topic and different object names."
],
"by_check": {
"extra": 2
}
},
{
"stage": "validate",
"oracle": 86.7,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 2,
"killed": 2,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -15,7 +15,7 @@ CLASS {{p}}mill_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
IMPORTING ir_data TYPE REF TO data IMPORTING ir_data TYPE REF TO data
iv_row TYPE i iv_row TYPE i
iv_col TYPE string iv_col TYPE string
RETURNING VALUE(rv_value) TYPE string. RETURNING VALUE(rv_value) TYPE {{p}}press-litres.
METHODS lines_of METHODS lines_of
IMPORTING ir_data TYPE REF TO data IMPORTING ir_data TYPE REF TO data
RETURNING VALUE(rv_lines) TYPE i. RETURNING VALUE(rv_lines) TYPE i.
@@ -60,9 +60,12 @@ CLASS {{p}}mill_hidden IMPLEMENTATION.
ENDMETHOD. ENDMETHOD.
METHOD amount_of. METHOD amount_of.
DATA lv_amount TYPE p LENGTH 15 DECIMALS 2. FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
lv_amount = value_of( ir_data = ir_data iv_row = iv_row iv_col = iv_col ). ASSIGN ir_data->* TO <lt_data>.
rv_value = CONV string( lv_amount ). 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 = <lv_value>.
ENDMETHOD. ENDMETHOD.
METHOD all_regions. METHOD all_regions.
@@ -70,29 +73,40 @@ CLASS {{p}}mill_hidden IMPLEMENTATION.
ENDMETHOD. ENDMETHOD.
METHOD sorted_by_litres. METHOD sorted_by_litres.
DATA(lr_data) = run( '20260101' ). DATA lr_data TYPE REF TO data.
lr_data = run( '20260101' ).
cl_abap_unit_assert=>assert_equals( exp = `NORTH` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ). cl_abap_unit_assert=>assert_equals( exp = `NORTH` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ).
cl_abap_unit_assert=>assert_equals( exp = `SOUTH` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `REGION` ) ). cl_abap_unit_assert=>assert_equals( exp = `SOUTH` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `REGION` ) ).
cl_abap_unit_assert=>assert_equals( exp = `WEST` act = value_of( ir_data = lr_data iv_row = 3 iv_col = `REGION` ) ). cl_abap_unit_assert=>assert_equals( exp = `WEST` act = value_of( ir_data = lr_data iv_row = 3 iv_col = `REGION` ) ).
ENDMETHOD. ENDMETHOD.
METHOD litres_and_kilos. METHOD litres_and_kilos.
DATA(lr_data) = run( '20260101' ). DATA lr_data TYPE REF TO data.
cl_abap_unit_assert=>assert_equals( exp = `290.00` act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ). DATA lv_expected TYPE {{p}}press-litres.
cl_abap_unit_assert=>assert_equals( exp = `1500` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `KILOS` ) ). lr_data = run( '20260101' ).
lv_expected = '290.00'.
cl_abap_unit_assert=>assert_equals( exp = lv_expected
act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ).
cl_abap_unit_assert=>assert_equals( exp = `1500`
act = value_of( ir_data = lr_data iv_row = 1 iv_col = `KILOS` ) ).
ENDMETHOD. ENDMETHOD.
METHOD region_filter. METHOD region_filter.
DATA(lr_data) = run( iv_from = '20260101' DATA lr_data TYPE REF TO data.
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ). DATA lv_expected TYPE {{p}}press-litres.
lr_data = run( iv_from = '20260101'
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
lv_expected = '150.00'.
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines_of( lr_data ) ). cl_abap_unit_assert=>assert_equals( exp = 1 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `150.00` act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ). cl_abap_unit_assert=>assert_equals( exp = lv_expected
act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ).
ENDMETHOD. ENDMETHOD.
METHOD two_region_filter. METHOD two_region_filter.
DATA(lr_data) = run( iv_from = '20260101' DATA lr_data TYPE REF TO data.
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'WEST' ) lr_data = run( iv_from = '20260101'
( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ). it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'WEST' )
( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( lr_data ) ). cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `SOUTH` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ). cl_abap_unit_assert=>assert_equals( exp = `SOUTH` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ).
cl_abap_unit_assert=>assert_equals( exp = `WEST` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `REGION` ) ). cl_abap_unit_assert=>assert_equals( exp = `WEST` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `REGION` ) ).

View File

@@ -1,5 +1,5 @@
{ {
"task": "T50", "task": "G1140",
"mutants": [ "mutants": [
{ {
"object": "{{P}}OIL_REPORT", "object": "{{P}}OIL_REPORT",
@@ -8,37 +8,29 @@
"hidden": "0/6", "hidden": "0/6",
"failed_tests": [ "failed_tests": [
"ALL_REGIONS", "ALL_REGIONS",
"SORTED_BY_LITRES", "DATE_FILTER",
"LITRES_AND_KILOS", "LITRES_AND_KILOS",
"REGION_FILTER", "REGION_FILTER",
"TWO_REGION_FILTER", "SORTED_BY_LITRES",
"DATE_FILTER" "TWO_REGION_FILTER"
] ]
}, },
{ {
"object": "{{P}}OIL_REPORT", "object": "{{P}}OIL_REPORT",
"mutant": "line 34: AND -> OR (logic)", "mutant": "line 34: AND -> OR (logic)",
"status": "killed", "status": "killed",
"hidden": "3/6", "hidden": "1/6",
"failed_tests": [ "failed_tests": [
"ALL_REGIONS",
"DATE_FILTER",
"REGION_FILTER", "REGION_FILTER",
"TWO_REGION_FILTER",
"DATE_FILTER"
]
},
{
"object": "{{P}}OIL_REPORT",
"mutant": "line 36: DESCENDING -> ASCENDING (order)",
"status": "killed",
"hidden": "4/6",
"failed_tests": [
"SORTED_BY_LITRES", "SORTED_BY_LITRES",
"TWO_REGION_FILTER" "TWO_REGION_FILTER"
] ]
} }
], ],
"valid": 3, "valid": 2,
"killed": 3, "killed": 2,
"kill_rate": 1.0, "kill_rate": 1.0,
"ok": true "ok": true
} }

View File

@@ -0,0 +1,51 @@
INTERFACE {{p}}if_parcel_fee PUBLIC.
TYPES ty_weight_g TYPE i.
TYPES ty_cents TYPE i.
TYPES ty_zone TYPE c LENGTH 1.
TYPES ty_reason TYPE c LENGTH 9.
CONSTANTS:
BEGIN OF c_zone,
home TYPE ty_zone VALUE '1',
national TYPE ty_zone VALUE '2',
europe TYPE ty_zone VALUE '3',
END OF c_zone.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'OK',
weight TYPE ty_reason VALUE 'WEIGHT',
zone TYPE ty_reason VALUE 'ZONE',
END OF c_reason.
CONSTANTS:
BEGIN OF c_limit,
min_weight TYPE ty_weight_g VALUE 1,
max_weight TYPE ty_weight_g VALUE 31500,
END OF c_limit.
CONSTANTS:
BEGIN OF c_surcharge,
per_step TYPE ty_cents VALUE 90,
express TYPE ty_cents VALUE 750,
END OF c_surcharge.
METHODS check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason.
METHODS base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents.
ENDINTERFACE.

View File

@@ -0,0 +1,51 @@
INTERFACE {{p}}if_parcel_fee PUBLIC.
TYPES ty_weight_g TYPE i.
TYPES ty_cents TYPE i.
TYPES ty_zone TYPE c LENGTH 1.
TYPES ty_reason TYPE c LENGTH 10.
CONSTANTS:
BEGIN OF c_zone,
home TYPE ty_zone VALUE 'Z',
national TYPE ty_zone VALUE '2',
europe TYPE ty_zone VALUE '3',
END OF c_zone.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'OK',
weight TYPE ty_reason VALUE 'WEIGHT',
zone TYPE ty_reason VALUE 'ZONE',
END OF c_reason.
CONSTANTS:
BEGIN OF c_limit,
min_weight TYPE ty_weight_g VALUE 1,
max_weight TYPE ty_weight_g VALUE 31500,
END OF c_limit.
CONSTANTS:
BEGIN OF c_surcharge,
per_step TYPE ty_cents VALUE 90,
express TYPE ty_cents VALUE 750,
END OF c_surcharge.
METHODS check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason.
METHODS base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents.
ENDINTERFACE.

View File

@@ -0,0 +1,51 @@
INTERFACE {{p}}if_parcel_fee PUBLIC.
TYPES ty_weight_g TYPE i.
TYPES ty_cents TYPE i.
TYPES ty_zone TYPE c LENGTH 1.
TYPES ty_reason TYPE c LENGTH 10.
CONSTANTS:
BEGIN OF c_zone,
home TYPE ty_zone VALUE '1',
national TYPE ty_zone VALUE '2',
europe TYPE ty_zone VALUE 'Z',
END OF c_zone.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'OK',
weight TYPE ty_reason VALUE 'WEIGHT',
zone TYPE ty_reason VALUE 'ZONE',
END OF c_reason.
CONSTANTS:
BEGIN OF c_limit,
min_weight TYPE ty_weight_g VALUE 1,
max_weight TYPE ty_weight_g VALUE 31500,
END OF c_limit.
CONSTANTS:
BEGIN OF c_surcharge,
per_step TYPE ty_cents VALUE 90,
express TYPE ty_cents VALUE 750,
END OF c_surcharge.
METHODS check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason.
METHODS base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents.
ENDINTERFACE.

View File

@@ -0,0 +1,51 @@
INTERFACE {{p}}if_parcel_fee PUBLIC.
TYPES ty_weight_g TYPE i.
TYPES ty_cents TYPE i.
TYPES ty_zone TYPE c LENGTH 1.
TYPES ty_reason TYPE c LENGTH 10.
CONSTANTS:
BEGIN OF c_zone,
home TYPE ty_zone VALUE '1',
national TYPE ty_zone VALUE '2',
europe TYPE ty_zone VALUE '3',
END OF c_zone.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'ZK',
weight TYPE ty_reason VALUE 'WEIGHT',
zone TYPE ty_reason VALUE 'ZONE',
END OF c_reason.
CONSTANTS:
BEGIN OF c_limit,
min_weight TYPE ty_weight_g VALUE 1,
max_weight TYPE ty_weight_g VALUE 31500,
END OF c_limit.
CONSTANTS:
BEGIN OF c_surcharge,
per_step TYPE ty_cents VALUE 90,
express TYPE ty_cents VALUE 750,
END OF c_surcharge.
METHODS check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason.
METHODS base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents.
ENDINTERFACE.

View File

@@ -0,0 +1,51 @@
INTERFACE {{p}}if_parcel_fee PUBLIC.
TYPES ty_weight_g TYPE i.
TYPES ty_cents TYPE i.
TYPES ty_zone TYPE c LENGTH 1.
TYPES ty_reason TYPE c LENGTH 10.
CONSTANTS:
BEGIN OF c_zone,
home TYPE ty_zone VALUE '1',
national TYPE ty_zone VALUE '2',
europe TYPE ty_zone VALUE '3',
END OF c_zone.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'OK',
weight TYPE ty_reason VALUE 'WEIGHT',
zone TYPE ty_reason VALUE 'YONE',
END OF c_reason.
CONSTANTS:
BEGIN OF c_limit,
min_weight TYPE ty_weight_g VALUE 1,
max_weight TYPE ty_weight_g VALUE 31500,
END OF c_limit.
CONSTANTS:
BEGIN OF c_surcharge,
per_step TYPE ty_cents VALUE 90,
express TYPE ty_cents VALUE 750,
END OF c_surcharge.
METHODS check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason.
METHODS base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents.
ENDINTERFACE.

View File

@@ -0,0 +1,29 @@
{
"id": "G1900",
"pool": "train",
"object_type": "INTF",
"category": "A",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 80.6,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 5,
"killed": 5,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -0,0 +1,159 @@
CLASS {{p}}parcel_fee_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PUBLIC SECTION.
INTERFACES {{p}}if_parcel_fee.
PRIVATE SECTION.
METHODS zone_values FOR TESTING.
METHODS reason_values FOR TESTING.
METHODS limit_values FOR TESTING.
METHODS surcharge_values FOR TESTING.
METHODS type_lengths FOR TESTING.
METHODS check_input_sig FOR TESTING.
METHODS base_fee_sig FOR TESTING.
METHODS weight_surcharge_sig FOR TESTING.
METHODS total_fee_sig FOR TESTING.
ENDCLASS.
CLASS {{p}}parcel_fee_hidden IMPLEMENTATION.
METHOD {{p}}if_parcel_fee~check_input.
IF iv_weight_g < {{p}}if_parcel_fee=>c_limit-min_weight
OR iv_weight_g > {{p}}if_parcel_fee=>c_limit-max_weight.
rv_reason = {{p}}if_parcel_fee=>c_reason-weight.
ELSEIF iv_zone <> {{p}}if_parcel_fee=>c_zone-home
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-national
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-europe.
rv_reason = {{p}}if_parcel_fee=>c_reason-zone.
ELSE.
rv_reason = {{p}}if_parcel_fee=>c_reason-ok.
ENDIF.
ENDMETHOD.
METHOD {{p}}if_parcel_fee~base_fee.
CASE iv_zone.
WHEN {{p}}if_parcel_fee=>c_zone-home.
rv_cents = 350.
WHEN {{p}}if_parcel_fee=>c_zone-national.
rv_cents = 620.
WHEN {{p}}if_parcel_fee=>c_zone-europe.
rv_cents = 990.
WHEN OTHERS.
rv_cents = 0.
ENDCASE.
ENDMETHOD.
METHOD {{p}}if_parcel_fee~weight_surcharge.
IF iv_weight_g > 1000.
rv_cents = ( iv_weight_g - 1000 + 999 ) DIV 1000
* {{p}}if_parcel_fee=>c_surcharge-per_step.
ENDIF.
ENDMETHOD.
METHOD {{p}}if_parcel_fee~total_fee.
IF me->{{p}}if_parcel_fee~check_input( iv_weight_g = iv_weight_g
iv_zone = iv_zone )
<> {{p}}if_parcel_fee=>c_reason-ok.
RETURN.
ENDIF.
rv_cents = me->{{p}}if_parcel_fee~base_fee( iv_zone )
+ me->{{p}}if_parcel_fee~weight_surcharge( iv_weight_g ).
IF iv_express = abap_true.
rv_cents = rv_cents + {{p}}if_parcel_fee=>c_surcharge-express.
ENDIF.
ENDMETHOD.
METHOD zone_values.
cl_abap_unit_assert=>assert_equals( exp = '1' act = {{p}}if_parcel_fee=>c_zone-home ).
cl_abap_unit_assert=>assert_equals( exp = '2' act = {{p}}if_parcel_fee=>c_zone-national ).
cl_abap_unit_assert=>assert_equals( exp = '3' act = {{p}}if_parcel_fee=>c_zone-europe ).
ENDMETHOD.
METHOD reason_values.
cl_abap_unit_assert=>assert_equals( exp = 'OK' act = {{p}}if_parcel_fee=>c_reason-ok ).
cl_abap_unit_assert=>assert_equals( exp = 'WEIGHT' act = {{p}}if_parcel_fee=>c_reason-weight ).
cl_abap_unit_assert=>assert_equals( exp = 'ZONE' act = {{p}}if_parcel_fee=>c_reason-zone ).
ENDMETHOD.
METHOD limit_values.
cl_abap_unit_assert=>assert_equals( exp = 1 act = {{p}}if_parcel_fee=>c_limit-min_weight ).
cl_abap_unit_assert=>assert_equals( exp = 31500 act = {{p}}if_parcel_fee=>c_limit-max_weight ).
ENDMETHOD.
METHOD surcharge_values.
cl_abap_unit_assert=>assert_equals( exp = 90 act = {{p}}if_parcel_fee=>c_surcharge-per_step ).
cl_abap_unit_assert=>assert_equals( exp = 750 act = {{p}}if_parcel_fee=>c_surcharge-express ).
ENDMETHOD.
METHOD type_lengths.
DATA lv_zone TYPE {{p}}if_parcel_fee=>ty_zone.
DATA lv_reason TYPE {{p}}if_parcel_fee=>ty_reason.
DATA lv_weight TYPE {{p}}if_parcel_fee=>ty_weight_g.
DATA lv_cents TYPE {{p}}if_parcel_fee=>ty_cents.
DATA(lv_char1) = 'A'.
DATA(lv_char10) = 'ABCDEFGHIJ'.
DATA(lo_zone) = cl_abap_typedescr=>describe_by_data( lv_zone ).
DATA(lo_reason) = cl_abap_typedescr=>describe_by_data( lv_reason ).
DATA(lo_weight) = cl_abap_typedescr=>describe_by_data( lv_weight ).
DATA(lo_cents) = cl_abap_typedescr=>describe_by_data( lv_cents ).
DATA(lo_char1) = cl_abap_typedescr=>describe_by_data( lv_char1 ).
DATA(lo_char10) = cl_abap_typedescr=>describe_by_data( lv_char10 ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
act = lo_zone->type_kind ).
cl_abap_unit_assert=>assert_equals( exp = lo_char1->length act = lo_zone->length ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
act = lo_reason->type_kind ).
cl_abap_unit_assert=>assert_equals( exp = lo_char10->length act = lo_reason->length ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
act = lo_weight->type_kind ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
act = lo_cents->type_kind ).
ENDMETHOD.
METHOD check_input_sig.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals(
exp = {{p}}if_parcel_fee=>c_reason-ok
act = lo_cut->check_input( iv_weight_g = 500 iv_zone = {{p}}if_parcel_fee=>c_zone-home ) ).
cl_abap_unit_assert=>assert_equals(
exp = {{p}}if_parcel_fee=>c_reason-weight
act = lo_cut->check_input( iv_weight_g = 0 iv_zone = {{p}}if_parcel_fee=>c_zone-home ) ).
cl_abap_unit_assert=>assert_equals(
exp = {{p}}if_parcel_fee=>c_reason-zone
act = lo_cut->check_input( iv_weight_g = 500 iv_zone = '9' ) ).
ENDMETHOD.
METHOD base_fee_sig.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 350
act = lo_cut->base_fee( iv_zone = {{p}}if_parcel_fee=>c_zone-home ) ).
cl_abap_unit_assert=>assert_equals( exp = 620
act = lo_cut->base_fee( iv_zone = {{p}}if_parcel_fee=>c_zone-national ) ).
cl_abap_unit_assert=>assert_equals( exp = 990
act = lo_cut->base_fee( iv_zone = {{p}}if_parcel_fee=>c_zone-europe ) ).
ENDMETHOD.
METHOD weight_surcharge_sig.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 0
act = lo_cut->weight_surcharge( iv_weight_g = 1000 ) ).
cl_abap_unit_assert=>assert_equals( exp = 90
act = lo_cut->weight_surcharge( iv_weight_g = 1001 ) ).
cl_abap_unit_assert=>assert_equals( exp = 180
act = lo_cut->weight_surcharge( iv_weight_g = 2001 ) ).
ENDMETHOD.
METHOD total_fee_sig.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 530
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_false ) ).
cl_abap_unit_assert=>assert_equals( exp = 1280
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_true ) ).
cl_abap_unit_assert=>assert_equals( exp = 0
act = lo_cut->total_fee( iv_weight_g = 0 iv_zone = '1' iv_express = abap_false ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,55 @@
{
"task": "G1900",
"mutants": [
{
"object": "{{P}}IF_PARCEL_FEE",
"mutant": "line 5: LENGTH 10 -> LENGTH 9 (length)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"TYPE_LENGTHS"
]
},
{
"object": "{{P}}IF_PARCEL_FEE",
"mutant": "line 9: VALUE '1' -> VALUE 'Z' (lit)",
"status": "killed",
"hidden": "7/9",
"failed_tests": [
"TOTAL_FEE_SIG",
"ZONE_VALUES"
]
},
{
"object": "{{P}}IF_PARCEL_FEE",
"mutant": "line 11: VALUE '3' -> VALUE 'Z' (lit)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"ZONE_VALUES"
]
},
{
"object": "{{P}}IF_PARCEL_FEE",
"mutant": "line 16: VALUE 'OK' -> VALUE 'ZK' (lit)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"REASON_VALUES"
]
},
{
"object": "{{P}}IF_PARCEL_FEE",
"mutant": "line 18: VALUE 'ZONE' -> VALUE 'YONE' (lit)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"REASON_VALUES"
]
}
],
"valid": 5,
"killed": 5,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,51 @@
INTERFACE {{p}}if_parcel_fee PUBLIC.
TYPES ty_weight_g TYPE i.
TYPES ty_cents TYPE i.
TYPES ty_zone TYPE c LENGTH 1.
TYPES ty_reason TYPE c LENGTH 10.
CONSTANTS:
BEGIN OF c_zone,
home TYPE ty_zone VALUE '1',
national TYPE ty_zone VALUE '2',
europe TYPE ty_zone VALUE '3',
END OF c_zone.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'OK',
weight TYPE ty_reason VALUE 'WEIGHT',
zone TYPE ty_reason VALUE 'ZONE',
END OF c_reason.
CONSTANTS:
BEGIN OF c_limit,
min_weight TYPE ty_weight_g VALUE 1,
max_weight TYPE ty_weight_g VALUE 31500,
END OF c_limit.
CONSTANTS:
BEGIN OF c_surcharge,
per_step TYPE ty_cents VALUE 90,
express TYPE ty_cents VALUE 750,
END OF c_surcharge.
METHODS check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason.
METHODS base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents.
METHODS total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents.
ENDINTERFACE.

View File

@@ -0,0 +1,180 @@
CLASS {{p}}parcel_fee_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PUBLIC SECTION.
INTERFACES {{p}}if_parcel_fee.
PRIVATE SECTION.
METHODS zone_values FOR TESTING.
METHODS reason_values FOR TESTING.
METHODS limit_values FOR TESTING.
METHODS surcharge_values FOR TESTING.
METHODS type_lengths FOR TESTING.
METHODS valid_input FOR TESTING.
METHODS weight_out_of_range FOR TESTING.
METHODS unknown_zone FOR TESTING.
METHODS base_fee_by_zone FOR TESTING.
METHODS surcharge_steps FOR TESTING.
METHODS total_fee_with_express FOR TESTING.
METHODS total_fee_invalid FOR TESTING.
ENDCLASS.
CLASS {{p}}parcel_fee_test IMPLEMENTATION.
METHOD {{p}}if_parcel_fee~check_input.
IF iv_weight_g < {{p}}if_parcel_fee=>c_limit-min_weight
OR iv_weight_g > {{p}}if_parcel_fee=>c_limit-max_weight.
rv_reason = {{p}}if_parcel_fee=>c_reason-weight.
ELSEIF iv_zone <> {{p}}if_parcel_fee=>c_zone-home
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-national
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-europe.
rv_reason = {{p}}if_parcel_fee=>c_reason-zone.
ELSE.
rv_reason = {{p}}if_parcel_fee=>c_reason-ok.
ENDIF.
ENDMETHOD.
METHOD {{p}}if_parcel_fee~base_fee.
CASE iv_zone.
WHEN {{p}}if_parcel_fee=>c_zone-home.
rv_cents = 350.
WHEN {{p}}if_parcel_fee=>c_zone-national.
rv_cents = 620.
WHEN {{p}}if_parcel_fee=>c_zone-europe.
rv_cents = 990.
WHEN OTHERS.
rv_cents = 0.
ENDCASE.
ENDMETHOD.
METHOD {{p}}if_parcel_fee~weight_surcharge.
IF iv_weight_g > 1000.
rv_cents = ( iv_weight_g - 1000 + 999 ) DIV 1000
* {{p}}if_parcel_fee=>c_surcharge-per_step.
ENDIF.
ENDMETHOD.
METHOD {{p}}if_parcel_fee~total_fee.
IF me->{{p}}if_parcel_fee~check_input( iv_weight_g = iv_weight_g
iv_zone = iv_zone )
<> {{p}}if_parcel_fee=>c_reason-ok.
RETURN.
ENDIF.
rv_cents = me->{{p}}if_parcel_fee~base_fee( iv_zone )
+ me->{{p}}if_parcel_fee~weight_surcharge( iv_weight_g ).
IF iv_express = abap_true.
rv_cents = rv_cents + {{p}}if_parcel_fee=>c_surcharge-express.
ENDIF.
ENDMETHOD.
METHOD zone_values.
cl_abap_unit_assert=>assert_equals( exp = '1' act = {{p}}if_parcel_fee=>c_zone-home ).
cl_abap_unit_assert=>assert_equals( exp = '2' act = {{p}}if_parcel_fee=>c_zone-national ).
cl_abap_unit_assert=>assert_equals( exp = '3' act = {{p}}if_parcel_fee=>c_zone-europe ).
ENDMETHOD.
METHOD reason_values.
cl_abap_unit_assert=>assert_equals( exp = 'OK' act = {{p}}if_parcel_fee=>c_reason-ok ).
cl_abap_unit_assert=>assert_equals( exp = 'WEIGHT' act = {{p}}if_parcel_fee=>c_reason-weight ).
cl_abap_unit_assert=>assert_equals( exp = 'ZONE' act = {{p}}if_parcel_fee=>c_reason-zone ).
ENDMETHOD.
METHOD limit_values.
cl_abap_unit_assert=>assert_equals( exp = 1 act = {{p}}if_parcel_fee=>c_limit-min_weight ).
cl_abap_unit_assert=>assert_equals( exp = 31500 act = {{p}}if_parcel_fee=>c_limit-max_weight ).
ENDMETHOD.
METHOD surcharge_values.
cl_abap_unit_assert=>assert_equals( exp = 90 act = {{p}}if_parcel_fee=>c_surcharge-per_step ).
cl_abap_unit_assert=>assert_equals( exp = 750 act = {{p}}if_parcel_fee=>c_surcharge-express ).
ENDMETHOD.
METHOD type_lengths.
DATA lv_zone TYPE {{p}}if_parcel_fee=>ty_zone.
DATA lv_reason TYPE {{p}}if_parcel_fee=>ty_reason.
DATA lv_weight TYPE {{p}}if_parcel_fee=>ty_weight_g.
DATA lv_cents TYPE {{p}}if_parcel_fee=>ty_cents.
DATA(lv_char1) = 'A'.
DATA(lv_char10) = 'ABCDEFGHIJ'.
DATA(lo_zone) = cl_abap_typedescr=>describe_by_data( lv_zone ).
DATA(lo_reason) = cl_abap_typedescr=>describe_by_data( lv_reason ).
DATA(lo_weight) = cl_abap_typedescr=>describe_by_data( lv_weight ).
DATA(lo_cents) = cl_abap_typedescr=>describe_by_data( lv_cents ).
DATA(lo_char1) = cl_abap_typedescr=>describe_by_data( lv_char1 ).
DATA(lo_char10) = cl_abap_typedescr=>describe_by_data( lv_char10 ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
act = lo_zone->type_kind ).
cl_abap_unit_assert=>assert_equals( exp = lo_char1->length act = lo_zone->length ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
act = lo_reason->type_kind ).
cl_abap_unit_assert=>assert_equals( exp = lo_char10->length act = lo_reason->length ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
act = lo_weight->type_kind ).
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
act = lo_cents->type_kind ).
ENDMETHOD.
METHOD valid_input.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-ok
act = lo_cut->check_input( iv_weight_g = 1 iv_zone = '1' ) ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-ok
act = lo_cut->check_input( iv_weight_g = 31500 iv_zone = '3' ) ).
ENDMETHOD.
METHOD weight_out_of_range.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-weight
act = lo_cut->check_input( iv_weight_g = 0 iv_zone = '1' ) ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-weight
act = lo_cut->check_input( iv_weight_g = 31501 iv_zone = '1' ) ).
ENDMETHOD.
METHOD unknown_zone.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-zone
act = lo_cut->check_input( iv_weight_g = 500 iv_zone = '9' ) ).
ENDMETHOD.
METHOD base_fee_by_zone.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 350 act = lo_cut->base_fee( iv_zone = '1' ) ).
cl_abap_unit_assert=>assert_equals( exp = 620 act = lo_cut->base_fee( iv_zone = '2' ) ).
cl_abap_unit_assert=>assert_equals( exp = 990 act = lo_cut->base_fee( iv_zone = '3' ) ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lo_cut->base_fee( iv_zone = '9' ) ).
ENDMETHOD.
METHOD surcharge_steps.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 0
act = lo_cut->weight_surcharge( iv_weight_g = 1000 ) ).
cl_abap_unit_assert=>assert_equals( exp = 90
act = lo_cut->weight_surcharge( iv_weight_g = 1001 ) ).
cl_abap_unit_assert=>assert_equals( exp = 90
act = lo_cut->weight_surcharge( iv_weight_g = 2000 ) ).
cl_abap_unit_assert=>assert_equals( exp = 180
act = lo_cut->weight_surcharge( iv_weight_g = 2001 ) ).
ENDMETHOD.
METHOD total_fee_with_express.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 530
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_false ) ).
cl_abap_unit_assert=>assert_equals( exp = 1280
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_true ) ).
ENDMETHOD.
METHOD total_fee_invalid.
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
lo_cut = me.
cl_abap_unit_assert=>assert_equals( exp = 0
act = lo_cut->total_fee( iv_weight_g = 0 iv_zone = '1' iv_express = abap_true ) ).
cl_abap_unit_assert=>assert_equals( exp = 0
act = lo_cut->total_fee( iv_weight_g = 500 iv_zone = '9' iv_express = abap_false ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,106 @@
# 1. Goal
A logistics program calculates the shipping fee of a parcel. The program must not
contain the fee rules itself. It uses an interface as the contract. Create this
interface.
# 2. Open questions
None.
# 3. Context
- Package $TMP, client 001.
- The interface {{P}}IF_PARCEL_FEE does not exist yet. Create it.
- A parcel has a weight in grams, a shipping zone, and an express flag. A fee is
an amount in whole cents.
- The interface is only a contract. Other programs implement it and calculate the
fee. The interface declares four methods:
- check_input: checks the weight and the zone and returns a reason code.
- base_fee: returns the base fee of a zone.
- weight_surcharge: returns the surcharge that the weight causes.
- total_fee: returns the complete fee of a parcel.
- The meaning of the values: zone '1' is the home zone, zone '2' is the national
zone, zone '3' is the europe zone. The reason 'OK' means that the input is
valid, 'WEIGHT' means that the weight is out of range, and 'ZONE' means that the
zone is unknown. The base fee is 350 cents for zone '1', 620 cents for zone '2',
and 990 cents for zone '3'. The first 1000 grams are included in the base fee.
Each started 1000 grams above 1000 grams costs 90 cents. The express option
costs 750 cents. A call with invalid input returns the fee 0.
# 4. Contract
Create the interface {{P}}IF_PARCEL_FEE in package $TMP. The interface is public.
It contains exactly the types, the constants, and the methods below. Do not add
other components.
Types:
- ty_weight_g TYPE i
- ty_cents TYPE i
- ty_zone TYPE c LENGTH 1
- ty_reason TYPE c LENGTH 10
Constants:
- c_zone: components home, national, europe, each TYPE ty_zone
- c_reason: components ok, weight, zone, each TYPE ty_reason
- c_limit: components min_weight, max_weight, each TYPE ty_weight_g
- c_surcharge: components per_step, express, each TYPE ty_cents
Methods:
- check_input
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
RETURNING VALUE(rv_reason) TYPE ty_reason
- base_fee
IMPORTING iv_zone TYPE ty_zone
RETURNING VALUE(rv_cents) TYPE ty_cents
- weight_surcharge
IMPORTING iv_weight_g TYPE ty_weight_g
RETURNING VALUE(rv_cents) TYPE ty_cents
- total_fee
IMPORTING iv_weight_g TYPE ty_weight_g
iv_zone TYPE ty_zone
iv_express TYPE abap_bool
RETURNING VALUE(rv_cents) TYPE ty_cents
# 5. Business rules
1. The type ty_zone is a character type with length 1. The type ty_reason is a
character type with length 10. The types ty_weight_g and ty_cents are integer
types.
2. The constant structure c_zone has the components home, national, and europe.
Each component has the type ty_zone. The values are '1', '2', and '3'.
3. The constant structure c_reason has the components ok, weight, and zone. Each
component has the type ty_reason. The values are 'OK', 'WEIGHT', and 'ZONE'.
4. The constant structure c_limit has the components min_weight and max_weight.
Each component has the type ty_weight_g. The values are 1 and 31500.
5. The constant structure c_surcharge has the components per_step and express.
Each component has the type ty_cents. The values are 90 and 750.
6. The method check_input has the importing parameter iv_weight_g of type
ty_weight_g, the importing parameter iv_zone of type ty_zone, and the returning
parameter rv_reason of type ty_reason. It returns c_reason-weight if the weight
is below c_limit-min_weight or above c_limit-max_weight. Else it returns
c_reason-zone if the zone is not one of the values of c_zone. Else it returns
c_reason-ok.
7. The method base_fee has the importing parameter iv_zone of type ty_zone and
the returning parameter rv_cents of type ty_cents. It returns 350 for zone '1',
620 for zone '2', 990 for zone '3', and 0 for every other zone.
8. The method weight_surcharge has the importing parameter iv_weight_g of type
ty_weight_g and the returning parameter rv_cents of type ty_cents. It returns 0
for a weight up to 1000 grams. For every started 1000 grams above 1000 grams it
returns c_surcharge-per_step cents.
9. The method total_fee has the importing parameters iv_weight_g of type
ty_weight_g, iv_zone of type ty_zone, and iv_express of type abap_bool, and the
returning parameter rv_cents of type ty_cents. It returns 0 if check_input does
not return c_reason-ok. Else it returns base_fee plus weight_surcharge, and it
adds c_surcharge-express if iv_express is abap_true.
# 6. Constraints
- Release target: SAP_BASIS 816 (SAP ABAP Platform 2025).
- Coding standards: Clean ABAP. No global variables. No comment that restates the
code.
- Out of scope: no database table, no CDS view, no function module, no report, and
no application class.
# 7. Acceptance
- The interface {{P}}IF_PARCEL_FEE is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the interface. An interface has no test
include, so create one global test class in package $TMP. The test class
implements the interface and checks the constants, the type lengths, and the
method signatures. Keep all test code in this one global class.

View File

@@ -0,0 +1,52 @@
{
"id": "G1900",
"category": "A",
"object_type": "INTF",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [],
"contract": [
{
"type": "INTF",
"name": "{{P}}IF_PARCEL_FEE"
}
],
"out_of_scope": [
"No database table",
"No CDS view",
"No function module",
"No report",
"No application class"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}PARCEL_FEE_HIDDEN",
"file": "hidden/parcel_fee_hidden.clas.abap",
"description": "Contract tests for {{P}}IF_PARCEL_FEE"
}
],
"reference": [
{
"type": "INTF",
"name": "{{P}}IF_PARCEL_FEE",
"file": "reference/if_parcel_fee.intf.abap",
"description": "Parcel fee contract"
},
{
"type": "CLAS",
"name": "{{P}}PARCEL_FEE_TEST",
"file": "reference/parcel_fee_test.clas.abap",
"description": "Own tests for the interface"
}
],
"craft_checks": [
"method_length",
"no_global_variables"
]
}

View File

@@ -0,0 +1,16 @@
@EndUserText.label : 'Bin stock'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}stock {
key client : abap.clnt not null;
key bin_id : abap.numc(8) not null;
key article_id : abap.char(12) not null;
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
quantity : abap.dec(13,3);
unit_of_measure : abap.unit(3);
shelf_no : abap.numc(4);
last_count_date : abap.dats;
blocked : abap.char(1);
}

View File

@@ -0,0 +1,16 @@
@EndUserText.label : 'Bin stock'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}stock {
key client : abap.clnt not null;
key bin_id : abap.char(8) not null;
key article_id : abap.char(12) not null;
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
quantity : abap.dec(13,4);
unit_of_measure : abap.unit(3);
shelf_no : abap.numc(4);
last_count_date : abap.dats;
blocked : abap.char(1);
}

View File

@@ -0,0 +1,16 @@
@EndUserText.label : 'Bin stock'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}stock {
key client : abap.clnt not null;
key bin_id : abap.char(8) not null;
key article_id : abap.char(12) not null;
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
quantity : abap.dec(13,3);
unit_of_measure : abap.unit(3);
shelf_no : abap.numc(3);
last_count_date : abap.dats;
blocked : abap.char(1);
}

View File

@@ -0,0 +1,16 @@
@EndUserText.label : 'Bin stock'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}stock {
key client : abap.clnt not null;
key bin_id : abap.char(8) not null;
key article_id : abap.char(12) not null;
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
quantity : abap.dec(13,3);
unit_of_measure : abap.unit(3);
shelf_no : abap.numc(4);
last_count_date : abap.tims;
blocked : abap.char(1);
}

View File

@@ -0,0 +1,19 @@
{
"id": "G1901",
"pool": "train",
"object_type": "TABL",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 4,
"killed": 4,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -0,0 +1,182 @@
CLASS {{p}}t20_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS fields_exist FOR TESTING.
METHODS key_fields FOR TESTING.
METHODS field_lengths FOR TESTING.
METHODS field_types FOR TESTING.
METHODS insert_and_read FOR TESTING.
METHODS decimal_roundtrip FOR TESTING.
METHODS duplicate_key FOR TESTING.
METHODS other_key_allowed FOR TESTING.
METHODS get_fields RETURNING VALUE(rt_fields) TYPE ddfields.
METHODS field_of IMPORTING it_fields TYPE ddfields
iv_name TYPE string
RETURNING VALUE(rs_field) TYPE dfies.
METHODS assert_has_field IMPORTING it_fields TYPE ddfields
iv_name TYPE string.
ENDCLASS.
CLASS {{p}}t20_hidden IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
ENDMETHOD.
METHOD get_fields.
DATA(lo_descr) = CAST cl_abap_structdescr( cl_abap_typedescr=>describe_by_name( '{{P}}STOCK' ) ).
rt_fields = lo_descr->get_ddic_field_list( ).
ENDMETHOD.
METHOD field_of.
LOOP AT it_fields INTO DATA(ls_field).
IF to_upper( ls_field-fieldname ) = to_upper( iv_name ).
rs_field = ls_field.
RETURN.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD assert_has_field.
cl_abap_unit_assert=>assert_true(
act = boolc( field_of( it_fields = it_fields iv_name = iv_name )-fieldname IS NOT INITIAL )
msg = |Field { iv_name } is missing| ).
ENDMETHOD.
METHOD fields_exist.
DATA(lt_fields) = get_fields( ).
cl_abap_unit_assert=>assert_equals( exp = 8 act = lines( lt_fields ) ).
assert_has_field( it_fields = lt_fields iv_name = 'CLIENT' ).
assert_has_field( it_fields = lt_fields iv_name = 'BIN_ID' ).
assert_has_field( it_fields = lt_fields iv_name = 'ARTICLE_ID' ).
assert_has_field( it_fields = lt_fields iv_name = 'QUANTITY' ).
assert_has_field( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' ).
assert_has_field( it_fields = lt_fields iv_name = 'SHELF_NO' ).
assert_has_field( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' ).
assert_has_field( it_fields = lt_fields iv_name = 'BLOCKED' ).
ENDMETHOD.
METHOD key_fields.
DATA(lt_fields) = get_fields( ).
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( it_fields = lt_fields iv_name = 'CLIENT' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( it_fields = lt_fields iv_name = 'BIN_ID' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( it_fields = lt_fields iv_name = 'ARTICLE_ID' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'SHELF_NO' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'BLOCKED' )-keyflag ).
ENDMETHOD.
METHOD field_lengths.
DATA(lt_fields) = get_fields( ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = field_of( it_fields = lt_fields iv_name = 'CLIENT' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 8 act = field_of( it_fields = lt_fields iv_name = 'BIN_ID' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 12 act = field_of( it_fields = lt_fields iv_name = 'ARTICLE_ID' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 13 act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = field_of( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 4 act = field_of( it_fields = lt_fields iv_name = 'SHELF_NO' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 8 act = field_of( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' )-leng ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = field_of( it_fields = lt_fields iv_name = 'BLOCKED' )-leng ).
ENDMETHOD.
METHOD field_types.
DATA(lt_fields) = get_fields( ).
cl_abap_unit_assert=>assert_equals( exp = 'CLNT' act = field_of( it_fields = lt_fields iv_name = 'CLIENT' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( it_fields = lt_fields iv_name = 'BIN_ID' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( it_fields = lt_fields iv_name = 'ARTICLE_ID' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'P' act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-inttype ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-decimals ).
cl_abap_unit_assert=>assert_equals( exp = 'UNIT' act = field_of( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'NUMC' act = field_of( it_fields = lt_fields iv_name = 'SHELF_NO' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = field_of( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( it_fields = lt_fields iv_name = 'BLOCKED' )-datatype ).
ENDMETHOD.
METHOD insert_and_read.
DATA ls_row TYPE {{p}}stock.
DATA ls_read TYPE {{p}}stock.
DATA lv_subrc TYPE sy-subrc.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0001'.
ls_row-article_id = 'ART-000001'.
ls_row-quantity = '12.500'.
ls_row-unit_of_measure = 'ST'.
ls_row-shelf_no = '0042'.
ls_row-last_count_date = '20250115'.
ls_row-blocked = 'X'.
INSERT {{p}}stock FROM @ls_row.
lv_subrc = sy-subrc.
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
SELECT SINGLE * FROM {{p}}stock
WHERE bin_id = 'BIN-0001' AND article_id = 'ART-000001'
INTO @ls_read.
cl_abap_unit_assert=>assert_equals( exp = ls_row-quantity act = ls_read-quantity ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-unit_of_measure act = ls_read-unit_of_measure ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-shelf_no act = ls_read-shelf_no ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-last_count_date act = ls_read-last_count_date ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-blocked act = ls_read-blocked ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-client act = ls_read-client ).
ENDMETHOD.
METHOD decimal_roundtrip.
DATA ls_row TYPE {{p}}stock.
DATA lv_quantity TYPE {{p}}stock-quantity.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0002'.
ls_row-article_id = 'ART-000002'.
ls_row-quantity = '0.125'.
ls_row-unit_of_measure = 'KG'.
INSERT {{p}}stock FROM @ls_row.
SELECT SINGLE quantity FROM {{p}}stock
WHERE bin_id = 'BIN-0002' AND article_id = 'ART-000002'
INTO @lv_quantity.
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '0.125' )
act = CONV decfloat34( lv_quantity ) ).
ENDMETHOD.
METHOD duplicate_key.
DATA ls_row TYPE {{p}}stock.
DATA lv_subrc TYPE sy-subrc.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0003'.
ls_row-article_id = 'ART-000003'.
INSERT {{p}}stock FROM @ls_row.
lv_subrc = sy-subrc.
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
INSERT {{p}}stock FROM @ls_row.
lv_subrc = sy-subrc.
cl_abap_unit_assert=>assert_equals( exp = 4 act = lv_subrc ).
ENDMETHOD.
METHOD other_key_allowed.
DATA ls_row TYPE {{p}}stock.
DATA lv_subrc TYPE sy-subrc.
DATA lv_count TYPE i.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0004'.
ls_row-article_id = 'ART-000004'.
INSERT {{p}}stock FROM @ls_row.
ls_row-article_id = 'ART-000005'.
INSERT {{p}}stock FROM @ls_row.
lv_subrc = sy-subrc.
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
SELECT COUNT(*) FROM {{p}}stock INTO @lv_count.
cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_count ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,55 @@
{
"task": "G1901",
"mutants": [
{
"object": "{{P}}STOCK",
"mutant": "line 8: key bin_id : -> bin_id : (key)",
"status": "invalid",
"hidden": "0/0",
"failed_tests": []
},
{
"object": "{{P}}STOCK",
"mutant": "line 8: abap.char(8) -> abap.numc(8) (type)",
"status": "killed",
"hidden": "5/8",
"failed_tests": [
"DECIMAL_ROUNDTRIP",
"FIELD_TYPES",
"INSERT_AND_READ"
]
},
{
"object": "{{P}}STOCK",
"mutant": "line 11: abap.dec(13,3) -> abap.dec(13,4) (decimals)",
"status": "killed",
"hidden": "7/8",
"failed_tests": [
"FIELD_TYPES"
]
},
{
"object": "{{P}}STOCK",
"mutant": "line 13: abap.numc(4) -> abap.numc(3) (length)",
"status": "killed",
"hidden": "7/8",
"failed_tests": [
"FIELD_LENGTHS"
]
},
{
"object": "{{P}}STOCK",
"mutant": "line 14: abap.dats -> abap.tims (type)",
"status": "killed",
"hidden": "6/8",
"failed_tests": [
"FIELD_LENGTHS",
"FIELD_TYPES"
]
}
],
"valid": 4,
"killed": 4,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,16 @@
@EndUserText.label : 'Bin stock'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}stock {
key client : abap.clnt not null;
key bin_id : abap.char(8) not null;
key article_id : abap.char(12) not null;
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
quantity : abap.dec(13,3);
unit_of_measure : abap.unit(3);
shelf_no : abap.numc(4);
last_count_date : abap.dats;
blocked : abap.char(1);
}

View File

@@ -0,0 +1,80 @@
CLASS {{p}}t20_test DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS stores_row FOR TESTING.
METHODS rejects_duplicate FOR TESTING.
METHODS keeps_three_decimals FOR TESTING.
ENDCLASS.
CLASS {{p}}t20_test IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
ENDMETHOD.
METHOD stores_row.
DATA ls_row TYPE {{p}}stock.
DATA ls_read TYPE {{p}}stock.
DATA lv_subrc TYPE sy-subrc.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0001'.
ls_row-article_id = 'ART-000001'.
ls_row-quantity = '12.500'.
ls_row-unit_of_measure = 'ST'.
ls_row-shelf_no = '0042'.
ls_row-last_count_date = '20250115'.
ls_row-blocked = 'X'.
INSERT {{p}}stock FROM @ls_row.
lv_subrc = sy-subrc.
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
SELECT SINGLE * FROM {{p}}stock
WHERE bin_id = 'BIN-0001' AND article_id = 'ART-000001'
INTO @ls_read.
cl_abap_unit_assert=>assert_equals( exp = ls_row-quantity act = ls_read-quantity ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-unit_of_measure act = ls_read-unit_of_measure ).
cl_abap_unit_assert=>assert_equals( exp = ls_row-blocked act = ls_read-blocked ).
ENDMETHOD.
METHOD rejects_duplicate.
DATA ls_row TYPE {{p}}stock.
DATA lv_subrc TYPE sy-subrc.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0002'.
ls_row-article_id = 'ART-000002'.
INSERT {{p}}stock FROM @ls_row.
INSERT {{p}}stock FROM @ls_row.
lv_subrc = sy-subrc.
cl_abap_unit_assert=>assert_equals( exp = 4 act = lv_subrc ).
ENDMETHOD.
METHOD keeps_three_decimals.
DATA ls_row TYPE {{p}}stock.
DATA lv_quantity TYPE {{p}}stock-quantity.
ls_row-client = sy-mandt.
ls_row-bin_id = 'BIN-0003'.
ls_row-article_id = 'ART-000003'.
ls_row-quantity = '0.125'.
ls_row-unit_of_measure = 'KG'.
INSERT {{p}}stock FROM @ls_row.
SELECT SINGLE quantity FROM {{p}}stock
WHERE bin_id = 'BIN-0003' AND article_id = 'ART-000003'
INTO @lv_quantity.
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '0.125' )
act = CONV decfloat34( lv_quantity ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,36 @@
# 1. Goal
The warehouse team counts the stock of each article in each storage bin. The team needs one table that stores the result of these counts. Other programs read the table, so the structure of the table is fixed.
# 2. Open questions
None.
# 3. Context
- System: SAP ABAP Platform 2025 (SAP_BASIS 816), client 001.
- Package: $TMP.
- No table for this data exists. Do not use tables of SAP application components.
# 4. Contract
- Create the transparent database table {{P}}STOCK in package $TMP.
- The table contains these fields: CLIENT, BIN_ID, ARTICLE_ID, QUANTITY, UNIT_OF_MEASURE, SHELF_NO, LAST_COUNT_DATE, BLOCKED.
- Key fields: CLIENT, BIN_ID, ARTICLE_ID.
# 5. Business rules
1. One row of the table is the stock of one article in one storage bin of one client. The key fields CLIENT, BIN_ID and ARTICLE_ID identify the row. A second row with the same key values is not allowed.
2. CLIENT: data type CLNT, length 3. Key field. Not null.
3. BIN_ID: data type CHAR, length 8. Key field. Not null. The number of the storage bin.
4. ARTICLE_ID: data type CHAR, length 12. Key field. Not null. The number of the article.
5. QUANTITY: data type DEC, length 13, 3 decimals. The stock quantity. It is a quantity in the unit of measure that UNIT_OF_MEASURE contains.
6. UNIT_OF_MEASURE: data type UNIT, length 3. The unit of measure of QUANTITY.
7. SHELF_NO: data type NUMC, length 4. The shelf number in the storage bin.
8. LAST_COUNT_DATE: data type DATS. The date of the last count.
9. BLOCKED: data type CHAR, length 1. The value 'X' means that the storage bin is blocked for withdrawals. The value is initial if the storage bin is not blocked.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
- Coding standards: Clean ABAP. Use ABAP SQL and ABAP Unit.
- Out of scope: do not create a CDS view, a report or a function module. Do not change other objects.
# 7. Acceptance
- The table {{P}}STOCK is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests with CL_OSQL_TEST_ENVIRONMENT in a global test class.

View File

@@ -0,0 +1,53 @@
{
"id": "G1901",
"category": "B",
"object_type": "TABL",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [],
"contract": [
{
"type": "TABL",
"name": "{{P}}STOCK",
"fields": [
"client",
"bin_id",
"article_id",
"quantity",
"unit_of_measure",
"shelf_no",
"last_count_date",
"blocked"
]
}
],
"out_of_scope": [],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T20_HIDDEN",
"file": "hidden/t20_hidden.clas.abap",
"description": "T20 hidden tests"
}
],
"reference": [
{
"type": "TABL",
"name": "{{P}}STOCK",
"file": "reference/stock.tabl.asabap",
"description": "Bin stock table"
},
{
"type": "CLAS",
"name": "{{P}}T20_TEST",
"file": "reference/t20_test.clas.abap",
"description": "T20 own tests"
}
],
"craft_checks": []
}

View File

@@ -0,0 +1,24 @@
{
"id": "G1902",
"pool": "train",
"object_type": "MSAG",
"category": "D",
"attempts": [
{
"stage": "validate",
"oracle": 85.0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 5,
"killed": 5,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -0,0 +1,97 @@
CLASS {{p}}t41_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS msg_001_text FOR TESTING.
METHODS msg_002_text FOR TESTING.
METHODS msg_003_text FOR TESTING.
METHODS msg_004_text FOR TESTING.
METHODS msg_005_text FOR TESTING.
METHODS placeholder_substitution FOR TESTING.
METHODS exception_text_two_args FOR TESTING.
METHODS exception_text_one_arg FOR TESTING.
METHODS exception_is_catchable FOR TESTING.
METHODS exception_is_static_check FOR TESTING.
ENDCLASS.
CLASS {{p}}t41_hidden IMPLEMENTATION.
METHOD msg_001_text.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP101' INTO lv_text.
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP101 is fully booked' act = lv_text ).
ENDMETHOD.
METHOD msg_002_text.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '002' WITH 'P-100' 'ABAP101' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Participant P-100 is already booked for course ABAP101'
act = lv_text ).
ENDMETHOD.
METHOD msg_003_text.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '003' WITH 'ABAP101' '3' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Course ABAP101 has only 3 free seats left'
act = lv_text ).
ENDMETHOD.
METHOD msg_004_text.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '004' WITH 'ABAP101' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Booking for course ABAP101 is confirmed'
act = lv_text ).
ENDMETHOD.
METHOD msg_005_text.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '005' WITH 'ABAP101' '20250301' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Course ABAP101 was cancelled on 20250301'
act = lv_text ).
ENDMETHOD.
METHOD placeholder_substitution.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP202' INTO lv_text.
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP202 is fully booked' act = lv_text ).
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP303' INTO lv_text.
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP303 is fully booked' act = lv_text ).
ENDMETHOD.
METHOD exception_text_two_args.
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '002' iv_arg1 = 'P-100' iv_arg2 = 'ABAP101' ).
cl_abap_unit_assert=>assert_equals(
exp = 'Participant P-100 is already booked for course ABAP101'
act = lo_cx->get_text( ) ).
ENDMETHOD.
METHOD exception_text_one_arg.
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '004' iv_arg1 = 'ABAP101' ).
cl_abap_unit_assert=>assert_equals(
exp = 'Booking for course ABAP101 is confirmed'
act = lo_cx->get_text( ) ).
ENDMETHOD.
METHOD exception_is_catchable.
DATA lv_caught TYPE abap_bool.
TRY.
RAISE EXCEPTION TYPE {{p}}cx_booking
EXPORTING iv_msgno = '003' iv_arg1 = 'ABAP101' iv_arg2 = '1'.
CATCH {{p}}cx_booking INTO DATA(lo_cx).
lv_caught = abap_true.
cl_abap_unit_assert=>assert_equals(
exp = 'Course ABAP101 has only 1 free seats left'
act = lo_cx->get_text( ) ).
ENDTRY.
cl_abap_unit_assert=>assert_true( lv_caught ).
ENDMETHOD.
METHOD exception_is_static_check.
DATA lo_cx TYPE REF TO cx_static_check.
lo_cx = NEW {{p}}cx_booking( iv_msgno = '001' iv_arg1 = 'ABAP101' ).
cl_abap_unit_assert=>assert_bound( lo_cx ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,58 @@
{
"task": "G1902",
"mutants": [
{
"object": "{{P}}MSG",
"mutant": "message 005: &1 -> &2 (placeholder)",
"status": "killed",
"hidden": "9/10",
"failed_tests": [
"MSG_005_TEXT"
]
},
{
"object": "{{P}}MSG",
"mutant": "message 003: &1 -> &2 (placeholder)",
"status": "killed",
"hidden": "8/10",
"failed_tests": [
"EXCEPTION_IS_CATCHABLE",
"MSG_003_TEXT"
]
},
{
"object": "{{P}}MSG",
"mutant": "message 004: &1 -> &2 (placeholder)",
"status": "killed",
"hidden": "8/10",
"failed_tests": [
"EXCEPTION_TEXT_ONE_ARG",
"MSG_004_TEXT"
]
},
{
"object": "{{P}}MSG",
"mutant": "message 002: text + ' x' (text)",
"status": "killed",
"hidden": "8/10",
"failed_tests": [
"EXCEPTION_TEXT_TWO_ARGS",
"MSG_002_TEXT"
]
},
{
"object": "{{P}}MSG",
"mutant": "message 001: text + ' x' (text)",
"status": "killed",
"hidden": "8/10",
"failed_tests": [
"MSG_001_TEXT",
"PLACEHOLDER_SUBSTITUTION"
]
}
],
"valid": 5,
"killed": 5,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,26 @@
CLASS {{p}}cx_booking DEFINITION PUBLIC INHERITING FROM cx_static_check FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING iv_msgno TYPE symsgno
iv_arg1 TYPE string DEFAULT ''
iv_arg2 TYPE string DEFAULT ''.
METHODS if_message~get_text REDEFINITION.
PRIVATE SECTION.
DATA mv_msgno TYPE symsgno.
DATA mv_arg1 TYPE string.
DATA mv_arg2 TYPE string.
ENDCLASS.
CLASS {{p}}cx_booking IMPLEMENTATION.
METHOD constructor.
super->constructor( ).
mv_msgno = iv_msgno.
mv_arg1 = iv_arg1.
mv_arg2 = iv_arg2.
ENDMETHOD.
METHOD if_message~get_text.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER mv_msgno WITH mv_arg1 mv_arg2 INTO result.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,73 @@
CLASS {{p}}t41_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS msg_001 FOR TESTING.
METHODS msg_002 FOR TESTING.
METHODS msg_003 FOR TESTING.
METHODS msg_004 FOR TESTING.
METHODS msg_005 FOR TESTING.
METHODS exception_two_args FOR TESTING.
METHODS exception_one_arg FOR TESTING.
METHODS exception_is_static_check FOR TESTING.
ENDCLASS.
CLASS {{p}}t41_test IMPLEMENTATION.
METHOD msg_001.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP101' INTO lv_text.
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP101 is fully booked' act = lv_text ).
ENDMETHOD.
METHOD msg_002.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '002' WITH 'P-100' 'ABAP101' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Participant P-100 is already booked for course ABAP101'
act = lv_text ).
ENDMETHOD.
METHOD msg_003.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '003' WITH 'ABAP101' '3' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Course ABAP101 has only 3 free seats left'
act = lv_text ).
ENDMETHOD.
METHOD msg_004.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '004' WITH 'ABAP101' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Booking for course ABAP101 is confirmed'
act = lv_text ).
ENDMETHOD.
METHOD msg_005.
DATA lv_text TYPE string.
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '005' WITH 'ABAP101' '20250301' INTO lv_text.
cl_abap_unit_assert=>assert_equals(
exp = 'Course ABAP101 was cancelled on 20250301'
act = lv_text ).
ENDMETHOD.
METHOD exception_two_args.
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '002' iv_arg1 = 'P-100' iv_arg2 = 'ABAP101' ).
cl_abap_unit_assert=>assert_equals(
exp = 'Participant P-100 is already booked for course ABAP101'
act = lo_cx->get_text( ) ).
ENDMETHOD.
METHOD exception_one_arg.
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '004' iv_arg1 = 'ABAP101' ).
cl_abap_unit_assert=>assert_equals(
exp = 'Booking for course ABAP101 is confirmed'
act = lo_cx->get_text( ) ).
ENDMETHOD.
METHOD exception_is_static_check.
DATA lo_cx TYPE REF TO cx_static_check.
lo_cx = NEW {{p}}cx_booking( iv_msgno = '001' iv_arg1 = 'ABAP101' ).
cl_abap_unit_assert=>assert_bound( lo_cx ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,19 @@
CLASS {{p}}cx_legacy DEFINITION PUBLIC INHERITING FROM cx_static_check FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor IMPORTING iv_text TYPE string.
METHODS if_message~get_text REDEFINITION.
PRIVATE SECTION.
DATA mv_text TYPE string.
ENDCLASS.
CLASS {{p}}cx_legacy IMPLEMENTATION.
METHOD constructor.
super->constructor( ).
mv_text = iv_text.
ENDMETHOD.
METHOD if_message~get_text.
result = mv_text.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,69 @@
# 1. Goal
Create the message class and the class-based exception for the training course
booking service. The booking service reports every problem with a message from
the message class. The exception class carries the text of such a message.
# 2. Open questions
None.
# 3. Context
- All objects are in package $TMP.
- The booking service books participants into training courses.
- The old exception class {{P}}CX_LEGACY exists. It takes a hard-coded text and
does not use a message class. The new exception class replaces it.
- All texts of the booking service come from one message class.
# 4. Contract
- Create the message class {{P}}MSG with the messages of section 5.
- Create the exception class {{P}}CX_BOOKING:
- It is a class-based exception and inherits from CX_STATIC_CHECK.
- The constructor has these parameters:
- iv_msgno TYPE symsgno
- iv_arg1 TYPE string, default ''
- iv_arg2 TYPE string, default ''
- The method GET_TEXT of the interface IF_MESSAGE returns the text of the
message with the number iv_msgno from {{P}}MSG. The placeholder &1 is
replaced by iv_arg1 and the placeholder &2 is replaced by iv_arg2.
# 5. Business rules
Message class {{P}}MSG:
1. Message 001, error message, text "Course &1 is fully booked". The booking
service uses it when a course has no free seat.
2. Message 002, error message, text "Participant &1 is already booked for
course &2". The booking service uses it for a double booking.
3. Message 003, warning message, text "Course &1 has only &2 free seats left".
The booking service uses it when a course has few free seats left.
4. Message 004, information message, text "Booking for course &1 is
confirmed". The booking service uses it for a successful booking.
5. Message 005, warning message, text "Course &1 was cancelled on &2". The
booking service uses it for a cancelled course.
Exception class {{P}}CX_BOOKING:
6. The constructor stores the message number and the two placeholders.
7. GET_TEXT returns the text of the message with the number iv_msgno from
{{P}}MSG. The placeholder &1 is replaced by iv_arg1 and the placeholder &2 is
replaced by iv_arg2.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
- Coding standards: Clean ABAP. Keep every method below 40 statements. Do not
produce ATC findings of priority 1 or 2.
- Out of scope: database tables, CDS views, reports, dynpro, and the old
exception class {{P}}CX_LEGACY.
# 7. Acceptance
- The message class {{P}}MSG is active. It contains the messages 001 to 005 with
the exact texts of section 5.
- The exception class {{P}}CX_BOOKING is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests for the messages and the exception class in a global
test class.

View File

@@ -0,0 +1,103 @@
{
"id": "G1902",
"category": "D",
"object_type": "MSAG",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [
{
"type": "CLAS",
"name": "{{P}}CX_LEGACY",
"file": "seed/cx_legacy.clas.abap",
"description": "Old exception class with a hard-coded text"
}
],
"contract": [
{
"type": "MSAG",
"name": "{{P}}MSG",
"messages": [
{
"msgno": "001"
},
{
"msgno": "002"
},
{
"msgno": "003"
},
{
"msgno": "004"
},
{
"msgno": "005"
}
]
},
{
"type": "CLAS",
"name": "{{P}}CX_BOOKING",
"description": "Class-based exception (CX_STATIC_CHECK). Constructor IMPORTING iv_msgno TYPE symsgno, iv_arg1 TYPE string DEFAULT '', iv_arg2 TYPE string DEFAULT ''. GET_TEXT of IF_MESSAGE returns the text of message iv_msgno from {{P}}MSG with &1 = iv_arg1 and &2 = iv_arg2."
}
],
"out_of_scope": [
"{{P}}CX_LEGACY"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T41_HIDDEN",
"file": "hidden/t41_hidden.clas.abap",
"description": "T41 hidden tests"
}
],
"reference": [
{
"type": "MSAG",
"name": "{{P}}MSG",
"description": "Booking messages",
"messages": [
{
"msgno": "001",
"text": "Course &1 is fully booked"
},
{
"msgno": "002",
"text": "Participant &1 is already booked for course &2"
},
{
"msgno": "003",
"text": "Course &1 has only &2 free seats left"
},
{
"msgno": "004",
"text": "Booking for course &1 is confirmed"
},
{
"msgno": "005",
"text": "Course &1 was cancelled on &2"
}
]
},
{
"type": "CLAS",
"name": "{{P}}CX_BOOKING",
"file": "reference/cx_booking.clas.abap",
"description": "Booking exception class"
},
{
"type": "CLAS",
"name": "{{P}}T41_TEST",
"file": "reference/t41_test.clas.abap",
"description": "T41 own tests"
}
],
"craft_checks": [
"method_length"
]
}

View File

@@ -0,0 +1,6 @@
@EndUserText.label : 'Audit information'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define structure {{p}}audit_info {
created_by : abap.char(11);
created_on : abap.dats;
}

View File

@@ -0,0 +1,6 @@
@EndUserText.label : 'Audit information'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define structure {{p}}audit_info {
created_by : abap.numc(12);
created_on : abap.dats;
}

View File

@@ -0,0 +1,10 @@
@EndUserText.label : 'Stock position line'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define structure {{p}}stock_line {
material : abap.char(18);
plant : abap.char(4);
net_amount : abap.dec(9,3);
currency : abap.char(5);
created_by : abap.char(12);
created_on : abap.dats;
}

View File

@@ -0,0 +1,6 @@
@EndUserText.label : 'Audit information'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define structure {{p}}audit_info {
created_by : abap.char(12);
created_on : abap.tims;
}

View File

@@ -0,0 +1,29 @@
{
"id": "G1903",
"pool": "train",
"object_type": "STRU",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 4,
"killed": 4,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -0,0 +1,177 @@
CLASS {{p}}t16_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS six_components FOR TESTING.
METHODS component_order FOR TESTING.
METHODS char_lengths FOR TESTING.
METHODS amount_type FOR TESTING.
METHODS date_type FOR TESTING.
METHODS flat_structure FOR TESTING.
METHODS audit_structure FOR TESTING.
METHODS audit_matches FOR TESTING.
METHODS read_stock_row FOR TESTING.
ENDCLASS.
CLASS {{p}}t16_hidden IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
DATA lt_stock TYPE STANDARD TABLE OF {{p}}stock WITH EMPTY KEY.
lt_stock = VALUE #(
( material = 'MAT-1' plant = 'P001' net_amount = '123.45'
currency = 'EUR' created_by = 'USER1' created_on = '20240115' )
( material = 'MAT-2' plant = 'P001' net_amount = '10.00'
currency = 'EUR' created_by = 'USER2' created_on = '20240116' )
( material = 'MAT-3' plant = 'P002' net_amount = '5.50'
currency = 'USD' created_by = 'USER1' created_on = '20240117' ) ).
go_env->insert_test_data( lt_stock ).
ENDMETHOD.
METHOD six_components.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
cl_abap_unit_assert=>assert_equals( exp = 6 act = lines( lt_fields ) ).
ENDMETHOD.
METHOD component_order.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_comp) = lo_struct->get_components( ).
cl_abap_unit_assert=>assert_equals( exp = 6 act = lines( lt_comp ) ).
DATA(lv_index) = 0.
LOOP AT lt_comp INTO DATA(ls_comp).
lv_index = lv_index + 1.
CASE lv_index.
WHEN 1.
cl_abap_unit_assert=>assert_equals( exp = 'MATERIAL' act = ls_comp-name ).
WHEN 2.
cl_abap_unit_assert=>assert_equals( exp = 'PLANT' act = ls_comp-name ).
WHEN 3.
cl_abap_unit_assert=>assert_equals( exp = 'NET_AMOUNT' act = ls_comp-name ).
WHEN 4.
cl_abap_unit_assert=>assert_equals( exp = 'CURRENCY' act = ls_comp-name ).
WHEN 5.
cl_abap_unit_assert=>assert_equals( exp = 'CREATED_BY' act = ls_comp-name ).
WHEN 6.
cl_abap_unit_assert=>assert_equals( exp = 'CREATED_ON' act = ls_comp-name ).
ENDCASE.
ENDLOOP.
ENDMETHOD.
METHOD char_lengths.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'MATERIAL'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component MATERIAL is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 18 act = CONV i( ls_field-leng ) ).
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'PLANT'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component PLANT is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 4 act = CONV i( ls_field-leng ) ).
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'CURRENCY'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CURRENCY is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 5 act = CONV i( ls_field-leng ) ).
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'CREATED_BY'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_BY is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 12 act = CONV i( ls_field-leng ) ).
ENDMETHOD.
METHOD amount_type.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'NET_AMOUNT'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component NET_AMOUNT is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 'DEC' act = ls_field-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 9 act = CONV i( ls_field-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = CONV i( ls_field-decimals ) ).
ENDMETHOD.
METHOD date_type.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'CREATED_ON'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_ON is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = ls_field-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 8 act = CONV i( ls_field-leng ) ).
ENDMETHOD.
METHOD flat_structure.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_comp) = lo_struct->get_components( ).
LOOP AT lt_comp INTO DATA(ls_comp).
cl_abap_unit_assert=>assert_equals(
exp = cl_abap_typedescr=>kind_elem
act = ls_comp-type->kind
msg = |Component { ls_comp-name } is not elementary| ).
ENDLOOP.
ENDMETHOD.
METHOD audit_structure.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}AUDIT_INFO' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_fields ) ).
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'CREATED_BY'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_BY is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = ls_field-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 12 act = CONV i( ls_field-leng ) ).
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'CREATED_ON'.
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_ON is missing' ).
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = ls_field-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 8 act = CONV i( ls_field-leng ) ).
ENDMETHOD.
METHOD audit_matches.
DATA(lo_line) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lo_audit) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}AUDIT_INFO' ) ).
DATA(lt_line) = lo_line->get_ddic_field_list( ).
DATA(lt_audit) = lo_audit->get_ddic_field_list( ).
LOOP AT lt_audit INTO DATA(ls_audit).
READ TABLE lt_line INTO DATA(ls_line) WITH KEY fieldname = ls_audit-fieldname.
cl_abap_unit_assert=>assert_subrc(
exp = 0 msg = |Component { ls_audit-fieldname } is missing| ).
cl_abap_unit_assert=>assert_equals( exp = ls_audit-leng act = ls_line-leng ).
cl_abap_unit_assert=>assert_equals( exp = ls_audit-datatype act = ls_line-datatype ).
ENDLOOP.
ENDMETHOD.
METHOD read_stock_row.
DATA ls_line TYPE {{p}}stock_line.
SELECT SINGLE material, plant, net_amount, currency, created_by, created_on
FROM {{p}}stock
INTO @ls_line
WHERE material = 'MAT-1' AND plant = 'P001'.
cl_abap_unit_assert=>assert_equals( exp = 'MAT-1' act = ls_line-material ).
cl_abap_unit_assert=>assert_equals( exp = 'P001' act = ls_line-plant ).
cl_abap_unit_assert=>assert_equals( exp = 'EUR' act = ls_line-currency ).
cl_abap_unit_assert=>assert_equals( exp = 'USER1' act = ls_line-created_by ).
cl_abap_unit_assert=>assert_equals(
exp = CONV d( '20240115' ) act = ls_line-created_on ).
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '123.45' )
act = CONV decfloat34( ls_line-net_amount ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,55 @@
{
"task": "G1903",
"mutants": [
{
"object": "{{P}}AUDIT_INFO",
"mutant": "line 4: abap.char(12) -> abap.char(11) (length)",
"status": "killed",
"hidden": "7/9",
"failed_tests": [
"AUDIT_MATCHES",
"AUDIT_STRUCTURE"
]
},
{
"object": "{{P}}STOCK_LINE",
"mutant": "line 4: abap.char(18) -> abap.numc(18) (type)",
"status": "invalid",
"hidden": "0/0",
"failed_tests": []
},
{
"object": "{{P}}AUDIT_INFO",
"mutant": "line 4: abap.char(12) -> abap.numc(12) (type)",
"status": "killed",
"hidden": "7/9",
"failed_tests": [
"AUDIT_MATCHES",
"AUDIT_STRUCTURE"
]
},
{
"object": "{{P}}STOCK_LINE",
"mutant": "line 6: abap.dec(9,2) -> abap.dec(9,3) (decimals)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"AMOUNT_TYPE"
]
},
{
"object": "{{P}}AUDIT_INFO",
"mutant": "line 5: abap.dats -> abap.tims (type)",
"status": "killed",
"hidden": "7/9",
"failed_tests": [
"AUDIT_MATCHES",
"AUDIT_STRUCTURE"
]
}
],
"valid": 4,
"killed": 4,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,6 @@
@EndUserText.label : 'Audit information'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define structure {{p}}audit_info {
created_by : abap.char(12);
created_on : abap.dats;
}

View File

@@ -0,0 +1,10 @@
@EndUserText.label : 'Stock position line'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define structure {{p}}stock_line {
material : abap.char(18);
plant : abap.char(4);
net_amount : abap.dec(9,2);
currency : abap.char(5);
created_by : abap.char(12);
created_on : abap.dats;
}

View File

@@ -0,0 +1,68 @@
CLASS {{p}}t16_test DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS six_components FOR TESTING.
METHODS reads_stock_line FOR TESTING.
METHODS audit_matches FOR TESTING.
ENDCLASS.
CLASS {{p}}t16_test IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
DATA lt_stock TYPE STANDARD TABLE OF {{p}}stock WITH EMPTY KEY.
lt_stock = VALUE #(
( material = 'MAT-1' plant = 'P001' net_amount = '123.45'
currency = 'EUR' created_by = 'USER1' created_on = '20240115' ) ).
go_env->insert_test_data( lt_stock ).
ENDMETHOD.
METHOD six_components.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
cl_abap_unit_assert=>assert_equals( exp = 6 act = lines( lt_fields ) ).
ENDMETHOD.
METHOD reads_stock_line.
DATA ls_line TYPE {{p}}stock_line.
SELECT SINGLE material, plant, net_amount, currency, created_by, created_on
FROM {{p}}stock
INTO @ls_line
WHERE material = 'MAT-1'.
cl_abap_unit_assert=>assert_equals( exp = 'MAT-1' act = ls_line-material ).
cl_abap_unit_assert=>assert_equals( exp = 'EUR' act = ls_line-currency ).
ENDMETHOD.
METHOD audit_matches.
DATA(lo_line) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
DATA(lo_audit) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}AUDIT_INFO' ) ).
DATA(lt_line) = lo_line->get_ddic_field_list( ).
DATA(lt_audit) = lo_audit->get_ddic_field_list( ).
LOOP AT lt_audit INTO DATA(ls_audit).
READ TABLE lt_line INTO DATA(ls_line) WITH KEY fieldname = ls_audit-fieldname.
cl_abap_unit_assert=>assert_subrc(
exp = 0 msg = |Component { ls_audit-fieldname } is missing| ).
cl_abap_unit_assert=>assert_equals( exp = ls_audit-leng act = ls_line-leng ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,14 @@
@EndUserText.label : 'Stock position'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}stock {
key client : abap.clnt not null;
key material : abap.char(18) not null;
key plant : abap.char(4) not null;
net_amount : abap.dec(9,2);
currency : abap.char(5);
created_by : abap.char(12);
created_on : abap.dats;
}

View File

@@ -0,0 +1,59 @@
# 1. Goal
A stock report and an OData service must use the same line type for warehouse
stock positions. Create the DDIC structure {{P}}STOCK_LINE and the DDIC
structure {{P}}AUDIT_INFO in package $TMP.
# 2. Open questions
None.
# 3. Context
- The table {{P}}STOCK exists in package $TMP. Its columns are, in this order:
MATERIAL (CHAR 18), PLANT (CHAR 4), NET_AMOUNT (DEC 9,2), CURRENCY (CHAR 5),
CREATED_BY (CHAR 12), CREATED_ON (DATS). The key is CLIENT, MATERIAL and
PLANT.
- The table {{P}}STOCK is empty. Test data comes from test doubles.
- {{P}}AUDIT_INFO is the audit part that other objects use, for example a
change log view and a service. {{P}}STOCK_LINE must carry the same audit
part.
- Both structures are used as line types in ABAP SQL and in an OData service.
# 4. Contract
- Create the DDIC structure {{P}}STOCK_LINE in package $TMP.
Components, in this order: material (CHAR 18), plant (CHAR 4),
net_amount (DEC 9,2), currency (CHAR 5), created_by (CHAR 12),
created_on (DATS).
- Create the DDIC structure {{P}}AUDIT_INFO in package $TMP.
Components, in this order: created_by (CHAR 12), created_on (DATS).
- {{P}}STOCK_LINE contains all six components itself. It does not use a DDIC
include.
# 5. Business rules
1. {{P}}STOCK_LINE has exactly six components.
2. The components of {{P}}STOCK_LINE are, in this order: material, plant,
net_amount, currency, created_by, created_on.
3. material is a character field of 18 characters.
4. plant is a character field of 4 characters.
5. net_amount is a packed number with 9 digits and 2 decimal places.
6. currency is a character field of 5 characters.
7. created_by is a character field of 12 characters.
8. created_on is a date field of type DATS.
9. {{P}}AUDIT_INFO has exactly two components, in this order: created_by
(CHAR 12) and created_on (DATS).
10. Every component of {{P}}STOCK_LINE is elementary. {{P}}STOCK_LINE is a
flat structure.
11. The components created_by and created_on of {{P}}STOCK_LINE have the same
data type and the same length as the components of {{P}}AUDIT_INFO.
12. A row of {{P}}STOCK can be read into a work area of type {{P}}STOCK_LINE
with ABAP SQL.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
- Package $TMP. Do not use transports.
- Coding standards: Clean ABAP. No ATC priority 1 or 2 findings.
- Out of scope: do not change the table {{P}}STOCK.
# 7. Acceptance
- Both structures are active and have no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests with CL_OSQL_TEST_ENVIRONMENT in a global test class
(for example {{P}}T16_TEST). The tests must prove rules 1 to 12.

View File

@@ -0,0 +1,74 @@
{
"id": "G1903",
"category": "B",
"object_type": "STRU",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [
{
"type": "TABL",
"name": "{{P}}STOCK",
"file": "seed/stock.tabl.asabap",
"description": "Stock positions"
}
],
"contract": [
{
"type": "STRU",
"name": "{{P}}STOCK_LINE",
"fields": [
"material",
"plant",
"net_amount",
"currency",
"created_by",
"created_on"
]
},
{
"type": "STRU",
"name": "{{P}}AUDIT_INFO",
"fields": [
"created_by",
"created_on"
]
}
],
"out_of_scope": [
"{{P}}STOCK"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T16_HIDDEN",
"file": "hidden/t16_hidden.clas.abap",
"description": "T16 hidden tests"
}
],
"reference": [
{
"type": "STRU",
"name": "{{P}}AUDIT_INFO",
"file": "reference/audit_info.ddls.asddls",
"description": "Audit information structure"
},
{
"type": "STRU",
"name": "{{P}}STOCK_LINE",
"file": "reference/stock_line.ddls.asddls",
"description": "Stock line structure"
},
{
"type": "CLAS",
"name": "{{P}}T16_TEST",
"file": "reference/t16_test.clas.abap",
"description": "T16 own tests"
}
],
"craft_checks": []
}

View File

@@ -0,0 +1,45 @@
{
"id": "G1904",
"pool": "train",
"object_type": "CLAS",
"category": "D",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 0,
"killed": 0,
"ok": false
}
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 0,
"killed": 0,
"ok": false
}
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 0,
"killed": 0,
"ok": false
}
}
],
"accepted": true,
"note": "mutation check rerun after exception-class mutants were added (3 of 3 killed)"
}

View File

@@ -0,0 +1,259 @@
CLASS {{p}}d02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS unknown_material_text FOR TESTING.
METHODS zero_quantity_text FOR TESTING.
METHODS over_limit_text FOR TESTING.
METHODS default_text_cases FOR TESTING.
METHODS context_of_each_case FOR TESTING.
METHODS unknown_case_keeps_data FOR TESTING.
METHODS text_via_cx_root FOR TESTING.
METHODS case_constant_values FOR TESTING.
METHODS is_static_check_exception FOR TESTING.
METHODS instances_keep_own_data FOR TESTING.
METHODS same_data_different_case FOR TESTING.
METHODS text_exact_length FOR TESTING.
METHODS caught_error
IMPORTING iv_case TYPE {{p}}if_issue_error_info=>ty_case
iv_material TYPE string
iv_quantity TYPE i
iv_limit TYPE i
RETURNING VALUE(ro_error) TYPE REF TO {{p}}cx_issue_error.
ENDCLASS.
CLASS {{p}}d02_hidden IMPLEMENTATION.
METHOD caught_error.
TRY.
RAISE EXCEPTION TYPE {{p}}cx_issue_error
EXPORTING
iv_case = iv_case
iv_material = iv_material
iv_quantity = iv_quantity
iv_limit = iv_limit.
CATCH {{p}}cx_issue_error INTO ro_error.
ENDTRY.
ENDMETHOD.
METHOD unknown_material_text.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = 'MAT-1'
iv_quantity = 5
iv_limit = 100 ).
cl_abap_unit_assert=>assert_equals( exp = `Material MAT-1 is not known`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD zero_quantity_text.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
iv_material = 'MAT-2'
iv_quantity = 0
iv_limit = 50 ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-2`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD over_limit_text.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'MAT-3'
iv_quantity = 120
iv_limit = 100 ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity 120 exceeds limit 100 for material MAT-3`
act = lo_error->get_text( ) ).
DATA(lo_big) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'M-BIG'
iv_quantity = 12345
iv_limit = 67890 ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity 12345 exceeds limit 67890 for material M-BIG`
act = lo_big->get_text( ) ).
ENDMETHOD.
METHOD default_text_cases.
DATA(lo_other) = caught_error( iv_case = 'SOMETHING_ELSE'
iv_material = 'MAT-4'
iv_quantity = 7
iv_limit = 70 ).
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material MAT-4`
act = lo_other->get_text( ) ).
DATA(lo_empty) = caught_error( iv_case = ''
iv_material = 'MAT-5'
iv_quantity = 1
iv_limit = 10 ).
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material MAT-5`
act = lo_empty->get_text( ) ).
ENDMETHOD.
METHOD context_of_each_case.
DATA(lo_unknown) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = 'M-1'
iv_quantity = 11
iv_limit = 111 ).
DATA(lo_zero) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
iv_material = 'M-2'
iv_quantity = 0
iv_limit = 222 ).
DATA(lo_over) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'M-3'
iv_quantity = 33
iv_limit = 333 ).
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
lo_info = lo_unknown.
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-unknown_material
act = lo_info->get_case( ) ).
cl_abap_unit_assert=>assert_equals( exp = `M-1` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = 11 act = lo_info->get_quantity( ) ).
cl_abap_unit_assert=>assert_equals( exp = 111 act = lo_info->get_limit( ) ).
lo_info = lo_zero.
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-zero_quantity
act = lo_info->get_case( ) ).
cl_abap_unit_assert=>assert_equals( exp = `M-2` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lo_info->get_quantity( ) ).
cl_abap_unit_assert=>assert_equals( exp = 222 act = lo_info->get_limit( ) ).
lo_info = lo_over.
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-over_limit
act = lo_info->get_case( ) ).
cl_abap_unit_assert=>assert_equals( exp = `M-3` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = 33 act = lo_info->get_quantity( ) ).
cl_abap_unit_assert=>assert_equals( exp = 333 act = lo_info->get_limit( ) ).
ENDMETHOD.
METHOD unknown_case_keeps_data.
DATA(lo_error) = caught_error( iv_case = 'SOMETHING_ELSE'
iv_material = 'M-X'
iv_quantity = 55
iv_limit = 66 ).
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
lo_info = lo_error.
cl_abap_unit_assert=>assert_equals( exp = 'SOMETHING_ELSE' act = lo_info->get_case( ) ).
cl_abap_unit_assert=>assert_equals( exp = `M-X` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = 55 act = lo_info->get_quantity( ) ).
cl_abap_unit_assert=>assert_equals( exp = 66 act = lo_info->get_limit( ) ).
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material M-X`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD text_via_cx_root.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
iv_material = 'MAT-6'
iv_quantity = 0
iv_limit = 60 ).
DATA lo_root TYPE REF TO cx_root.
lo_root = lo_error.
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-6`
act = lo_root->get_text( ) ).
ENDMETHOD.
METHOD case_constant_values.
cl_abap_unit_assert=>assert_equals( exp = 'UNKNOWN_MATERIAL'
act = {{p}}cx_issue_error=>c_case-unknown_material ).
cl_abap_unit_assert=>assert_equals( exp = 'ZERO_QUANTITY'
act = {{p}}cx_issue_error=>c_case-zero_quantity ).
cl_abap_unit_assert=>assert_equals( exp = 'OVER_LIMIT'
act = {{p}}cx_issue_error=>c_case-over_limit ).
ENDMETHOD.
METHOD is_static_check_exception.
DATA lo_caught TYPE REF TO cx_static_check.
TRY.
RAISE EXCEPTION TYPE {{p}}cx_issue_error
EXPORTING
iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = 'MAT-7'
iv_quantity = 3
iv_limit = 30.
CATCH cx_static_check INTO lo_caught.
ENDTRY.
cl_abap_unit_assert=>assert_bound( act = lo_caught ).
ENDMETHOD.
METHOD instances_keep_own_data.
DATA(lo_first) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = 'FIRST'
iv_quantity = 1
iv_limit = 10 ).
DATA(lo_second) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'SECOND'
iv_quantity = 2
iv_limit = 20 ).
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
lo_info = lo_first.
cl_abap_unit_assert=>assert_equals( exp = `FIRST` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = `Material FIRST is not known`
act = lo_first->get_text( ) ).
lo_info = lo_second.
cl_abap_unit_assert=>assert_equals( exp = `SECOND` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity 2 exceeds limit 20 for material SECOND`
act = lo_second->get_text( ) ).
lo_info = lo_first.
cl_abap_unit_assert=>assert_equals( exp = `FIRST` act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lo_info->get_quantity( ) ).
ENDMETHOD.
METHOD same_data_different_case.
DATA lv_material TYPE string.
DATA lv_quantity TYPE i.
DATA lv_limit TYPE i.
lv_material = 'MAT-S'.
lv_quantity = 5.
lv_limit = 10.
DATA(lo_unknown) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = lv_material
iv_quantity = lv_quantity
iv_limit = lv_limit ).
DATA(lo_zero) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
iv_material = lv_material
iv_quantity = lv_quantity
iv_limit = lv_limit ).
DATA(lo_over) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = lv_material
iv_quantity = lv_quantity
iv_limit = lv_limit ).
cl_abap_unit_assert=>assert_equals( exp = `Material MAT-S is not known`
act = lo_unknown->get_text( ) ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-S`
act = lo_zero->get_text( ) ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity 5 exceeds limit 10 for material MAT-S`
act = lo_over->get_text( ) ).
ENDMETHOD.
METHOD text_exact_length.
DATA(lo_unknown) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = 'MAT-1'
iv_quantity = 5
iv_limit = 100 ).
DATA(lo_zero) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
iv_material = 'MAT-2'
iv_quantity = 0
iv_limit = 50 ).
DATA(lo_over) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'MAT-3'
iv_quantity = 120
iv_limit = 100 ).
DATA lv_unknown TYPE string.
DATA lv_zero TYPE string.
DATA lv_over TYPE string.
lv_unknown = `Material MAT-1 is not known`.
lv_zero = `Quantity must not be zero for material MAT-2`.
lv_over = `Quantity 120 exceeds limit 100 for material MAT-3`.
cl_abap_unit_assert=>assert_equals( exp = strlen( lv_unknown )
act = strlen( lo_unknown->get_text( ) ) ).
cl_abap_unit_assert=>assert_equals( exp = strlen( lv_zero )
act = strlen( lo_zero->get_text( ) ) ).
cl_abap_unit_assert=>assert_equals( exp = strlen( lv_over )
act = strlen( lo_over->get_text( ) ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,36 @@
{
"task": "G1904",
"mutants": [
{
"object": "{{P}}CX_ISSUE_ERROR",
"mutant": "line 12: VALUE 'UNKNOWN_MATERIAL' -> VALUE 'ZNKNOWN_MATERIAL' (lit)",
"status": "killed",
"hidden": "11/12",
"failed_tests": [
"CASE_CONSTANT_VALUES"
]
},
{
"object": "{{P}}CX_ISSUE_ERROR",
"mutant": "line 13: VALUE 'ZERO_QUANTITY' -> VALUE 'YERO_QUANTITY' (lit)",
"status": "killed",
"hidden": "11/12",
"failed_tests": [
"CASE_CONSTANT_VALUES"
]
},
{
"object": "{{P}}CX_ISSUE_ERROR",
"mutant": "line 14: VALUE 'OVER_LIMIT' -> VALUE 'ZVER_LIMIT' (lit)",
"status": "killed",
"hidden": "11/12",
"failed_tests": [
"CASE_CONSTANT_VALUES"
]
}
],
"valid": 3,
"killed": 3,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,73 @@
CLASS {{p}}cx_issue_error DEFINITION
PUBLIC
INHERITING FROM cx_static_check
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_issue_error_info.
CONSTANTS:
BEGIN OF c_case,
unknown_material TYPE {{p}}if_issue_error_info=>ty_case VALUE 'UNKNOWN_MATERIAL',
zero_quantity TYPE {{p}}if_issue_error_info=>ty_case VALUE 'ZERO_QUANTITY',
over_limit TYPE {{p}}if_issue_error_info=>ty_case VALUE 'OVER_LIMIT',
END OF c_case.
METHODS constructor
IMPORTING
iv_case TYPE {{p}}if_issue_error_info=>ty_case
iv_material TYPE string
iv_quantity TYPE i
iv_limit TYPE i.
METHODS get_text REDEFINITION.
PRIVATE SECTION.
DATA mv_case TYPE {{p}}if_issue_error_info=>ty_case.
DATA mv_material TYPE string.
DATA mv_quantity TYPE i.
DATA mv_limit TYPE i.
ENDCLASS.
CLASS {{p}}cx_issue_error IMPLEMENTATION.
METHOD constructor.
super->constructor( ).
mv_case = iv_case.
mv_material = iv_material.
mv_quantity = iv_quantity.
mv_limit = iv_limit.
ENDMETHOD.
METHOD {{p}}if_issue_error_info~get_case.
rv_case = mv_case.
ENDMETHOD.
METHOD {{p}}if_issue_error_info~get_material.
rv_material = mv_material.
ENDMETHOD.
METHOD {{p}}if_issue_error_info~get_quantity.
rv_quantity = mv_quantity.
ENDMETHOD.
METHOD {{p}}if_issue_error_info~get_limit.
rv_limit = mv_limit.
ENDMETHOD.
METHOD get_text.
CASE mv_case.
WHEN c_case-unknown_material.
result = |Material { mv_material } is not known|.
WHEN c_case-zero_quantity.
result = |Quantity must not be zero for material { mv_material }|.
WHEN c_case-over_limit.
result = |Quantity { mv_quantity } exceeds limit { mv_limit } for material { mv_material }|.
WHEN OTHERS.
result = |Stock issue failed for material { mv_material }|.
ENDCASE.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,81 @@
CLASS ltc_issue_error DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS unknown_material_text FOR TESTING.
METHODS zero_quantity_text FOR TESTING.
METHODS over_limit_text FOR TESTING.
METHODS default_text FOR TESTING.
METHODS keeps_context_data FOR TESTING.
METHODS caught_error
IMPORTING iv_case TYPE {{p}}if_issue_error_info=>ty_case
iv_material TYPE string
iv_quantity TYPE i
iv_limit TYPE i
RETURNING VALUE(ro_error) TYPE REF TO {{p}}cx_issue_error.
ENDCLASS.
CLASS ltc_issue_error IMPLEMENTATION.
METHOD caught_error.
TRY.
RAISE EXCEPTION TYPE {{p}}cx_issue_error
EXPORTING
iv_case = iv_case
iv_material = iv_material
iv_quantity = iv_quantity
iv_limit = iv_limit.
CATCH {{p}}cx_issue_error INTO ro_error.
ENDTRY.
ENDMETHOD.
METHOD unknown_material_text.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
iv_material = 'MAT-A'
iv_quantity = 1
iv_limit = 10 ).
cl_abap_unit_assert=>assert_equals( exp = `Material MAT-A is not known`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD zero_quantity_text.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
iv_material = 'MAT-B'
iv_quantity = 0
iv_limit = 20 ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-B`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD over_limit_text.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'MAT-C'
iv_quantity = 30
iv_limit = 20 ).
cl_abap_unit_assert=>assert_equals( exp = `Quantity 30 exceeds limit 20 for material MAT-C`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD default_text.
DATA(lo_error) = caught_error( iv_case = 'OTHER'
iv_material = 'MAT-D'
iv_quantity = 5
iv_limit = 50 ).
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material MAT-D`
act = lo_error->get_text( ) ).
ENDMETHOD.
METHOD keeps_context_data.
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
iv_material = 'MAT-E'
iv_quantity = 99
iv_limit = 90 ).
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
lo_info = lo_error.
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-over_limit
act = lo_info->get_case( ) ).
cl_abap_unit_assert=>assert_equals( exp = 'MAT-E' act = lo_info->get_material( ) ).
cl_abap_unit_assert=>assert_equals( exp = 99 act = lo_info->get_quantity( ) ).
cl_abap_unit_assert=>assert_equals( exp = 90 act = lo_info->get_limit( ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,12 @@
INTERFACE {{p}}if_issue_error_info PUBLIC.
TYPES ty_case TYPE c LENGTH 20.
METHODS get_case
RETURNING VALUE(rv_case) TYPE ty_case.
METHODS get_material
RETURNING VALUE(rv_material) TYPE string.
METHODS get_quantity
RETURNING VALUE(rv_quantity) TYPE i.
METHODS get_limit
RETURNING VALUE(rv_limit) TYPE i.
ENDINTERFACE.

View File

@@ -0,0 +1,58 @@
# 1. Goal
A stock issue program must stop with a clear reason when it cannot issue a
quantity of a material. The caller needs one exception class that carries the
reason and the data of the failed issue.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_ISSUE_ERROR_INFO exists in package $TMP. It is active.
- The interface has the type TY_CASE (CHAR 20) and the methods GET_CASE,
GET_MATERIAL, GET_QUANTITY and GET_LIMIT.
- The exception class {{P}}CX_ISSUE_ERROR is raised by the stock issue program
and caught by the caller.
# 4. Contract
- Create the class {{P}}CX_ISSUE_ERROR in package $TMP.
- The class inherits from CX_STATIC_CHECK.
- The class is public, final, and has a public constructor.
- The class implements {{P}}IF_ISSUE_ERROR_INFO.
- The class has the constant structure C_CASE. Its components are
UNKNOWN_MATERIAL, ZERO_QUANTITY and OVER_LIMIT. The type of each component is
{{P}}IF_ISSUE_ERROR_INFO=>TY_CASE. The value of each component is its name.
- The constructor has these importing parameters, in this order:
IV_CASE (type {{P}}IF_ISSUE_ERROR_INFO=>TY_CASE), IV_MATERIAL (type STRING),
IV_QUANTITY (type I), IV_LIMIT (type I).
- The class redefines the inherited method GET_TEXT. GET_TEXT has no importing
parameter and returns the text as a string.
- Do not add other public methods.
# 5. Business rules
1. The constructor stores IV_CASE, IV_MATERIAL, IV_QUANTITY and IV_LIMIT.
2. {{P}}IF_ISSUE_ERROR_INFO~GET_CASE returns the stored case, ~GET_MATERIAL the
stored material, ~GET_QUANTITY the stored quantity and ~GET_LIMIT the stored
limit.
3. GET_TEXT returns one text that depends on the stored case:
- C_CASE-UNKNOWN_MATERIAL: 'Material <material> is not known'
- C_CASE-ZERO_QUANTITY: 'Quantity must not be zero for material <material>'
- C_CASE-OVER_LIMIT: 'Quantity <quantity> exceeds limit <limit> for material
<material>'
- any other case, including an empty case: 'Stock issue failed for material
<material>'
<material>, <quantity> and <limit> are the stored values. Write the quantity
and the limit as plain digits, for example 120 and 100. The text has no
leading and no trailing blanks.
4. The class is a static check exception. A caller must handle it or declare it
in the RAISING clause.
# 6. Constraints
- Release target: SAP_BASIS 816.
- 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_ISSUE_ERROR_INFO.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,50 @@
{
"id": "G1904",
"category": "D",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_ISSUE_ERROR_INFO",
"file": "seed/if_issue_error_info.intf.abap",
"description": "Contract interface with the case type and the getters of the issue error"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}CX_ISSUE_ERROR",
"implements": "{{P}}IF_ISSUE_ERROR_INFO"
}
],
"out_of_scope": [
"{{P}}IF_ISSUE_ERROR_INFO"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}D02_HIDDEN",
"file": "hidden/d02_hidden.clas.abap",
"description": "D02 hidden tests for the issue error exception class"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}CX_ISSUE_ERROR",
"file": "reference/cx_issue_error.clas.abap",
"description": "Exception class with case constants, context data and a redefined GET_TEXT",
"testclasses_file": "reference/cx_issue_error.testclasses.abap"
}
],
"craft_checks": [
"method_length"
]
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Cider pressing batch'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}cider {
key client : abap.clnt not null;
batch_id : abap.char(10) not null;
orchard_id : abap.char(6) not null;
apple_variety : abap.char(20);
pressed_on : abap.dats;
bin_count : abap.int4;
apple_kg : abap.dec(13,3);
juice_l : abap.dec(13,3);
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
press_fee : abap.curr(15,2);
currency_code : abap.cuky;
grade : abap.char(2);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Cider pressing batch'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}cider {
key client : abap.clnt not null;
key batch_id : abap.char(10) not null;
orchard_id : abap.numc(6) not null;
apple_variety : abap.char(20);
pressed_on : abap.dats;
bin_count : abap.int4;
apple_kg : abap.dec(13,3);
juice_l : abap.dec(13,3);
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
press_fee : abap.curr(15,2);
currency_code : abap.cuky;
grade : abap.char(2);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Cider pressing batch'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}cider {
key client : abap.clnt not null;
key batch_id : abap.char(10) not null;
orchard_id : abap.char(5) not null;
apple_variety : abap.char(20);
pressed_on : abap.dats;
bin_count : abap.int4;
apple_kg : abap.dec(13,3);
juice_l : abap.dec(13,3);
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
press_fee : abap.curr(15,2);
currency_code : abap.cuky;
grade : abap.char(2);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Cider pressing batch'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}cider {
key client : abap.clnt not null;
key batch_id : abap.char(10) not null;
orchard_id : abap.char(6) not null;
apple_variety : abap.char(19);
pressed_on : abap.dats;
bin_count : abap.int4;
apple_kg : abap.dec(13,3);
juice_l : abap.dec(13,3);
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
press_fee : abap.curr(15,2);
currency_code : abap.cuky;
grade : abap.char(2);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Cider pressing batch'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}cider {
key client : abap.clnt not null;
key batch_id : abap.char(10) not null;
orchard_id : abap.char(6) not null;
apple_variety : abap.char(20);
pressed_on : abap.dats;
bin_count : abap.int4;
apple_kg : abap.dec(13,3);
juice_l : abap.dec(13,3);
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
press_fee : abap.curr(15,3);
currency_code : abap.cuky;
grade : abap.char(2);
}

View File

@@ -0,0 +1,19 @@
{
"id": "G1910",
"pool": "train",
"object_type": "TABL",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 5,
"killed": 5,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -0,0 +1,157 @@
CLASS {{p}}cider_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES ty_name TYPE c LENGTH 30.
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS field_of
IMPORTING iv_name TYPE ty_name
RETURNING VALUE(rs_field) TYPE dfies.
METHODS field_names FOR TESTING.
METHODS key_fields FOR TESTING.
METHODS not_null_fields FOR TESTING.
METHODS char_lengths FOR TESTING.
METHODS numeric_types FOR TESTING.
METHODS decimals FOR TESTING.
METHODS insert_and_read FOR TESTING.
METHODS duplicate_key FOR TESTING.
ENDCLASS.
CLASS {{p}}cider_hidden IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( '{{P}}CIDER' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
ENDMETHOD.
METHOD field_of.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}CIDER' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
READ TABLE lt_fields INTO rs_field WITH KEY fieldname = iv_name.
cl_abap_unit_assert=>assert_subrc( exp = 0 act = sy-subrc
msg = |Field { iv_name } is missing| ).
ENDMETHOD.
METHOD field_names.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}CIDER' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
cl_abap_unit_assert=>assert_equals( exp = 11 act = lines( lt_fields ) ).
DATA lt_expected TYPE STANDARD TABLE OF ty_name WITH EMPTY KEY.
lt_expected = VALUE #(
( 'CLIENT' ) ( 'BATCH_ID' ) ( 'ORCHARD_ID' ) ( 'APPLE_VARIETY' )
( 'PRESSED_ON' ) ( 'BIN_COUNT' ) ( 'APPLE_KG' ) ( 'JUICE_L' )
( 'PRESS_FEE' ) ( 'CURRENCY_CODE' ) ( 'GRADE' ) ).
LOOP AT lt_expected INTO DATA(lv_name).
READ TABLE lt_fields TRANSPORTING NO FIELDS WITH KEY fieldname = lv_name.
cl_abap_unit_assert=>assert_subrc( exp = 0 act = sy-subrc
msg = |Field { lv_name } is missing| ).
ENDLOOP.
ENDMETHOD.
METHOD key_fields.
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( 'CLIENT' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( 'BATCH_ID' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( 'ORCHARD_ID' )-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( 'PRESS_FEE' )-keyflag ).
ENDMETHOD.
METHOD not_null_fields.
DATA lt_expected TYPE STANDARD TABLE OF ty_name WITH EMPTY KEY.
lt_expected = VALUE #( ( 'CLIENT' ) ( 'BATCH_ID' ) ( 'ORCHARD_ID' ) ).
LOOP AT lt_expected INTO DATA(lv_name).
DATA(ls_field) = field_of( lv_name ).
ASSIGN COMPONENT 'NOT_NULL' OF STRUCTURE ls_field TO FIELD-SYMBOL(<lv_flag>).
IF sy-subrc = 0.
cl_abap_unit_assert=>assert_equals( exp = 'X' act = <lv_flag>
msg = |Field { lv_name } must be not null| ).
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD char_lengths.
cl_abap_unit_assert=>assert_equals( exp = 3 act = CONV i( field_of( 'CLIENT' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 10 act = CONV i( field_of( 'BATCH_ID' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 6 act = CONV i( field_of( 'ORCHARD_ID' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 20 act = CONV i( field_of( 'APPLE_VARIETY' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 5 act = CONV i( field_of( 'CURRENCY_CODE' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = CONV i( field_of( 'GRADE' )-leng ) ).
ENDMETHOD.
METHOD numeric_types.
cl_abap_unit_assert=>assert_equals( exp = 'CLNT' act = field_of( 'CLIENT' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( 'BATCH_ID' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = field_of( 'PRESSED_ON' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'INT4' act = field_of( 'BIN_COUNT' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'CURR' act = field_of( 'PRESS_FEE' )-datatype ).
cl_abap_unit_assert=>assert_equals( exp = 'CUKY' act = field_of( 'CURRENCY_CODE' )-datatype ).
ENDMETHOD.
METHOD decimals.
cl_abap_unit_assert=>assert_equals( exp = 13 act = CONV i( field_of( 'APPLE_KG' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = CONV i( field_of( 'APPLE_KG' )-decimals ) ).
cl_abap_unit_assert=>assert_equals( exp = 13 act = CONV i( field_of( 'JUICE_L' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = CONV i( field_of( 'JUICE_L' )-decimals ) ).
cl_abap_unit_assert=>assert_equals( exp = 15 act = CONV i( field_of( 'PRESS_FEE' )-leng ) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = CONV i( field_of( 'PRESS_FEE' )-decimals ) ).
ENDMETHOD.
METHOD insert_and_read.
DATA lt_batch TYPE STANDARD TABLE OF {{p}}cider WITH EMPTY KEY.
lt_batch = VALUE #(
( batch_id = 'B000000001' orchard_id = 'OR0001' apple_variety = 'ELSTAR'
pressed_on = '20250801' bin_count = 12 apple_kg = '480.500'
juice_l = '320.250' press_fee = '125.50' currency_code = 'EUR'
grade = 'A1' )
( batch_id = 'B000000002' orchard_id = 'OR0002' apple_variety = 'GALA'
pressed_on = '20250802' bin_count = 8 apple_kg = '310.000'
juice_l = '205.000' press_fee = '80.00' currency_code = 'EUR'
grade = 'B2' ) ).
INSERT {{p}}cider FROM TABLE @lt_batch.
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
SELECT SINGLE orchard_id, bin_count, apple_kg, press_fee, currency_code, grade
FROM {{p}}cider
WHERE batch_id = 'B000000001'
INTO @DATA(ls_row).
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 'OR0001' act = ls_row-orchard_id ).
cl_abap_unit_assert=>assert_equals( exp = 12 act = ls_row-bin_count ).
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '480.5' ) act = CONV decfloat34( ls_row-apple_kg ) ).
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '125.50' ) act = CONV decfloat34( ls_row-press_fee ) ).
cl_abap_unit_assert=>assert_equals( exp = 'EUR' act = ls_row-currency_code ).
cl_abap_unit_assert=>assert_equals( exp = 'A1' act = ls_row-grade ).
ENDMETHOD.
METHOD duplicate_key.
DATA ls_batch TYPE {{p}}cider.
ls_batch-batch_id = 'B000000009'.
ls_batch-orchard_id = 'OR0009'.
INSERT {{p}}cider FROM @ls_batch.
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
INSERT {{p}}cider FROM @ls_batch.
cl_abap_unit_assert=>assert_equals( exp = 4 act = sy-subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,56 @@
{
"task": "G1910",
"mutants": [
{
"object": "{{P}}CIDER",
"mutant": "line 8: key batch_id : -> batch_id : (key)",
"status": "killed",
"hidden": "6/8",
"failed_tests": [
"INSERT_AND_READ",
"KEY_FIELDS"
]
},
{
"object": "{{P}}CIDER",
"mutant": "line 9: abap.char(6) -> abap.numc(6) (type)",
"status": "killed",
"hidden": "7/8",
"failed_tests": [
"INSERT_AND_READ"
]
},
{
"object": "{{P}}CIDER",
"mutant": "line 9: abap.char(6) -> abap.char(5) (length)",
"status": "killed",
"hidden": "6/8",
"failed_tests": [
"CHAR_LENGTHS",
"INSERT_AND_READ"
]
},
{
"object": "{{P}}CIDER",
"mutant": "line 10: abap.char(20) -> abap.char(19) (length)",
"status": "killed",
"hidden": "7/8",
"failed_tests": [
"CHAR_LENGTHS"
]
},
{
"object": "{{P}}CIDER",
"mutant": "line 16: abap.curr(15,2) -> abap.curr(15,3) (decimals)",
"status": "killed",
"hidden": "7/8",
"failed_tests": [
"DECIMALS"
]
}
],
"valid": 5,
"killed": 5,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Cider pressing batch'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}cider {
key client : abap.clnt not null;
key batch_id : abap.char(10) not null;
orchard_id : abap.char(6) not null;
apple_variety : abap.char(20);
pressed_on : abap.dats;
bin_count : abap.int4;
apple_kg : abap.dec(13,3);
juice_l : abap.dec(13,3);
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
press_fee : abap.curr(15,2);
currency_code : abap.cuky;
grade : abap.char(2);
}

View File

@@ -0,0 +1,58 @@
CLASS {{p}}cider_test DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS stores_batch FOR TESTING.
METHODS rejects_duplicate FOR TESTING.
ENDCLASS.
CLASS {{p}}cider_test IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( '{{P}}CIDER' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
ENDMETHOD.
METHOD stores_batch.
DATA ls_batch TYPE {{p}}cider.
ls_batch-batch_id = 'B000000001'.
ls_batch-orchard_id = 'OR0001'.
ls_batch-apple_kg = '480.500'.
ls_batch-press_fee = '125.50'.
ls_batch-currency_code = 'EUR'.
INSERT {{p}}cider FROM @ls_batch.
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
SELECT SINGLE orchard_id, apple_kg FROM {{p}}cider
WHERE batch_id = 'B000000001'
INTO @DATA(ls_read).
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 'OR0001' act = ls_read-orchard_id ).
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '480.5' ) act = CONV decfloat34( ls_read-apple_kg ) ).
ENDMETHOD.
METHOD rejects_duplicate.
DATA ls_batch TYPE {{p}}cider.
ls_batch-batch_id = 'B000000002'.
INSERT {{p}}cider FROM @ls_batch.
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
INSERT {{p}}cider FROM @ls_batch.
cl_abap_unit_assert=>assert_equals( exp = 4 act = sy-subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,51 @@
# 1. Goal
The cider press house of a fruit cooperative records each pressing batch of
apple deliveries. Create the database table that stores these batches. A press
terminal writes the batches and a reporting tool reads them. The field names,
the data types and the key must be exactly as agreed.
# 2. Open questions
None.
# 3. Context
- The table is in package $TMP.
- The press house receives apples from the member orchards.
- One pressing batch comes from one orchard.
- The press house charges a fee for each pressing. The fee is money.
# 4. Contract
- Create the transparent database table {{P}}CIDER in package $TMP as a DDL
source.
- The table has these fields, with these names: CLIENT, BATCH_ID, ORCHARD_ID,
APPLE_VARIETY, PRESSED_ON, BIN_COUNT, APPLE_KG, JUICE_L, PRESS_FEE,
CURRENCY_CODE, GRADE.
- The table has no other fields.
- The key of the table is CLIENT and BATCH_ID.
# 5. Business rules
1. CLIENT is the client field of the table (CLNT, 3 characters). It is a key
field. It is not null.
2. BATCH_ID is the number of the pressing batch (CHAR 10). It is a key field.
It is not null.
3. ORCHARD_ID is the number of the orchard that delivered the apples (CHAR 6).
It is not null.
4. APPLE_VARIETY is the name of the apple variety (CHAR 20).
5. PRESSED_ON is the date of the pressing (DATS, 8 characters).
6. BIN_COUNT is the number of apple bins of the batch (INT4).
7. APPLE_KG is the weight of the apples in kilograms (DEC 13,3).
8. JUICE_L is the volume of the juice in litres (DEC 13,3).
9. PRESS_FEE is the fee of the pressing (CURR 15,2). It is an amount in the
currency that CURRENCY_CODE gives.
10. CURRENCY_CODE is the currency key (CUKY, 5 characters).
11. GRADE is the quality grade of the juice (CHAR 2).
# 6. Constraints
- Release target: 8.16 (SAP_BASIS 816).
- Coding standards: Clean ABAP.
- Out of scope: do not create other tables, views, classes or programs. Create
only the table and your own test class.
# 7. Acceptance
- The table is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests with CL_OSQL_TEST_ENVIRONMENT in a global test class.

View File

@@ -0,0 +1,56 @@
{
"id": "G1910",
"category": "B",
"object_type": "TABL",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [],
"contract": [
{
"type": "TABL",
"name": "{{P}}CIDER",
"fields": [
"client",
"batch_id",
"orchard_id",
"apple_variety",
"pressed_on",
"bin_count",
"apple_kg",
"juice_l",
"press_fee",
"currency_code",
"grade"
]
}
],
"out_of_scope": [],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}CIDER_HIDDEN",
"file": "hidden/cider_hidden.clas.abap",
"description": "Hidden tests for the cider pressing batch table"
}
],
"reference": [
{
"type": "TABL",
"name": "{{P}}CIDER",
"file": "reference/cider.tabl.asabap",
"description": "Cider pressing batch table"
},
{
"type": "CLAS",
"name": "{{P}}CIDER_TEST",
"file": "reference/cider_test.clas.abap",
"description": "Own tests for the table"
}
],
"craft_checks": []
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Beach item hire'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}hire {
key client : abap.clnt not null;
hire_id : abap.numc(10) not null;
kiosk_id : abap.char(6) not null;
hire_date : abap.dats not null;
item_type : abap.char(4);
item_count : abap.int4;
hire_days : abap.int4;
@Semantics.amount.currencyCode : '{{p}}hire.currency_code'
daily_rate : abap.dec(13,2);
currency_code : abap.cuky;
customer_name : abap.char(30);
is_returned : abap.char(1);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Beach item hire'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}hire {
key client : abap.clnt not null;
key hire_id : abap.numc(10) not null;
kiosk_id : abap.char(5) not null;
hire_date : abap.dats not null;
item_type : abap.char(4);
item_count : abap.int4;
hire_days : abap.int4;
@Semantics.amount.currencyCode : '{{p}}hire.currency_code'
daily_rate : abap.dec(13,2);
currency_code : abap.cuky;
customer_name : abap.char(30);
is_returned : abap.char(1);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Beach item hire'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}hire {
key client : abap.clnt not null;
key hire_id : abap.numc(10) not null;
kiosk_id : abap.char(6) not null;
hire_date : abap.dats not null;
item_type : abap.char(4);
item_count : abap.int8;
hire_days : abap.int4;
@Semantics.amount.currencyCode : '{{p}}hire.currency_code'
daily_rate : abap.dec(13,2);
currency_code : abap.cuky;
customer_name : abap.char(30);
is_returned : abap.char(1);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Beach item hire'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}hire {
key client : abap.clnt not null;
key hire_id : abap.numc(10) not null;
kiosk_id : abap.char(6) not null;
hire_date : abap.dats not null;
item_type : abap.char(4);
item_count : abap.int4;
hire_days : abap.int4;
@Semantics.amount.currencyCode : '{{p}}hire.currency_code'
daily_rate : abap.dec(13,3);
currency_code : abap.cuky;
customer_name : abap.char(30);
is_returned : abap.char(1);
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Beach item hire'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}hire {
key client : abap.clnt not null;
key hire_id : abap.numc(10) not null;
kiosk_id : abap.char(6) not null;
hire_date : abap.dats not null;
item_type : abap.char(4);
item_count : abap.int4;
hire_days : abap.int4;
@Semantics.amount.currencyCode : '{{p}}hire.currency_code'
daily_rate : abap.dec(13,2);
currency_code : abap.cuky;
customer_name : abap.numc(30);
is_returned : abap.char(1);
}

View File

@@ -0,0 +1,24 @@
{
"id": "G1911",
"pool": "train",
"object_type": "TABL",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0,
"mutation": {
"valid": 5,
"killed": 5,
"ok": true
}
}
],
"accepted": true
}

View File

@@ -0,0 +1,231 @@
CLASS {{p}}hire_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS get_fields RETURNING VALUE(rt_fields) TYPE dfies_table.
METHODS field_of
IMPORTING iv_name TYPE fieldname
RETURNING VALUE(rs_field) TYPE dfies.
METHODS is_key
IMPORTING iv_name TYPE fieldname
RETURNING VALUE(rv_is_key) TYPE abap_bool.
METHODS is_not_null
IMPORTING iv_name TYPE fieldname
RETURNING VALUE(rv_not_null) TYPE abap_bool.
METHODS fields_exist FOR TESTING.
METHODS key_fields FOR TESTING.
METHODS not_null_fields FOR TESTING.
METHODS lengths_in_chars FOR TESTING.
METHODS decimals_of_rate FOR TESTING.
METHODS field_types FOR TESTING.
METHODS insert_and_read FOR TESTING.
METHODS duplicate_key FOR TESTING.
METHODS select_by_kiosk FOR TESTING.
ENDCLASS.
CLASS {{p}}hire_hidden IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( '{{P}}HIRE' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
ENDMETHOD.
METHOD get_fields.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}HIRE' ) ).
rt_fields = lo_struct->get_ddic_field_list( ).
ENDMETHOD.
METHOD field_of.
DATA(lt_fields) = get_fields( ).
READ TABLE lt_fields INTO rs_field WITH KEY fieldname = iv_name.
IF sy-subrc <> 0.
cl_abap_unit_assert=>fail( msg = |The field { iv_name } is missing| ).
ENDIF.
ENDMETHOD.
METHOD is_key.
DATA(ls_field) = field_of( iv_name ).
IF ls_field-keyflag = 'X'.
rv_is_key = abap_true.
ENDIF.
ENDMETHOD.
METHOD is_not_null.
DATA(ls_field) = field_of( iv_name ).
ASSIGN COMPONENT 'NOTNULL' OF STRUCTURE ls_field TO FIELD-SYMBOL(<lv_flag>).
IF sy-subrc <> 0.
ASSIGN COMPONENT 'NOT_NULL' OF STRUCTURE ls_field TO <lv_flag>.
ENDIF.
IF sy-subrc = 0.
IF <lv_flag> = 'X'.
rv_not_null = abap_true.
ENDIF.
ENDIF.
ENDMETHOD.
METHOD fields_exist.
DATA(lt_fields) = get_fields( ).
cl_abap_unit_assert=>assert_equals( exp = 11 act = lines( lt_fields ) ).
DATA lt_names TYPE STANDARD TABLE OF fieldname WITH EMPTY KEY.
lt_names = VALUE #( ( 'CLIENT' ) ( 'HIRE_ID' ) ( 'KIOSK_ID' )
( 'HIRE_DATE' ) ( 'ITEM_TYPE' ) ( 'ITEM_COUNT' )
( 'HIRE_DAYS' ) ( 'DAILY_RATE' ) ( 'CURRENCY_CODE' )
( 'CUSTOMER_NAME' ) ( 'IS_RETURNED' ) ).
LOOP AT lt_names INTO DATA(lv_name).
READ TABLE lt_fields TRANSPORTING NO FIELDS WITH KEY fieldname = lv_name.
cl_abap_unit_assert=>assert_equals(
exp = 0 act = sy-subrc msg = |The field { lv_name } is missing| ).
ENDLOOP.
ENDMETHOD.
METHOD key_fields.
cl_abap_unit_assert=>assert_equals( exp = abap_true act = is_key( 'CLIENT' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_true act = is_key( 'HIRE_ID' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = is_key( 'KIOSK_ID' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = is_key( 'HIRE_DATE' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = is_key( 'DAILY_RATE' ) ).
ENDMETHOD.
METHOD not_null_fields.
DATA(ls_field) = field_of( 'KIOSK_ID' ).
ASSIGN COMPONENT 'NOTNULL' OF STRUCTURE ls_field TO FIELD-SYMBOL(<lv_flag>).
IF sy-subrc <> 0.
RETURN.
ENDIF.
cl_abap_unit_assert=>assert_equals( exp = 'X' act = <lv_flag> ).
cl_abap_unit_assert=>assert_equals( exp = abap_true act = is_not_null( 'HIRE_ID' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_true act = is_not_null( 'HIRE_DATE' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = is_not_null( 'ITEM_TYPE' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = is_not_null( 'DAILY_RATE' ) ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = is_not_null( 'CUSTOMER_NAME' ) ).
ENDMETHOD.
METHOD lengths_in_chars.
DATA(ls_field) = field_of( 'CLIENT' ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'HIRE_ID' ).
cl_abap_unit_assert=>assert_equals( exp = 10 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'KIOSK_ID' ).
cl_abap_unit_assert=>assert_equals( exp = 6 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'HIRE_DATE' ).
cl_abap_unit_assert=>assert_equals( exp = 8 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'ITEM_TYPE' ).
cl_abap_unit_assert=>assert_equals( exp = 4 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'ITEM_COUNT' ).
cl_abap_unit_assert=>assert_equals( exp = 10 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'HIRE_DAYS' ).
cl_abap_unit_assert=>assert_equals( exp = 10 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'DAILY_RATE' ).
cl_abap_unit_assert=>assert_equals( exp = 13 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'CURRENCY_CODE' ).
cl_abap_unit_assert=>assert_equals( exp = 5 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'CUSTOMER_NAME' ).
cl_abap_unit_assert=>assert_equals( exp = 30 act = CONV i( ls_field-leng ) ).
ls_field = field_of( 'IS_RETURNED' ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = CONV i( ls_field-leng ) ).
ENDMETHOD.
METHOD decimals_of_rate.
DATA(ls_field) = field_of( 'DAILY_RATE' ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = CONV i( ls_field-decimals ) ).
ls_field = field_of( 'ITEM_COUNT' ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = CONV i( ls_field-decimals ) ).
ls_field = field_of( 'HIRE_DAYS' ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = CONV i( ls_field-decimals ) ).
ENDMETHOD.
METHOD field_types.
DATA(ls_field) = field_of( 'CLIENT' ).
cl_abap_unit_assert=>assert_equals( exp = 'CLNT' act = ls_field-datatype ).
ls_field = field_of( 'HIRE_ID' ).
cl_abap_unit_assert=>assert_equals( exp = 'NUMC' act = ls_field-datatype ).
ls_field = field_of( 'KIOSK_ID' ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = ls_field-datatype ).
ls_field = field_of( 'HIRE_DATE' ).
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = ls_field-datatype ).
ls_field = field_of( 'ITEM_TYPE' ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = ls_field-datatype ).
ls_field = field_of( 'ITEM_COUNT' ).
cl_abap_unit_assert=>assert_equals( exp = 'INT4' act = ls_field-datatype ).
ls_field = field_of( 'HIRE_DAYS' ).
cl_abap_unit_assert=>assert_equals( exp = 'INT4' act = ls_field-datatype ).
ls_field = field_of( 'DAILY_RATE' ).
cl_abap_unit_assert=>assert_equals( exp = 'DEC' act = ls_field-datatype(3) ).
ls_field = field_of( 'CURRENCY_CODE' ).
cl_abap_unit_assert=>assert_equals( exp = 'CUKY' act = ls_field-datatype ).
ls_field = field_of( 'CUSTOMER_NAME' ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = ls_field-datatype ).
ls_field = field_of( 'IS_RETURNED' ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = ls_field-datatype ).
ENDMETHOD.
METHOD insert_and_read.
DATA lt_hire TYPE STANDARD TABLE OF {{p}}hire WITH EMPTY KEY.
DATA lv_date TYPE d.
DATA lv_curr TYPE c LENGTH 5.
lv_date = '20250701'.
lv_curr = 'EUR'.
lt_hire = VALUE #(
( hire_id = '0000000001' kiosk_id = 'K1' hire_date = '20250701'
item_type = 'CHAI' item_count = 2 hire_days = 3
daily_rate = '4.50' currency_code = 'EUR'
customer_name = 'SMITH' is_returned = 'X' )
( hire_id = '0000000002' kiosk_id = 'K2' hire_date = '20250702'
item_type = 'PARA' item_count = 1 hire_days = 1
daily_rate = '6.00' currency_code = 'EUR'
customer_name = 'JONES' is_returned = ' ' ) ).
go_env->insert_test_data( lt_hire ).
SELECT SINGLE hire_id, kiosk_id, hire_date, item_type, item_count,
hire_days, daily_rate, currency_code, customer_name,
is_returned
FROM {{p}}hire
WHERE hire_id = '0000000001'
INTO @DATA(ls_hire).
cl_abap_unit_assert=>assert_equals( exp = '0000000001' act = ls_hire-hire_id ).
cl_abap_unit_assert=>assert_equals( exp = 'K1' act = ls_hire-kiosk_id ).
cl_abap_unit_assert=>assert_equals( exp = lv_date act = ls_hire-hire_date ).
cl_abap_unit_assert=>assert_equals( exp = 'CHAI' act = ls_hire-item_type ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = ls_hire-item_count ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = ls_hire-hire_days ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '4.50' )
act = CONV decfloat34( ls_hire-daily_rate ) ).
cl_abap_unit_assert=>assert_equals( exp = lv_curr act = ls_hire-currency_code ).
cl_abap_unit_assert=>assert_equals( exp = 'SMITH' act = ls_hire-customer_name ).
cl_abap_unit_assert=>assert_equals( exp = 'X' act = ls_hire-is_returned ).
ENDMETHOD.
METHOD duplicate_key.
DATA ls_hire TYPE {{p}}hire.
ls_hire-hire_id = '0000000009'.
ls_hire-kiosk_id = 'K9'.
ls_hire-hire_date = '20250703'.
INSERT {{p}}hire FROM @ls_hire.
INSERT {{p}}hire FROM @ls_hire.
cl_abap_unit_assert=>assert_equals( exp = 4 act = sy-subrc ).
ENDMETHOD.
METHOD select_by_kiosk.
DATA lt_hire TYPE STANDARD TABLE OF {{p}}hire WITH EMPTY KEY.
lt_hire = VALUE #(
( hire_id = '0000000011' kiosk_id = 'K1' hire_date = '20250704' )
( hire_id = '0000000012' kiosk_id = 'K1' hire_date = '20250704' )
( hire_id = '0000000013' kiosk_id = 'K2' hire_date = '20250704' ) ).
go_env->insert_test_data( lt_hire ).
SELECT COUNT(*) FROM {{p}}hire WHERE kiosk_id = 'K1' INTO @DATA(lv_count).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_count ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,58 @@
{
"task": "G1911",
"mutants": [
{
"object": "{{P}}HIRE",
"mutant": "line 8: key hire_id : -> hire_id : (key)",
"status": "killed",
"hidden": "6/9",
"failed_tests": [
"INSERT_AND_READ",
"KEY_FIELDS",
"SELECT_BY_KIOSK"
]
},
{
"object": "{{P}}HIRE",
"mutant": "line 9: abap.char(6) -> abap.char(5) (length)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"LENGTHS_IN_CHARS"
]
},
{
"object": "{{P}}HIRE",
"mutant": "line 12: abap.int4 -> abap.int8 (type)",
"status": "killed",
"hidden": "7/9",
"failed_tests": [
"FIELD_TYPES",
"LENGTHS_IN_CHARS"
]
},
{
"object": "{{P}}HIRE",
"mutant": "line 15: abap.dec(13,2) -> abap.dec(13,3) (decimals)",
"status": "killed",
"hidden": "8/9",
"failed_tests": [
"DECIMALS_OF_RATE"
]
},
{
"object": "{{P}}HIRE",
"mutant": "line 17: abap.char(30) -> abap.numc(30) (type)",
"status": "killed",
"hidden": "7/9",
"failed_tests": [
"FIELD_TYPES",
"INSERT_AND_READ"
]
}
],
"valid": 5,
"killed": 5,
"kill_rate": 1.0,
"ok": true
}

View File

@@ -0,0 +1,19 @@
@EndUserText.label : 'Beach item hire'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}hire {
key client : abap.clnt not null;
key hire_id : abap.numc(10) not null;
kiosk_id : abap.char(6) not null;
hire_date : abap.dats not null;
item_type : abap.char(4);
item_count : abap.int4;
hire_days : abap.int4;
@Semantics.amount.currencyCode : '{{p}}hire.currency_code'
daily_rate : abap.dec(13,2);
currency_code : abap.cuky;
customer_name : abap.char(30);
is_returned : abap.char(1);
}

View File

@@ -0,0 +1,76 @@
CLASS {{p}}hire_test DEFINITION PUBLIC FINAL CREATE PUBLIC
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 setup.
METHODS structure_ok FOR TESTING.
METHODS stores_and_reads FOR TESTING.
METHODS duplicate_is_rejected FOR TESTING.
ENDCLASS.
CLASS {{p}}hire_test IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create(
i_dependency_list = VALUE #( ( '{{P}}HIRE' ) ) ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
go_env->clear_doubles( ).
ENDMETHOD.
METHOD structure_ok.
DATA(lo_struct) = CAST cl_abap_structdescr(
cl_abap_typedescr=>describe_by_name( '{{P}}HIRE' ) ).
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
cl_abap_unit_assert=>assert_equals( exp = 11 act = lines( lt_fields ) ).
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'HIRE_ID'.
cl_abap_unit_assert=>assert_equals( exp = 'X' act = ls_field-keyflag ).
cl_abap_unit_assert=>assert_equals( exp = 10 act = CONV i( ls_field-leng ) ).
ENDMETHOD.
METHOD stores_and_reads.
DATA ls_hire TYPE {{p}}hire.
DATA lt_hire TYPE STANDARD TABLE OF {{p}}hire WITH EMPTY KEY.
ls_hire-hire_id = '0000000001'.
ls_hire-kiosk_id = 'K1'.
ls_hire-hire_date = '20250701'.
ls_hire-item_type = 'CHAI'.
ls_hire-item_count = 2.
ls_hire-hire_days = 3.
ls_hire-daily_rate = '4.50'.
ls_hire-currency_code = 'EUR'.
ls_hire-customer_name = 'SMITH'.
ls_hire-is_returned = 'X'.
APPEND ls_hire TO lt_hire.
go_env->insert_test_data( lt_hire ).
SELECT SINGLE kiosk_id, item_count, hire_days, daily_rate, is_returned
FROM {{p}}hire
WHERE hire_id = '0000000001'
INTO @DATA(ls_read).
cl_abap_unit_assert=>assert_equals( exp = 'K1' act = ls_read-kiosk_id ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = ls_read-item_count ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = ls_read-hire_days ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '4.50' )
act = CONV decfloat34( ls_read-daily_rate ) ).
cl_abap_unit_assert=>assert_equals( exp = 'X' act = ls_read-is_returned ).
ENDMETHOD.
METHOD duplicate_is_rejected.
DATA ls_hire TYPE {{p}}hire.
ls_hire-hire_id = '0000000002'.
ls_hire-kiosk_id = 'K2'.
ls_hire-hire_date = '20250702'.
INSERT {{p}}hire FROM @ls_hire.
INSERT {{p}}hire FROM @ls_hire.
cl_abap_unit_assert=>assert_equals( exp = 4 act = sy-subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,78 @@
# 1. Goal
A beach kiosk hires out beach chairs and parasols to holidaymakers. The kiosk
needs one database table that stores every hire. A report and an OData service
will read this table later, so the table is the only object of this task.
# 2. Open questions
None.
# 3. Context
- The table is new. It has no data and no other object uses it yet.
- One hire is one row of the table.
- hire_id is the number of the hire receipt.
- kiosk_id identifies the kiosk that made the hire.
- hire_date is the day of the hire.
- item_type is the code of the hired item: 'CHAI' is a beach chair and 'PARA'
is a parasol.
- item_count is the number of hired items.
- hire_days is the number of days of the hire.
- daily_rate is the price of one item for one day, in the currency
currency_code.
- customer_name is the name of the customer.
- is_returned is 'X' when the customer returned the items and ' ' when the
items are still out.
- All objects are in package $TMP.
# 4. Contract
- Create the transparent database table {{P}}HIRE in package $TMP.
- The table has these 11 fields, with these names (lower case): client,
hire_id, kiosk_id, hire_date, item_type, item_count, hire_days, daily_rate,
currency_code, customer_name, is_returned.
- The data types, the key fields and the not-null fields are fixed by rule 1
of section 5.
# 5. Business rules
1. The table has exactly these fields, in this order:
| Field | Type and length | Key | Not null |
|---------------|-----------------|-----|----------|
| client | abap.clnt | yes | yes |
| hire_id | abap.numc(10) | yes | yes |
| kiosk_id | abap.char(6) | no | yes |
| hire_date | abap.dats | no | yes |
| item_type | abap.char(4) | no | no |
| item_count | abap.int4 | no | no |
| hire_days | abap.int4 | no | no |
| daily_rate | abap.dec(13,2) | no | no |
| currency_code | abap.cuky | no | no |
| customer_name | abap.char(30) | no | no |
| is_returned | abap.char(1) | no | no |
2. client and hire_id are the key of the table. Two rows with the same client
and the same hire_id are not allowed.
3. client, hire_id, kiosk_id and hire_date are not null. All other fields can
be empty.
4. daily_rate is an amount with 2 decimals in the currency currency_code.
item_count and hire_days are whole numbers.
5. A row that was inserted can be read again with the same field values.
6. Rows can be selected by kiosk_id.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025), client 001.
- Coding standards: Clean ABAP. Create the table as a DDL source with
"define table ..." and the annotations @EndUserText.label,
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE,
@AbapCatalog.tableCategory : #TRANSPARENT,
@AbapCatalog.deliveryClass : #A and
@AbapCatalog.dataMaintenance : #RESTRICTED.
- daily_rate is an amount in the currency currency_code, so it needs the
currency reference to currency_code.
- Out of scope: no CDS view, no report, no function module, no class other
than the test class for your own ABAP Unit tests.
# 7. Acceptance
- The table {{P}}HIRE is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests in a global test class in package $TMP. Check the
structure of the table with cl_abap_typedescr and the data access with
cl_osql_test_environment.

View File

@@ -0,0 +1,56 @@
{
"id": "G1911",
"category": "B",
"object_type": "TABL",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [],
"contract": [
{
"type": "TABL",
"name": "{{P}}HIRE",
"fields": [
"client",
"hire_id",
"kiosk_id",
"hire_date",
"item_type",
"item_count",
"hire_days",
"daily_rate",
"currency_code",
"customer_name",
"is_returned"
]
}
],
"out_of_scope": [],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}HIRE_HIDDEN",
"file": "hidden/hire_hidden.clas.abap",
"description": "Structure (RTTI) and data access (OSQL test double) tests for the table"
}
],
"reference": [
{
"type": "TABL",
"name": "{{P}}HIRE",
"file": "reference/hire.tabl.asabap",
"description": "Beach item hire table"
},
{
"type": "CLAS",
"name": "{{P}}HIRE_TEST",
"file": "reference/hire_test.clas.abap",
"description": "Own ABAP Unit tests for the table"
}
],
"craft_checks": []
}

Some files were not shown because too many files have changed in this diff Show More