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>
This commit is contained in:
Kral
2026-10-05 18:52:29 +02:00
parent 454db6626c
commit b119f1afac
39 changed files with 2329 additions and 24 deletions

View File

@@ -53,6 +53,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"):

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,29 @@ 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 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:
@@ -351,7 +375,7 @@ def render(d):
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>" + 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,57 @@ 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, lengths, decimals, types), 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.""",
"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 +272,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 +346,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 +466,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": []}

79
harness/mix.py Normal file
View File

@@ -0,0 +1,79 @@
"""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

View File

@@ -148,6 +148,72 @@ 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"(\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 +222,22 @@ 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)
return mutants(src, o["type"], f"{task_id}:{o['name']}", n)
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,6 +248,13 @@ 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)
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) 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 {}
@@ -187,7 +268,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,10 +129,21 @@ 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 cands = [t for t in tasks if (t, 0) not in done and (t, 0) not in self.inflight]
if (t, 0) not in done and (t, 0) not in self.inflight: if cands: # first attempts: the kind with the biggest deficit against the target mix goes first
self.inflight.add((t, 0)) counts = {}
return t, 0 for r in rows:
if self.evaluate(r)["accepted"]:
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
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"]):
@@ -275,6 +286,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 +320,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}.
""" """

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)
@@ -285,9 +295,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 []
for iname in ([impl] if isinstance(impl, str) else impl): # one interface or a list
if not re.search(rf"INTERFACES\s+{re.escape(str(iname))}\b", src, re.I):
g2 = False g2 = False
g2_detail.append(f"{c['name']}: does not implement {c['implements']}") 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 +315,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"]:
cols = set()
if c["type"] != "STRU": # a structure cannot be selected
_, q = mcp.call("sap_sql_query", {"query": f"SELECT * FROM {c['name']}", "maxRows": 1}) _, 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", [])} 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}

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")
@@ -186,6 +187,137 @@ 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:
while backlog() > BACKLOG_LIMIT and not os.path.exists(STOP_FLAG) and not (deadline and time.time() > deadline):
time.sleep(120)
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)
kind = mix.deficit_pick(counts, allowed=set(mix.TYPE_SHARE) - blocked)
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 +372,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 +387,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)

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"
]
}