Hard batch G0022-G0026: 5/5 accepted; budget floor, CDS checks, mutation check

- generator: budget floor (2x oracle activations, 3x calls), static checks for CDS
  $parameters and UNION annotation
- harness/mutation.py: deterministic mutants of the reference; hidden tests must fail
- G0022 revalidated with harness fixes: oracle 100, null 0
- results in docs/faz1-tasarim.md 11f

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kral
2026-10-03 05:16:30 +02:00
parent 3570d7ef9b
commit 32f03fcad4
53 changed files with 2931 additions and 14 deletions

View File

@@ -80,7 +80,8 @@ python3 -m harness.cli run T01 --agent oracle|null|llm [--model NAME] --run N
python3 -m harness.cli rescore runs/<run_dir> python3 -m harness.cli rescore runs/<run_dir>
python3 -m harness.cli cleanup-list <PREFIX> runs/<dir> && python3 -m harness.cli teardown runs/<dir> python3 -m harness.cli cleanup-list <PREFIX> runs/<dir> && python3 -m harness.cli teardown runs/<dir>
python3 -m harness.generator --id G0100 --pool eval --object-type CLAS --category C --run-base 2000 python3 -m harness.generator --id G0100 --pool eval --object-type CLAS --category C --run-base 2000
python3 -m harness.pilot 2 python3 -m harness.pilot 2 # pilot list; `python3 -m harness.pilot 22 hard 1400` hard batch
python3 -m harness.mutation G0002 --pool eval --run-base 5000 [--keep]
python3 -c "from harness.ledger import spent; print(spent())" python3 -c "from harness.ledger import spent; print(spent())"
``` ```
@@ -89,18 +90,18 @@ python3 -c "from harness.ledger import spent; print(spent())"
- Done: harness on Mac mini; test include creation in the server; teardown; FUNC/PROG/DDLS support; - Done: harness on Mac mini; test include creation in the server; teardown; FUNC/PROG/DDLS support;
task generator; ledger and budget guard. task generator; ledger and budget guard.
- Results: T01 oracle 100, Qwen 27B 41.7 (old run 103), DeepSeek 98.5. T13 DeepSeek 85, T14 DeepSeek 100. - Results: T01 oracle 100, Qwen 27B 41.7 (old run 103), DeepSeek 98.5. T13 DeepSeek 85, T14 DeepSeek 100.
- Pilot (G0002–G0021) done: 16/20 accepted, 0.16 USD per accepted task (`runs/gen/pilot.json`, - Pilot (G0002–G0021): 16/20 accepted, 0.16 USD per accepted task (`docs/faz1-tasarim.md` 11e).
analysis in `docs/faz1-tasarim.md` 11e). Generator improved after the pilot: static checks G0020 and G0013 fixed in review and revalidated → 18/20.
(name length, seed type, reserved words, contract/test classes), local abaplint parser check before SAP, - Hard batch (G0022–G0026, 2026-10-03): 5/5 accepted (G0022 after harness fixes), 0.097 USD per accepted
max_tokens 80k, G2 detail in the repair feedback. Runner: G2 works for CDS views with parameters. task, first-attempt 1/5 (`docs/faz1-tasarim.md` 11f).
The improved generator is not yet tested with new tasks. - Generator: static checks, local abaplint parser check, dependency order of seed/reference, budget floor,
- G0020 (rejected) passes with oracle 100 after one fix (remove parameter `default`). G0013 (accepted) has a test max_tokens 80k. Runner: G2 for CDS with parameters, G6 ignores unknown standard superclasses.
class in its contract: fix in review. - Step D: `harness/mutation.py` (deterministic mutants of the reference, hidden tests must fail).
## 7. Next steps ## 7. Next steps
1. Test the improved generator with a small batch (~5 tasks, ~0.7 USD); compare repairs per task with the pilot (2.75 calls). 1. Step D: run the mutation check on all accepted tasks; integrate it into the generator.
2. Step D: mutation check (hidden tests must fail on a broken reference). 2. Use killed mutants as faulty references for own-test scoring (component (a)).
3. Step F: eval set of 110 tasks: ~150 candidates, empirical filter (2–3 models), Claude review with a 3. Step F: eval set of 110 tasks: ~150 candidates, empirical filter (2–3 models), Claude review with a
checklist, Kral spot-checks ~10 flagged tasks. checklist, Kral spot-checks ~10 flagged tasks.
4. Scoring for stop tasks (category H) and category K (free-text input, other tool schema). 4. Scoring for stop tasks (category H) and category K (free-text input, other tool schema).

View File

@@ -576,6 +576,35 @@ Diğer bulgular:
- Üretilen görevler örnekteki isimleri kopyalıyor (`T02_HIDDEN`, `T14_TEST`). Kozmetik. - Üretilen görevler örnekteki isimleri kopyalıyor (`T02_HIDDEN`, `T14_TEST`). Kozmetik.
- Statik kontrol ve abaplint ön kontrolü SAP'ye gitmez. Pilottaki 37 başarısız denemenin ~18'ini doğrulama koşusundan önce yakalar (tahmin). - Statik kontrol ve abaplint ön kontrolü SAP'ye gitmez. Pilottaki 37 başarısız denemenin ~18'ini doğrulama koşusundan önce yakalar (tahmin).
## 11f. Zor görev partisi (2026-10-03, G0022–G0026)
Amaç: pilot sonrası iyileştirmeleri pilotta düşen tiplerle ölçmek. Görevler: CLAS D (exception), CLAS E (packed sayılı eski kod, `LENGTH` tuzağı), parametreli CDS, UNION'lı CDS, FM. Sonuçlar: `runs/gen/hard.json`.
| | Pilot (20) | Parti (5, zor) |
|---|---|---|
| İlk denemede kabul | 3/20 (%15) | 1/5 (%20): G0023 |
| Toplam kabul | 16/20 | 4/5 üretimde; G0022 harness düzeltmesinden sonra → 5/5 |
| LLM çağrısı / görev | 2,75 | 2,6 |
| Maliyet / kabul edilen görev | 0,16 $ | **0,097 $** (0,39 $ / 5) |
| En büyük çıktı | 393k (kaçak) | 61,5k |
SAP'ye gitmeden yakalanan hata: **2** (abaplint `parser_error`, G0026 ×2). Statik kontroller bu partide hata bulmadı: 30 karakter, tablo tipi, `LENGTH` hataları tekrar etmedi (prompt kuralları etkili).
SAP'de düşen 8 doğrulama:
| Neden | Sayı | Durum |
|---|---|---|
| Harness: referans objeler bağımlılık sırasında kurulmuyor (G0022, G0026) | 2 | Düzeltildi: generator seed/reference listesini sıraya dizer |
| Harness: abaplint `CX_STATIC_CHECK`'i tanımıyor → exception hiyerarşisi G6'da düşer (G0022). LLM koşularında da kategori D'yi 0'a indirirdi | 1 | Düzeltildi: G6 bu mesajı yok sayar (G1 aktivasyonu kontrol eder) |
| CDS view entity'de `:p_from` (G0024) | 1 | Statik kontrol + prompt kuralı |
| UNION'da `@Metadata.ignorePropagatedAnnotations` yok (G0025) | 1 | Statik kontrol + prompt kuralı |
| Referansın kendi testlerinde tip hatası (G0022) | 2 | Onarım turu |
| Abaplint hatası; asıl neden G6 harness hatasıydı (G0022) | 1 | Düzeltildi |
Ek bulgu: G0022'nin bütçesi (12 aktivasyon) referansın kendisine (13) yetmiyordu → oracle disiplinden 5 puan kaybetti. Generator artık bütçeye alt sınır koyar: aktivasyon ≥ 2 × oracle, tool çağrısı ≥ 3 × oracle. Mevcut görevlerden sadece G0022 etkilendi.
G0022 bundle'ı değiştirilmeden yeniden doğrulandı (harness düzeltmeleri + bütçe): oracle 100, null 0 → kabul.
## 12. Açık sorular ## 12. Açık sorular
Cevaplananlar (Adım 1.2): Cevaplananlar (Adım 1.2):

View File

@@ -86,8 +86,8 @@ Adım 3 görev ister; görevler el ile yazılırsa darboğaz olur. Bu yüzden il
| A | Önek şeması: koşu 4 karakter base36, görev 3 karakter base36 (`Z005P001_`) | ✓ | | A | Önek şeması: koşu 4 karakter base36, görev 3 karakter base36 (`Z005P001_`) | ✓ |
| B | Harness: FUGR/FUNC, PROG, DDLS desteği (G2 kontrat kontrolü, abaplint dışa aktarımı, silme sırası) | ✓ T13–T15 oracle 100 / null 0. Server eşzamanlılık sorunu bulundu (harness'te retry) | | B | Harness: FUGR/FUNC, PROG, DDLS desteği (G2 kontrat kontrolü, abaplint dışa aktarımı, silme sırası) | ✓ T13–T15 oracle 100 / null 0. Server eşzamanlılık sorunu bulundu (harness'te retry) |
| C | Görev üreteci: DeepSeek görev yazar (spec, seed, gizli testler, referans + local testler); kategori ve obje tipi dağılımına göre | ✓ `harness/generator.py`: üretim → yapı + statik kontrol + yerel abaplint → oracle/null doğrulama → en fazla 3 onarım turu. Pilot sonrası iyileştirildi (faz1-tasarim 11e) | | C | Görev üreteci: DeepSeek görev yazar (spec, seed, gizli testler, referans + local testler); kategori ve obje tipi dağılımına göre | ✓ `harness/generator.py`: üretim → yapı + statik kontrol + yerel abaplint → oracle/null doğrulama → en fazla 3 onarım turu. Pilot sonrası iyileştirildi (faz1-tasarim 11e) |
| D | Otomatik görev doğrulama: oracle = 100, null = 0, gizli testler bozuk referansta düşmeli | oracle/null hazır; mutasyon kontrolü sonra | | D | Otomatik görev doğrulama: oracle = 100, null = 0, gizli testler bozuk referansta düşmeli | oracle/null hazır; mutasyon kontrolü `harness/mutation.py` (2026-10-03) |
| E | Pilot: ~20 görev, tüm obje tipleri; kabul oranı ve gerçek maliyet ölçümü | ✓ 16/20 kabul, 0,16 $ / kabul edilen görev (faz1-tasarim 11e). İyileştirilmiş generator henüz denenmedi | | E | Pilot: ~20 görev, tüm obje tipleri; kabul oranı ve gerçek maliyet ölçümü | ✓ 16/20 kabul, 0,16 $ / kabul edilen görev (faz1-tasarim 11e). Zor parti (5 görev): 5/5 kabul, 0,097 $ / kabul edilen görev (11f) |
| F | Eval seti: 110 görev; Kral incelemesi. Eğitim havuzu: otomatik doğrulanmış görevler, eval ile çakışmasız | | | F | Eval seti: 110 görev; Kral incelemesi. Eğitim havuzu: otomatik doğrulanmış görevler, eval ile çakışmasız | |
| G | Ölçüm (1.6) ve yörünge üretimi (3.2) paralel worker'larla (bulut model: en fazla 6) | | | G | Ölçüm (1.6) ve yörünge üretimi (3.2) paralel worker'larla (bulut model: en fazla 6) | |

View File

@@ -49,8 +49,10 @@ Rules for the bundle:
- Method parameters need a complete type: "TYPE c LENGTH 4" is not allowed in a signature. - Method parameters need a complete type: "TYPE c LENGTH 4" is not allowed in a signature.
Declare a type first (TYPES ty_zone TYPE c LENGTH 4) and use it. Declare a type first (TYPES ty_zone TYPE c LENGTH 4) and use it.
- Field names in tables and CDS views are not SQL reserved words (no HOURS, MODE, ORDER, DATE, COUNT ...). - Field names in tables and CDS views are not SQL reserved words (no HOURS, MODE, ORDER, DATE, COUNT ...).
- CDS: use "define view entity". Parameters have no default value. In a UNION, all branches have the same - CDS: use "define view entity". Parameters have no default value. Use a parameter as
key elements and element names. Hidden tests for a view with parameters pass all parameters. $parameters.p_name (not :p_name). In a UNION, all branches have the same
key elements and element names, and the view
has the annotation @Metadata.ignorePropagatedAnnotations: true. Hidden tests for a view with parameters pass all parameters.
- Legacy seed code (categories E, I) must activate on SAP_BASIS 816: do not mix old and new syntax in one - Legacy seed code (categories E, I) must activate on SAP_BASIS 816: do not mix old and new syntax in one
SQL statement (with new syntax, every host variable needs "@"). SQL statement (with new syntax, every host variable needs "@").
- The contract lists only the objects that the hidden tests call. Test classes are never in the contract. - The contract lists only the objects that the hidden tests call. Test classes are never in the contract.
@@ -178,6 +180,14 @@ def lint_files(b):
for f in re.findall(r"^\s*(?:key\s+)?(\w+)\s*:", src, re.I | re.M): for f in re.findall(r"^\s*(?:key\s+)?(\w+)\s*:", src, re.I | re.M):
if f.upper() in RESERVED: if f.upper() in RESERVED:
errs.append(f"{rel}: field name {f} is a reserved word; choose another name") errs.append(f"{rel}: field name {f} is a reserved word; choose another name")
for rel, src in b.get("files", {}).items():
if rel.endswith(".asddls") and re.search(r"define\s+(root\s+)?view\s+entity", src, re.I):
if re.search(r"\bunion\b", src, re.I) and not re.search(r"@Metadata\.ignorePropagatedAnnotations\s*:\s*true",
src, re.I):
errs.append(f"{rel}: a view entity with UNION needs @Metadata.ignorePropagatedAnnotations: true")
for p in sorted(set(re.findall(r"[=<>(,]\s*:(\w+)|\bbetween\s+:(\w+)|\band\s+:(\w+)", src, re.I))):
name = next(x for x in p if x)
errs.append(f"{rel}: parameter :{name}; a view entity needs $parameters.{name}")
for k in ("seed", "reference"): for k in ("seed", "reference"):
for o in t.get(k, []): for o in t.get(k, []):
m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), "")) m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), ""))
@@ -279,11 +289,23 @@ def dependency_order(objs, files):
return out return out
def floor_budget(task):
"""The model needs more calls than the oracle (reads, repairs). G0022: budget 12 activations,
reference alone 13. Minimum: 2 x oracle activations, 3 x oracle tool calls."""
ref = task.get("reference", [])
pushes = sum(bool(o.get("file")) + bool(o.get("testclasses_file")) for o in ref)
calls = len(ref) + pushes
bud = task.setdefault("budget", {})
bud["max_activations"] = max(int(bud.get("max_activations", 0)), 2 * pushes)
bud["max_tool_calls"] = max(int(bud.get("max_tool_calls", 0)), 3 * calls)
def write_bundle(b, task_dir, task_id): def write_bundle(b, task_dir, task_id):
os.makedirs(task_dir, exist_ok=True) os.makedirs(task_dir, exist_ok=True)
b["task"]["id"] = task_id b["task"]["id"] = task_id
for k in ("seed", "reference"): for k in ("seed", "reference"):
b["task"][k] = dependency_order(b["task"].get(k, []), b["files"]) b["task"][k] = dependency_order(b["task"].get(k, []), b["files"])
floor_budget(b["task"])
json.dump(b["task"], open(os.path.join(task_dir, "task.json"), "w"), indent=2) json.dump(b["task"], open(os.path.join(task_dir, "task.json"), "w"), indent=2)
for rel, content in b["files"].items(): for rel, content in b["files"].items():
path = os.path.join(task_dir, rel) path = os.path.join(task_dir, rel)

188
harness/mutation.py Normal file
View File

@@ -0,0 +1,188 @@
"""Mutation check (step D): the hidden tests must fail on a broken reference.
Small deterministic changes (mutants) go into the main source of the contract objects of the reference.
For each mutant the oracle writes the mutated reference and the harness runs the hidden tests.
killed = reference active and at least one hidden test fails
survived = reference active and all hidden tests pass (a test gap or an equivalent mutant)
invalid = the mutant does not activate (not counted)
python3 -m harness.mutation G0002 G0003 --pool eval --run-base 5000
"""
import argparse
import json
import os
import random
import re
import shutil
from .adt_client import load_env
from .agents import OracleAgent
from .runner import Runner
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAX_MUTANTS = 5
MIN_VALID = 3
MIN_KILL_RATE = 0.75
# (kind, pattern, replacement); patterns match only outside comments and literals
ABAP_OPS = [
("rel", r"(?<=\s)>=(?=\s)", "<"), ("rel", r"(?<=\s)<=(?=\s)", ">"),
("rel", r"(?<=\s)>(?=\s)", ">="), ("rel", r"(?<=\s)<(?=\s)", "<="),
("rel", r"(?<=\s)<>(?=\s)", "="),
("rel", r"\bGE\b", "LT"), ("rel", r"\bLE\b", "GT"), ("rel", r"\bGT\b", "GE"), ("rel", r"\bLT\b", "LE"),
("rel", r"\bNE\b", "EQ"),
("logic", r"(?<=\s)AND(?=\s)", "OR"), ("logic", r"(?<=\s)OR(?=\s)", "AND"),
("arith", r"(?<=\s)\+(?=\s)", "-"), ("arith", r"(?<=\s)-(?=\s)", "+"), ("arith", r"(?<=\s)\*(?=\s)", "/"),
("bool", r"\babap_true\b", "abap_false"), ("bool", r"\babap_false\b", "abap_true"),
("const", r"(?<![\w.'-])([1-9]\d{0,5})(?![\w.'])", None), # integer literal + 1
]
CDS_OPS = [
("rel", r"(?<=\s)>=(?=\s)", "<"), ("rel", r"(?<=\s)<=(?=\s)", ">"),
("rel", r"(?<=\s)>(?=\s)", ">="), ("rel", r"(?<=\s)<(?=\s)", "<="), ("rel", r"(?<=\s)<>(?=\s)", "="),
("logic", r"(?<=\s)and(?=\s)", "or"),
("arith", r"(?<=\s)\+(?=\s)", "-"), ("arith", r"(?<=\s)-(?=\s)", "+"),
("agg", r"\bsum\s*\(", "max("), ("agg", r"\bcount\s*\(", "max("), ("agg", r"\bavg\s*\(", "max("),
("join", r"\binner\s+join\b", "left outer join"),
("const", r"(?<![\w.'-])([1-9]\d{0,5})(?![\w.'])", None),
]
def _mask(src, cds):
"""True for each character inside a comment or a literal."""
m = [False] * len(src)
pats = ([r"//[^\n]*", r"/\*.*?\*/", r"'[^']*'", r"@[^\n]*"] if cds else
[r'"[^\n]*', r"^\*[^\n]*", r"'(?:[^']|'')*'", r"`[^`]*`", r"\|(?:[^|\\]|\\.)*\|"])
for p in pats:
for x in re.finditer(p, src, re.M | re.S):
for i in range(x.start(), x.end()):
m[i] = True
return m
def _region(src, otype):
"""Start and end of the part that may change: the implementation, not declarations or test classes."""
start, end = 0, len(src)
if otype == "CLAS":
x = re.search(r"^\s*CLASS\s+\S+\s+IMPLEMENTATION", src, re.I | re.M)
start = x.end() if x else len(src)
if otype == "PROG":
x = re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING", src, re.I | re.M)
end = x.start() if x else len(src)
if otype == "DDLS":
x = re.search(r"\bas\s+select\b|\bas\s+projection\b", src, re.I)
start = x.start() if x else 0
return start, end
def _skip_line(line):
"""Declarations and signatures: a change there gives syntax errors or no behavior change."""
return re.match(r"\s*(DATA|TYPES|CONSTANTS|METHODS|CLASS-METHODS|PARAMETERS|SELECT-OPTIONS|"
r"IMPORTING|EXPORTING|RETURNING|RAISING|TABLES|FIELD-SYMBOLS)\b", line, re.I)
def mutants(src, otype, seed, n=MAX_MUTANTS):
"""Up to n mutants as (description, source). Different kinds first, spread over the source."""
cds = otype == "DDLS"
mask = _mask(src, cds)
lo, hi = _region(src, otype)
sites = []
for kind, pat, rep in (CDS_OPS if cds else ABAP_OPS):
for x in re.finditer(pat, src, re.I):
if not lo <= x.start() < hi or mask[x.start()]:
continue
line_start = src.rfind("\n", 0, x.start()) + 1
line_end = src.find("\n", x.start())
line = src[line_start:line_end if line_end >= 0 else len(src)]
if _skip_line(line) or (kind == "const" and re.search(r"\bLENGTH\b|\bDECIMALS\b|\(\s*\d+\s*,", line, re.I)):
continue
new = str(int(x.group(1)) + 1) if rep is None else rep
sites.append((kind, x.start(), x.end(), new, src.count("\n", 0, x.start()) + 1, x.group(0)))
rnd = random.Random(seed)
rnd.shuffle(sites)
chosen, kinds, lines = [], set(), set()
for prefer_new in (True, False):
for s in sites:
if len(chosen) >= n:
break
if s in chosen or s[4] in lines or (prefer_new and s[0] in kinds):
continue
chosen.append(s)
kinds.add(s[0])
lines.add(s[4])
out = []
for kind, a, b, new, line, old in sorted(chosen, key=lambda s: s[1]):
out.append((f"line {line}: {old.strip()} -> {new} ({kind})", src[:a] + new + src[b:]))
return out
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."""
task_dir = os.path.join(pool_root, task_id)
meta = json.load(open(os.path.join(task_dir, "task.json")))
contract = {c["name"].upper() for c in meta["contract"]}
targets = [o for o in meta["reference"] if o["name"].upper() in contract and o.get("file")
and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS")]
work = os.path.join(ROOT, "runs", "gen", "mut")
os.makedirs(work, exist_ok=True)
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
cand = [[(o, d, m) for d, m in mutants(open(os.path.join(task_dir, o["file"])).read(), o["type"],
f"{task_id}:{o['name']}", n)] for o in targets]
plan = []
while len(plan) < n and any(cand):
for c in cand:
if c and len(plan) < n:
plan.append(c.pop(0))
results = []
for k, (o, desc, msrc) in enumerate(plan):
mdir = os.path.join(work, "pool", task_id)
shutil.rmtree(mdir, ignore_errors=True)
shutil.copytree(task_dir, mdir)
open(os.path.join(mdir, o["file"]), "w").write(msrc)
rep, _ = runner.run(task_id, OracleAgent(), run_base + k)
h = rep.get("hidden_tests") or {}
g = rep.get("gates") or {}
if not g.get("G1_active") or not h.get("total"):
status = "invalid"
elif h["passed"] < h["total"]:
status = "killed"
else:
status = "survived"
results.append({"object": o["name"], "mutant": desc, "status": status,
"hidden": f"{h.get('passed')}/{h.get('total')}",
"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
fdir = os.path.join(task_dir, "faulty")
os.makedirs(fdir, exist_ok=True)
open(os.path.join(fdir, f"m{k}_{os.path.basename(o['file'])}"), "w").write(msrc)
shutil.rmtree(os.path.join(work, "pool", task_id), ignore_errors=True)
valid = [r for r in results if r["status"] != "invalid"]
killed = [r for r in valid if r["status"] == "killed"]
summary = {"task": task_id, "mutants": results, "valid": len(valid), "killed": len(killed),
"kill_rate": round(len(killed) / len(valid), 2) if valid else None,
"ok": len(valid) >= MIN_VALID and len(killed) / max(1, len(valid)) >= MIN_KILL_RATE}
if len(valid) < MIN_VALID:
summary["note"] = f"fewer than {MIN_VALID} valid mutants; check by review"
json.dump(summary, open(os.path.join(task_dir, "mutation.json"), "w"), indent=1)
return summary
def main():
load_env(os.path.join(ROOT, ".env"))
ap = argparse.ArgumentParser()
ap.add_argument("tasks", nargs="+")
ap.add_argument("--pool", default="eval")
ap.add_argument("--run-base", type=int, required=True)
ap.add_argument("-n", type=int, default=MAX_MUTANTS)
ap.add_argument("--keep", action="store_true", help="store killed mutants in <task>/faulty/")
a = ap.parse_args()
root = os.path.join(ROOT, "tasks_gen", a.pool) if a.pool != "tasks" else os.path.join(ROOT, "tasks")
for i, t in enumerate(a.tasks):
s = check_task(root, t, a.run_base + 10 * i, a.n, a.keep)
print(json.dumps({k: v for k, v in s.items() if k != "mutants"}), flush=True)
for m in s["mutants"]:
print(" ", m["status"], m["object"], m["mutant"], m["hidden"], flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,43 @@
{
"id": "G0022",
"pool": "eval",
"object_type": "CLAS",
"category": "D",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "revalidate",
"by": "harness fix 2026-10-03",
"change": "G6 ignores unknown standard superclass (abaplint); reference in dependency order. Bundle not changed.",
"oracle": 95.0,
"null": 0
},
{
"stage": "revalidate",
"by": "harness fix 2026-10-03",
"change": "budget floor: max_activations 12 -> 14 (reference alone needs 13).",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,223 @@
CLASS {{p}}d01_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES ty_amount TYPE {{p}}if_expense_claim=>ty_amount.
DATA mo_cut TYPE REF TO {{p}}expense_validator.
METHODS setup.
METHODS valid_claim_passes FOR TESTING.
METHODS zero_amount_rejected FOR TESTING.
METHODS negative_amount_rejected FOR TESTING.
METHODS unknown_currency_rejected FOR TESTING.
METHODS future_date_rejected FOR TESTING.
METHODS today_is_accepted FOR TESTING.
METHODS receipt_required FOR TESTING.
METHODS receipt_not_required_below FOR TESTING.
METHODS cost_center_required FOR TESTING.
METHODS cost_center_not_required FOR TESTING.
METHODS amount_rule_has_priority FOR TESTING.
METHODS receipt_rule_has_priority FOR TESTING.
METHODS build_claim
IMPORTING iv_amount TYPE ty_amount
iv_currency TYPE {{p}}if_expense_claim=>ty_currency
iv_date TYPE d
iv_cc TYPE {{p}}if_expense_claim=>ty_cost_center
iv_receipt TYPE abap_bool
RETURNING VALUE(rs_claim) TYPE {{p}}if_expense_claim=>ty_claim.
ENDCLASS.
CLASS {{p}}d01_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}expense_validator( ).
ENDMETHOD.
METHOD build_claim.
rs_claim = VALUE #( amount = iv_amount
currency = iv_currency
claim_date = iv_date
cost_center = iv_cc
has_receipt = iv_receipt ).
ENDMETHOD.
METHOD valid_claim_passes.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
CATCH {{p}}cx_claim_error INTO DATA(lx).
cl_abap_unit_assert=>fail( msg = lx->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD zero_amount_rejected.
DATA(ls_claim) = build_claim( iv_amount = 0 iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_amount INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '0.00' )
act = lx->mv_amount ).
cl_abap_unit_assert=>assert_equals( exp = 'Amount must be positive'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD negative_amount_rejected.
DATA(ls_claim) = build_claim( iv_amount = '-50.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_amount INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '-50.00' )
act = lx->mv_amount ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD unknown_currency_rejected.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'JPY'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_currency INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = 'JPY'
act = lx->mv_currency ).
cl_abap_unit_assert=>assert_equals( exp = 'Currency is not supported'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD future_date_rejected.
DATA lv_tomorrow TYPE d.
lv_tomorrow = sy-datum + 1.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = lv_tomorrow iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_date INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = lv_tomorrow
act = lx->mv_claim_date ).
cl_abap_unit_assert=>assert_equals( exp = 'Claim date is in the future'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD today_is_accepted.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
CATCH {{p}}cx_claim_error INTO DATA(lx).
cl_abap_unit_assert=>fail( msg = lx->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD receipt_required.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_false ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_receipt INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '250.00' )
act = lx->mv_amount ).
cl_abap_unit_assert=>assert_equals( exp = 'Receipt is required'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD receipt_not_required_below.
DATA(ls_claim) = build_claim( iv_amount = '99.99' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_false ).
TRY.
mo_cut->validate( ls_claim ).
CATCH {{p}}cx_claim_error INTO DATA(lx).
cl_abap_unit_assert=>fail( msg = lx->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD cost_center_required.
DATA(ls_claim) = build_claim( iv_amount = '1500.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = ''
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_costcenter INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '1500.00' )
act = lx->mv_amount ).
cl_abap_unit_assert=>assert_equals( exp = 'Cost center is required'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD cost_center_not_required.
DATA(ls_claim) = build_claim( iv_amount = '999.99' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = ''
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
CATCH {{p}}cx_claim_error INTO DATA(lx).
cl_abap_unit_assert=>fail( msg = lx->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD amount_rule_has_priority.
DATA lv_future TYPE d.
lv_future = sy-datum + 1.
DATA(ls_claim) = build_claim( iv_amount = 0 iv_currency = 'JPY'
iv_date = lv_future iv_cc = ''
iv_receipt = abap_false ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_amount INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '0.00' )
act = lx->mv_amount ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD receipt_rule_has_priority.
DATA(ls_claim) = build_claim( iv_amount = '1500.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = ''
iv_receipt = abap_false ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_receipt INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '1500.00' )
act = lx->mv_amount ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,20 @@
CLASS {{p}}cx_claim_amount DEFINITION
PUBLIC
INHERITING FROM {{p}}cx_claim_error
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_amount TYPE {{p}}if_expense_claim=>ty_amount READ-ONLY.
METHODS constructor
IMPORTING iv_amount TYPE {{p}}if_expense_claim=>ty_amount.
ENDCLASS.
CLASS {{p}}cx_claim_amount IMPLEMENTATION.
METHOD constructor.
super->constructor( iv_message = 'Amount must be positive' ).
mv_amount = iv_amount.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,20 @@
CLASS {{p}}cx_claim_costcenter DEFINITION
PUBLIC
INHERITING FROM {{p}}cx_claim_error
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_amount TYPE {{p}}if_expense_claim=>ty_amount READ-ONLY.
METHODS constructor
IMPORTING iv_amount TYPE {{p}}if_expense_claim=>ty_amount.
ENDCLASS.
CLASS {{p}}cx_claim_costcenter IMPLEMENTATION.
METHOD constructor.
super->constructor( iv_message = 'Cost center is required' ).
mv_amount = iv_amount.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,20 @@
CLASS {{p}}cx_claim_currency DEFINITION
PUBLIC
INHERITING FROM {{p}}cx_claim_error
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_currency TYPE {{p}}if_expense_claim=>ty_currency READ-ONLY.
METHODS constructor
IMPORTING iv_currency TYPE {{p}}if_expense_claim=>ty_currency.
ENDCLASS.
CLASS {{p}}cx_claim_currency IMPLEMENTATION.
METHOD constructor.
super->constructor( iv_message = 'Currency is not supported' ).
mv_currency = iv_currency.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,20 @@
CLASS {{p}}cx_claim_date DEFINITION
PUBLIC
INHERITING FROM {{p}}cx_claim_error
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_claim_date TYPE d READ-ONLY.
METHODS constructor
IMPORTING iv_claim_date TYPE d.
ENDCLASS.
CLASS {{p}}cx_claim_date IMPLEMENTATION.
METHOD constructor.
super->constructor( iv_message = 'Claim date is in the future' ).
mv_claim_date = iv_claim_date.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,20 @@
CLASS {{p}}cx_claim_receipt DEFINITION
PUBLIC
INHERITING FROM {{p}}cx_claim_error
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_amount TYPE {{p}}if_expense_claim=>ty_amount READ-ONLY.
METHODS constructor
IMPORTING iv_amount TYPE {{p}}if_expense_claim=>ty_amount.
ENDCLASS.
CLASS {{p}}cx_claim_receipt IMPLEMENTATION.
METHOD constructor.
super->constructor( iv_message = 'Receipt is required' ).
mv_amount = iv_amount.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,85 @@
CLASS {{p}}expense_validator DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS validate
IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
PRIVATE SECTION.
CONSTANTS:
c_receipt_limit TYPE {{p}}if_expense_claim=>ty_amount VALUE '100.00',
c_cc_limit TYPE {{p}}if_expense_claim=>ty_amount VALUE '1000.00'.
METHODS check_amount
IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
METHODS check_currency
IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
METHODS check_date
IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
METHODS check_receipt
IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
METHODS check_cost_center
IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
METHODS is_supported_currency
IMPORTING iv_currency TYPE {{p}}if_expense_claim=>ty_currency
RETURNING VALUE(rv_supported) TYPE abap_bool.
ENDCLASS.
CLASS {{p}}expense_validator IMPLEMENTATION.
METHOD validate.
check_amount( is_claim ).
check_currency( is_claim ).
check_date( is_claim ).
check_receipt( is_claim ).
check_cost_center( is_claim ).
ENDMETHOD.
METHOD check_amount.
IF is_claim-amount <= 0.
RAISE EXCEPTION TYPE {{p}}cx_claim_amount
EXPORTING iv_amount = is_claim-amount.
ENDIF.
ENDMETHOD.
METHOD check_currency.
IF is_supported_currency( is_claim-currency ) = abap_false.
RAISE EXCEPTION TYPE {{p}}cx_claim_currency
EXPORTING iv_currency = is_claim-currency.
ENDIF.
ENDMETHOD.
METHOD check_date.
IF is_claim-claim_date > sy-datum.
RAISE EXCEPTION TYPE {{p}}cx_claim_date
EXPORTING iv_claim_date = is_claim-claim_date.
ENDIF.
ENDMETHOD.
METHOD check_receipt.
IF is_claim-amount >= c_receipt_limit AND is_claim-has_receipt = abap_false.
RAISE EXCEPTION TYPE {{p}}cx_claim_receipt
EXPORTING iv_amount = is_claim-amount.
ENDIF.
ENDMETHOD.
METHOD check_cost_center.
IF is_claim-amount >= c_cc_limit AND is_claim-cost_center IS INITIAL.
RAISE EXCEPTION TYPE {{p}}cx_claim_costcenter
EXPORTING iv_amount = is_claim-amount.
ENDIF.
ENDMETHOD.
METHOD is_supported_currency.
rv_supported = xsdbool( iv_currency = 'EUR' OR iv_currency = 'USD'
OR iv_currency = 'GBP' OR iv_currency = 'CHF' ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,127 @@
CLASS ltc_expense_validator DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES ty_amount TYPE {{p}}if_expense_claim=>ty_amount.
DATA mo_cut TYPE REF TO {{p}}expense_validator.
METHODS setup.
METHODS valid_claim FOR TESTING.
METHODS zero_amount FOR TESTING.
METHODS unknown_currency FOR TESTING.
METHODS future_date FOR TESTING.
METHODS receipt_missing FOR TESTING.
METHODS cost_center_missing FOR TESTING.
METHODS build_claim
IMPORTING iv_amount TYPE ty_amount
iv_currency TYPE {{p}}if_expense_claim=>ty_currency
iv_date TYPE d
iv_cc TYPE {{p}}if_expense_claim=>ty_cost_center
iv_receipt TYPE abap_bool
RETURNING VALUE(rs_claim) TYPE {{p}}if_expense_claim=>ty_claim.
ENDCLASS.
CLASS ltc_expense_validator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}expense_validator( ).
ENDMETHOD.
METHOD build_claim.
rs_claim = VALUE #( amount = iv_amount
currency = iv_currency
claim_date = iv_date
cost_center = iv_cc
has_receipt = iv_receipt ).
ENDMETHOD.
METHOD valid_claim.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
CATCH {{p}}cx_claim_error INTO DATA(lx).
cl_abap_unit_assert=>fail( msg = lx->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD zero_amount.
DATA(ls_claim) = build_claim( iv_amount = 0 iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_amount INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = CONV ty_amount( '0.00' )
act = lx->mv_amount ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD unknown_currency.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'JPY'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_currency INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = 'JPY'
act = lx->mv_currency ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD future_date.
DATA lv_future TYPE d.
lv_future = sy-datum + 1.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = lv_future iv_cc = 'CC1000'
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_date INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = lv_future
act = lx->mv_claim_date ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD receipt_missing.
DATA(ls_claim) = build_claim( iv_amount = '250.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = 'CC1000'
iv_receipt = abap_false ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_receipt INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = 'Receipt is required'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
METHOD cost_center_missing.
DATA(ls_claim) = build_claim( iv_amount = '1500.00' iv_currency = 'EUR'
iv_date = sy-datum iv_cc = ''
iv_receipt = abap_true ).
TRY.
mo_cut->validate( ls_claim ).
cl_abap_unit_assert=>fail( msg = 'Exception expected' ).
CATCH {{p}}cx_claim_costcenter INTO DATA(lx).
cl_abap_unit_assert=>assert_equals( exp = 'Cost center is required'
act = lx->get_message( ) ).
CATCH {{p}}cx_claim_error INTO DATA(lx_other).
cl_abap_unit_assert=>fail( msg = lx_other->get_message( ) ).
ENDTRY.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,27 @@
CLASS {{p}}cx_claim_error DEFINITION
PUBLIC
INHERITING FROM cx_static_check
CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING iv_message TYPE string.
METHODS get_message
RETURNING VALUE(rv_message) TYPE string.
PRIVATE SECTION.
DATA mv_message TYPE string.
ENDCLASS.
CLASS {{p}}cx_claim_error IMPLEMENTATION.
METHOD constructor.
super->constructor( ).
mv_message = iv_message.
ENDMETHOD.
METHOD get_message.
rv_message = mv_message.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,14 @@
INTERFACE {{p}}if_expense_claim PUBLIC.
TYPES ty_amount TYPE p LENGTH 8 DECIMALS 2.
TYPES ty_currency TYPE c LENGTH 3.
TYPES ty_cost_center TYPE c LENGTH 10.
TYPES:
BEGIN OF ty_claim,
amount TYPE ty_amount,
currency TYPE ty_currency,
claim_date TYPE d,
cost_center TYPE ty_cost_center,
has_receipt TYPE abap_bool,
END OF ty_claim.
ENDINTERFACE.

View File

@@ -0,0 +1,72 @@
# 1. Goal
Validate an employee expense claim before it is posted to accounting. The check
reports the first rule that fails as a class-based exception. Each exception
carries the value that caused the failure and a short message.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_EXPENSE_CLAIM exists in package $TMP and is active. It
contains types only and has no methods:
- TY_AMOUNT, a packed number with length 8 and 2 decimals.
- TY_CURRENCY, a character field with length 3.
- TY_COST_CENTER, a character field with length 10.
- TY_CLAIM, a structure with the components AMOUNT (TY_AMOUNT), CURRENCY
(TY_CURRENCY), CLAIM_DATE (type D), COST_CENTER (TY_COST_CENTER) and
HAS_RECEIPT (ABAP_BOOL).
- The class {{P}}CX_CLAIM_ERROR exists in package $TMP and is active. It is a
direct subclass of CX_STATIC_CHECK. Its constructor has the importing
parameter IV_MESSAGE (TYPE string). The method GET_MESSAGE returns this text
(TYPE string).
# 4. Contract
- Create the class {{P}}EXPENSE_VALIDATOR in package $TMP.
- The class is public and final and has a public constructor without parameters.
- The class has exactly one public method:
VALIDATE IMPORTING is_claim TYPE {{p}}if_expense_claim=>ty_claim
RAISING {{p}}cx_claim_error.
- Create these exception classes in package $TMP. Each one inherits from
{{P}}CX_CLAIM_ERROR, is final and has a public constructor.
- {{P}}CX_CLAIM_AMOUNT: constructor parameter IV_AMOUNT of type TY_AMOUNT of
{{P}}IF_EXPENSE_CLAIM. Public read-only attribute MV_AMOUNT of the same
type. Message: Amount must be positive
- {{P}}CX_CLAIM_CURRENCY: constructor parameter IV_CURRENCY of type
TY_CURRENCY. Public read-only attribute MV_CURRENCY of the same type.
Message: Currency is not supported
- {{P}}CX_CLAIM_DATE: constructor parameter IV_CLAIM_DATE of type D. Public
read-only attribute MV_CLAIM_DATE of type D. Message: Claim date is in the
future
- {{P}}CX_CLAIM_RECEIPT: constructor parameter IV_AMOUNT of type TY_AMOUNT.
Public read-only attribute MV_AMOUNT of the same type. Message: Receipt is
required
- {{P}}CX_CLAIM_COSTCENTER: constructor parameter IV_AMOUNT of type TY_AMOUNT.
Public read-only attribute MV_AMOUNT of the same type. Message: Cost center
is required
- GET_MESSAGE returns the message of the exception.
- Do not add other public methods or attributes.
# 5. Business rules
Check the claim in this sequence. Raise the exception of the first rule that
fails. If no rule fails, the method ends without an exception.
1. AMOUNT must be greater than zero. Else raise {{P}}CX_CLAIM_AMOUNT with
IV_AMOUNT = AMOUNT.
2. CURRENCY must be one of EUR, USD, GBP, CHF. Else raise
{{P}}CX_CLAIM_CURRENCY with IV_CURRENCY = CURRENCY.
3. CLAIM_DATE must not be later than the current date. Else raise
{{P}}CX_CLAIM_DATE with IV_CLAIM_DATE = CLAIM_DATE.
4. If AMOUNT is 100.00 or more, HAS_RECEIPT must be true. Else raise
{{P}}CX_CLAIM_RECEIPT with IV_AMOUNT = AMOUNT.
5. If AMOUNT is 1000.00 or more, COST_CENTER must not be initial. Else raise
{{P}}CX_CLAIM_COSTCENTER with IV_AMOUNT = AMOUNT.
# 6. Constraints
- Release target: 8.16.
- Coding standards: Clean ABAP. Method length below 40 statements. No global
variables. No comment that restates the code.
- Out of scope: do not change {{P}}IF_EXPENSE_CLAIM and {{P}}CX_CLAIM_ERROR.
# 7. Acceptance
- All objects are active and have no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for {{P}}EXPENSE_VALIDATOR.

View File

@@ -0,0 +1,107 @@
{
"id": "G0022",
"category": "D",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 45,
"max_activations": 14
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_EXPENSE_CLAIM",
"file": "seed/if_expense_claim.intf.abap",
"description": "Types for an expense claim"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_ERROR",
"file": "seed/cx_claim_error.clas.abap",
"description": "Base exception that carries a message text"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_AMOUNT"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_CURRENCY"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_DATE"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_RECEIPT"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_COSTCENTER"
},
{
"type": "CLAS",
"name": "{{P}}EXPENSE_VALIDATOR"
}
],
"out_of_scope": [
"{{P}}IF_EXPENSE_CLAIM",
"{{P}}CX_CLAIM_ERROR"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}D01_HIDDEN",
"file": "hidden/d01_hidden.clas.abap",
"description": "Hidden tests for the expense claim validation"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_AMOUNT",
"file": "reference/cx_claim_amount.clas.abap",
"description": "Exception for a non positive amount"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_CURRENCY",
"file": "reference/cx_claim_currency.clas.abap",
"description": "Exception for an unsupported currency"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_DATE",
"file": "reference/cx_claim_date.clas.abap",
"description": "Exception for a claim date in the future"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_RECEIPT",
"file": "reference/cx_claim_receipt.clas.abap",
"description": "Exception for a missing receipt"
},
{
"type": "CLAS",
"name": "{{P}}CX_CLAIM_COSTCENTER",
"file": "reference/cx_claim_costcenter.clas.abap",
"description": "Exception for a missing cost center"
},
{
"type": "CLAS",
"name": "{{P}}EXPENSE_VALIDATOR",
"file": "reference/expense_validator.clas.abap",
"description": "Validates an expense claim and raises class-based exceptions",
"testclasses_file": "reference/expense_validator.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"exception_design"
]
}

View File

@@ -0,0 +1,14 @@
{
"id": "G0023",
"pool": "eval",
"object_type": "CLAS",
"category": "E",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,73 @@
CLASS {{p}}e01_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_freight_calc.
METHODS setup.
METHODS assert_cost
IMPORTING iv_zone TYPE {{p}}if_freight_calc=>ty_zone
iv_weight TYPE {{p}}if_freight_calc=>ty_weight
iv_exp TYPE {{p}}if_freight_calc=>ty_amount.
METHODS unknown_zone_is_zero FOR TESTING.
METHODS minimum_applies_after_charge FOR TESTING.
METHODS weight_charge_rounds_half_up FOR TESTING.
METHODS all_zones_are_rated FOR TESTING.
METHODS surcharge_uses_minimum FOR TESTING.
METHODS heavy_discount_on_total FOR TESTING.
METHODS no_discount_at_exactly_1000 FOR TESTING.
METHODS discount_rounds_half_up FOR TESTING.
METHODS small_weight_uses_minimum FOR TESTING.
ENDCLASS.
CLASS {{p}}e01_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}freight_calculator( ).
ENDMETHOD.
METHOD assert_cost.
cl_abap_unit_assert=>assert_equals(
exp = iv_exp
act = mo_cut->calculate( iv_zone = iv_zone iv_weight = iv_weight )
msg = |zone { iv_zone }, weight { iv_weight }| ).
ENDMETHOD.
METHOD unknown_zone_is_zero.
assert_cost( iv_zone = 'XX' iv_weight = '500.000' iv_exp = '0.00' ).
ENDMETHOD.
METHOD minimum_applies_after_charge.
assert_cost( iv_zone = 'DE' iv_weight = '1.000' iv_exp = '26.63' ).
ENDMETHOD.
METHOD weight_charge_rounds_half_up.
assert_cost( iv_zone = 'CN' iv_weight = '20.004' iv_exp = '54.76' ).
ENDMETHOD.
METHOD all_zones_are_rated.
assert_cost( iv_zone = 'DE' iv_weight = '100.000' iv_exp = '103.84' ).
assert_cost( iv_zone = 'FR' iv_weight = '20.000' iv_exp = '39.78' ).
assert_cost( iv_zone = 'NL' iv_weight = '50.000' iv_exp = '50.11' ).
assert_cost( iv_zone = 'US' iv_weight = '50.000' iv_exp = '97.65' ).
assert_cost( iv_zone = 'CN' iv_weight = '100.000' iv_exp = '164.25' ).
ENDMETHOD.
METHOD surcharge_uses_minimum.
assert_cost( iv_zone = 'NL' iv_weight = '0.000' iv_exp = '21.10' ).
ENDMETHOD.
METHOD heavy_discount_on_total.
assert_cost( iv_zone = 'DE' iv_weight = '1500.000' iv_exp = '1302.63' ).
ENDMETHOD.
METHOD no_discount_at_exactly_1000.
assert_cost( iv_zone = 'DE' iv_weight = '1000.000' iv_exp = '918.56' ).
ENDMETHOD.
METHOD discount_rounds_half_up.
assert_cost( iv_zone = 'DE' iv_weight = '1090.070' iv_exp = '950.10' ).
ENDMETHOD.
METHOD small_weight_uses_minimum.
assert_cost( iv_zone = 'FR' iv_weight = '5.000' iv_exp = '32.25' ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,62 @@
CLASS {{p}}freight_calculator DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_freight_calc.
PRIVATE SECTION.
TYPES:
BEGIN OF ty_rate,
zone TYPE {{p}}if_freight_calc=>ty_zone,
base TYPE {{p}}if_freight_calc=>ty_amount,
per_kg TYPE {{p}}if_freight_calc=>ty_amount,
minimum TYPE {{p}}if_freight_calc=>ty_amount,
surcharge TYPE p LENGTH 4 DECIMALS 3,
END OF ty_rate,
tt_rate TYPE SORTED TABLE OF ty_rate WITH UNIQUE KEY zone.
CONSTANTS:
c_heavy_limit TYPE {{p}}if_freight_calc=>ty_weight VALUE '1000.000',
c_discount TYPE p LENGTH 3 DECIMALS 2 VALUE '0.95'.
METHODS rates RETURNING VALUE(rt_rate) TYPE tt_rate.
ENDCLASS.
CLASS {{p}}freight_calculator IMPLEMENTATION.
METHOD {{p}}if_freight_calc~calculate.
DATA(lt_rate) = rates( ).
READ TABLE lt_rate INTO DATA(ls_rate) WITH TABLE KEY zone = iv_zone.
IF sy-subrc <> 0.
rv_cost = 0.
RETURN.
ENDIF.
DATA lv_charge TYPE {{p}}if_freight_calc=>ty_amount.
lv_charge = ls_rate-per_kg * iv_weight.
DATA lv_freight TYPE {{p}}if_freight_calc=>ty_amount.
lv_freight = ls_rate-base + lv_charge.
IF lv_freight < ls_rate-minimum.
lv_freight = ls_rate-minimum.
ENDIF.
DATA lv_surcharge TYPE {{p}}if_freight_calc=>ty_amount.
lv_surcharge = lv_freight * ls_rate-surcharge.
DATA lv_total TYPE {{p}}if_freight_calc=>ty_amount.
lv_total = lv_freight + lv_surcharge.
IF iv_weight > c_heavy_limit.
lv_total = lv_total * c_discount.
ENDIF.
rv_cost = lv_total.
ENDMETHOD.
METHOD rates.
rt_rate = VALUE #(
( zone = 'DE' base = '12.50' per_kg = '0.85' minimum = '25.00' surcharge = '0.065' )
( zone = 'FR' base = '15.00' per_kg = '1.10' minimum = '30.00' surcharge = '0.075' )
( zone = 'NL' base = '10.00' per_kg = '0.75' minimum = '20.00' surcharge = '0.055' )
( zone = 'US' base = '20.00' per_kg = '1.40' minimum = '45.00' surcharge = '0.085' )
( zone = 'CN' base = '25.00' per_kg = '1.25' minimum = '50.00' surcharge = '0.095' ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,75 @@
CLASS ltc_freight_calculator DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_freight_calc.
METHODS setup.
METHODS unknown_zone FOR TESTING.
METHODS minimum_charge FOR TESTING.
METHODS all_zones FOR TESTING.
METHODS heavy_discount FOR TESTING.
METHODS no_discount_at_1000 FOR TESTING.
METHODS rounding_half_up FOR TESTING.
ENDCLASS.
CLASS ltc_freight_calculator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}freight_calculator( ).
ENDMETHOD.
METHOD unknown_zone.
DATA lv_exp TYPE {{p}}if_freight_calc=>ty_amount.
lv_exp = '0.00'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'XX' iv_weight = '10.000' ) ).
ENDMETHOD.
METHOD minimum_charge.
DATA lv_exp TYPE {{p}}if_freight_calc=>ty_amount.
lv_exp = '26.63'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'DE' iv_weight = '1.000' ) ).
ENDMETHOD.
METHOD all_zones.
DATA lv_exp TYPE {{p}}if_freight_calc=>ty_amount.
lv_exp = '103.84'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'DE' iv_weight = '100.000' ) msg = `DE` ).
lv_exp = '39.78'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'FR' iv_weight = '20.000' ) msg = `FR` ).
lv_exp = '164.25'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'CN' iv_weight = '100.000' ) msg = `CN` ).
ENDMETHOD.
METHOD heavy_discount.
DATA lv_exp TYPE {{p}}if_freight_calc=>ty_amount.
lv_exp = '1302.63'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'DE' iv_weight = '1500.000' ) ).
ENDMETHOD.
METHOD no_discount_at_1000.
DATA lv_exp TYPE {{p}}if_freight_calc=>ty_amount.
lv_exp = '918.56'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'DE' iv_weight = '1000.000' ) ).
ENDMETHOD.
METHOD rounding_half_up.
DATA lv_exp TYPE {{p}}if_freight_calc=>ty_amount.
lv_exp = '54.76'.
cl_abap_unit_assert=>assert_equals(
exp = lv_exp
act = mo_cut->calculate( iv_zone = 'CN' iv_weight = '20.004' ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,61 @@
CLASS {{p}}freight_legacy DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_freight_calc.
ENDCLASS.
CLASS {{p}}freight_legacy IMPLEMENTATION.
METHOD {{p}}if_freight_calc~calculate.
DATA: lv_base TYPE {{p}}if_freight_calc=>ty_amount,
lv_per_kg TYPE {{p}}if_freight_calc=>ty_amount,
lv_minimum TYPE {{p}}if_freight_calc=>ty_amount,
lv_surcharge TYPE p LENGTH 4 DECIMALS 3,
lv_charge TYPE {{p}}if_freight_calc=>ty_amount,
lv_freight TYPE {{p}}if_freight_calc=>ty_amount,
lv_fee TYPE {{p}}if_freight_calc=>ty_amount,
lv_total TYPE {{p}}if_freight_calc=>ty_amount.
CASE iv_zone.
WHEN 'DE'.
lv_base = '12.50'.
lv_per_kg = '0.85'.
lv_minimum = '25.00'.
lv_surcharge = '0.065'.
WHEN 'FR'.
lv_base = '15.00'.
lv_per_kg = '1.10'.
lv_minimum = '30.00'.
lv_surcharge = '0.075'.
WHEN 'NL'.
lv_base = '10.00'.
lv_per_kg = '0.75'.
lv_minimum = '20.00'.
lv_surcharge = '0.055'.
WHEN 'US'.
lv_base = '20.00'.
lv_per_kg = '1.40'.
lv_minimum = '45.00'.
lv_surcharge = '0.085'.
WHEN 'CN'.
lv_base = '25.00'.
lv_per_kg = '1.25'.
lv_minimum = '50.00'.
lv_surcharge = '0.095'.
WHEN OTHERS.
rv_cost = 0.
RETURN.
ENDCASE.
lv_charge = lv_per_kg * iv_weight.
lv_freight = lv_base + lv_charge.
IF lv_freight < lv_minimum.
lv_freight = lv_minimum.
ENDIF.
lv_fee = lv_freight * lv_surcharge.
lv_total = lv_freight + lv_fee.
IF iv_weight > '1000.000'.
lv_total = lv_total * '0.95'.
ENDIF.
rv_cost = lv_total.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,11 @@
INTERFACE {{p}}if_freight_calc PUBLIC.
TYPES:
ty_zone TYPE c LENGTH 2,
ty_weight TYPE p LENGTH 9 DECIMALS 3,
ty_amount TYPE p LENGTH 7 DECIMALS 2.
METHODS calculate
IMPORTING iv_zone TYPE ty_zone
iv_weight TYPE ty_weight
RETURNING VALUE(rv_cost) TYPE ty_amount.
ENDINTERFACE.

View File

@@ -0,0 +1,69 @@
# 1. Goal
A freight forwarder calculates the freight cost of a shipment. A legacy class
calculates this cost today. Write the calculation as a clean class. The cost of
every shipment must stay the same.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_FREIGHT_CALC exists in package $TMP and is active.
- The legacy class {{P}}FREIGHT_LEGACY exists in package $TMP and is active. It
implements {{P}}IF_FREIGHT_CALC. Its results are the reference for the new class.
- The interface declares the types TY_ZONE (character, length 2), TY_WEIGHT
(packed number, length 9, 3 decimal places) and TY_AMOUNT (packed number,
length 7, 2 decimal places), and the method CALCULATE.
# 4. Contract
- Create the class {{P}}FREIGHT_CALCULATOR in package $TMP.
- The class is public, final, and has a public constructor without parameters.
- The class implements {{P}}IF_FREIGHT_CALC.
- Do not add other public methods.
# 5. Business rules
All amounts are packed numbers with 2 decimal places. Round an amount to
2 decimal places before you use it in the next step. Round half away from zero.
1. Zone
The zone is a two-character code. These zones exist:
zone base per kg minimum surcharge factor
DE 12.50 0.85 25.00 0.065
FR 15.00 1.10 30.00 0.075
NL 10.00 0.75 20.00 0.055
US 20.00 1.40 45.00 0.085
CN 25.00 1.25 50.00 0.095
If the zone is not one of these, the result is 0.00 and no other rule applies.
2. Weight charge
weight charge = weight * per kg of the zone, rounded to 2 decimal places.
3. Freight
freight = base of the zone + weight charge.
If freight is less than the minimum of the zone, freight is the minimum.
4. Surcharge
surcharge = freight * surcharge factor of the zone, rounded to 2 decimal places.
5. Total
total = freight + surcharge.
6. Heavy shipment
If the weight is greater than 1000.000 kg, total = total * 0.95, rounded to
2 decimal places.
7. The result is total.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
- Coding standards: Clean ABAP. Method length below 40 statements. No global
variables. No comment that restates the code.
- Out of scope: do not change {{P}}IF_FREIGHT_CALC and {{P}}FREIGHT_LEGACY.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,58 @@
{
"id": "G0023",
"category": "E",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_FREIGHT_CALC",
"file": "seed/if_freight_calc.intf.abap",
"description": "Freight calculation contract with packed number types"
},
{
"type": "CLAS",
"name": "{{P}}FREIGHT_LEGACY",
"file": "seed/freight_legacy.clas.abap",
"description": "Legacy freight calculator with packed arithmetic and a CASE over zones"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALCULATOR",
"implements": "{{P}}IF_FREIGHT_CALC"
}
],
"out_of_scope": [
"{{P}}IF_FREIGHT_CALC",
"{{P}}FREIGHT_LEGACY"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}E01_HIDDEN",
"file": "hidden/e01_hidden.clas.abap",
"description": "E01 hidden tests for the refactored freight calculator"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALCULATOR",
"file": "reference/freight_calculator.clas.abap",
"description": "Refactored freight calculator with a rate table",
"testclasses_file": "reference/freight_calculator.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"no_global_variables"
]
}

View File

@@ -0,0 +1,19 @@
{
"id": "G0024",
"pool": "eval",
"object_type": "DDLS",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,89 @@
CLASS {{p}}t16_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CONSTANTS c_from TYPE d VALUE '20240101'.
CONSTANTS c_to TYPE d VALUE '20240131'.
CLASS-DATA go_env TYPE REF TO if_cds_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS row_per_product FOR TESTING.
METHODS sums_quantity FOR TESTING.
METHODS sums_amount FOR TESTING.
METHODS excludes_before_from FOR TESTING.
METHODS excludes_after_to FOR TESTING.
METHODS bounds_inclusive FOR TESTING.
METHODS empty_period FOR TESTING.
ENDCLASS.
CLASS {{p}}t16_hidden IMPLEMENTATION.
METHOD class_setup.
go_env = cl_cds_test_environment=>create( i_for_entity = '{{P}}I_SALES_REPORT' ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
DATA lt_sales TYPE STANDARD TABLE OF {{p}}sales WITH EMPTY KEY.
go_env->clear_doubles( ).
lt_sales = VALUE #(
( doc_id = 'D1' item_no = 1 product = 'MAT_A' quantity = 3 price = '10.00' sold_on = '20240110' )
( doc_id = 'D1' item_no = 2 product = 'MAT_A' quantity = 2 price = '5.00' sold_on = '20240115' )
( doc_id = 'D2' item_no = 1 product = 'MAT_B' quantity = 1 price = '100.00' sold_on = '20240131' )
( doc_id = 'D3' item_no = 1 product = 'MAT_B' quantity = 4 price = '2.50' sold_on = '20231231' )
( doc_id = 'D4' item_no = 1 product = 'MAT_C' quantity = 7 price = '1.00' sold_on = '20240201' ) ).
go_env->insert_test_data( lt_sales ).
ENDMETHOD.
METHOD row_per_product.
SELECT COUNT(*) FROM {{p}}i_sales_report( p_from = @c_from, p_to = @c_to )
INTO @DATA(lv_rows).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_rows ).
ENDMETHOD.
METHOD sums_quantity.
SELECT SINGLE total_qty FROM {{p}}i_sales_report( p_from = @c_from, p_to = @c_to )
WHERE product = 'MAT_A' INTO @DATA(lv_qty).
cl_abap_unit_assert=>assert_equals( exp = 5 act = lv_qty ).
ENDMETHOD.
METHOD sums_amount.
SELECT SINGLE total_amount FROM {{p}}i_sales_report( p_from = @c_from, p_to = @c_to )
WHERE product = 'MAT_A' INTO @DATA(lv_amount).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '40.00' )
act = CONV decfloat34( lv_amount ) ).
ENDMETHOD.
METHOD excludes_before_from.
SELECT SINGLE total_qty FROM {{p}}i_sales_report( p_from = @c_from, p_to = @c_to )
WHERE product = 'MAT_B' INTO @DATA(lv_qty).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_qty ).
ENDMETHOD.
METHOD excludes_after_to.
SELECT COUNT(*) FROM {{p}}i_sales_report( p_from = @c_from, p_to = @c_to )
WHERE product = 'MAT_C' INTO @DATA(lv_rows).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_rows ).
ENDMETHOD.
METHOD bounds_inclusive.
DATA lv_day TYPE d.
lv_day = '20240131'.
SELECT SINGLE total_qty FROM {{p}}i_sales_report( p_from = @lv_day, p_to = @lv_day )
WHERE product = 'MAT_B' INTO @DATA(lv_qty).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_qty ).
ENDMETHOD.
METHOD empty_period.
DATA lv_from TYPE d.
DATA lv_to TYPE d.
lv_from = '20230101'.
lv_to = '20230131'.
SELECT COUNT(*) FROM {{p}}i_sales_report( p_from = @lv_from, p_to = @lv_to )
INTO @DATA(lv_rows).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_rows ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,15 @@
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales per product in a period'
define view entity {{P}}I_SALES_REPORT
with parameters
p_from : abap.dats,
p_to : abap.dats
as select from {{p}}sales as sale
{
key sale.product as product,
sum( sale.quantity ) as total_qty,
sum( cast( sale.quantity as abap.dec( 15, 2 ) ) * sale.price ) as total_amount
}
where sale.sold_on >= $parameters.p_from
and sale.sold_on <= $parameters.p_to
group by sale.product

View File

@@ -0,0 +1,44 @@
CLASS {{p}}t16_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA go_env TYPE REF TO if_cds_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS period_is_inclusive FOR TESTING.
ENDCLASS.
CLASS {{p}}t16_test IMPLEMENTATION.
METHOD class_setup.
go_env = cl_cds_test_environment=>create( i_for_entity = '{{P}}I_SALES_REPORT' ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
DATA lt_sales TYPE STANDARD TABLE OF {{p}}sales WITH EMPTY KEY.
go_env->clear_doubles( ).
lt_sales = VALUE #(
( doc_id = 'S1' item_no = 1 product = 'P1' quantity = 2 price = '1.50' sold_on = '20240501' )
( doc_id = 'S1' item_no = 2 product = 'P1' quantity = 1 price = '4.00' sold_on = '20240531' )
( doc_id = 'S2' item_no = 1 product = 'P2' quantity = 9 price = '0.10' sold_on = '20240601' ) ).
go_env->insert_test_data( lt_sales ).
ENDMETHOD.
METHOD period_is_inclusive.
DATA lv_from TYPE d.
DATA lv_to TYPE d.
lv_from = '20240501'.
lv_to = '20240531'.
SELECT SINGLE total_qty, total_amount
FROM {{p}}i_sales_report( p_from = @lv_from, p_to = @lv_to )
WHERE product = 'P1'
INTO @DATA(ls_result).
cl_abap_unit_assert=>assert_equals( exp = 3 act = ls_result-total_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '7.00' )
act = CONV decfloat34( ls_result-total_amount ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,14 @@
@EndUserText.label : 'Sales line'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}sales {
key client : abap.clnt not null;
key doc_id : abap.char(10) not null;
key item_no : abap.int4 not null;
product : abap.char(10);
quantity : abap.int4;
price : abap.dec(15,2);
sold_on : abap.dats;
}

View File

@@ -0,0 +1,22 @@
CLASS {{p}}seed_sales DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS {{p}}seed_sales IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA lt_sales TYPE STANDARD TABLE OF {{p}}sales WITH EMPTY KEY.
lt_sales = VALUE #(
( doc_id = 'D1' item_no = 1 product = 'MAT_A' quantity = 3 price = '10.00' sold_on = '20240110' )
( doc_id = 'D1' item_no = 2 product = 'MAT_A' quantity = 2 price = '5.00' sold_on = '20240115' )
( doc_id = 'D2' item_no = 1 product = 'MAT_B' quantity = 1 price = '100.00' sold_on = '20240131' )
( doc_id = 'D3' item_no = 1 product = 'MAT_B' quantity = 4 price = '2.50' sold_on = '20231231' )
( doc_id = 'D4' item_no = 1 product = 'MAT_C' quantity = 7 price = '1.00' sold_on = '20240201' ) ).
DELETE FROM {{p}}sales.
INSERT {{p}}sales FROM TABLE @lt_sales.
out->write( |Seed rows: { lines( lt_sales ) }| ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,40 @@
# 1. Goal
Give the sales figures of each product for a period. The period is an input of the
view. A report and an OData service read the figures, so the calculation must be in
the database, in a CDS view.
# 2. Open questions
None.
# 3. Context
- Table {{P}}SALES (sales line), package $TMP. Key: DOC_ID (CHAR 10), ITEM_NO (INT4).
Fields: PRODUCT (CHAR 10), QUANTITY (INT4), PRICE (DEC 15,2, the price of one
unit), SOLD_ON (DATS, the date of the sale).
- The class {{P}}SEED_SALES fills {{P}}SALES with sample data. Run it if you want to
test the view manually.
# 4. Contract
- Create the CDS view entity {{P}}I_SALES_REPORT in package $TMP.
- Input parameters, with these names and types: P_FROM (DATS), P_TO (DATS).
The parameters have no default value.
- Elements, with these names: PRODUCT (key), TOTAL_QTY, TOTAL_AMOUNT.
- No authorization check (#NOT_REQUIRED).
# 5. Business rules
1. A sales line is in the period when SOLD_ON is not before P_FROM and not after
P_TO. P_FROM and P_TO are part of the period.
2. Show one row for each product with at least one sales line in the period. Do not
show a product without a sales line in the period.
3. TOTAL_QTY is the sum of QUANTITY of all sales lines of the product in the period.
4. TOTAL_AMOUNT is the sum of QUANTITY multiplied by PRICE of all sales lines of the
product in the period, with 2 decimals.
# 6. Constraints
- Release target: 7.58.
- Coding standards: Clean ABAP.
- Out of scope: do not change {{P}}SALES and {{P}}SEED_SALES.
# 7. Acceptance
- The view is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests with CL_CDS_TEST_ENVIRONMENT in a global test class.

View File

@@ -0,0 +1,75 @@
{
"id": "G0024",
"category": "B",
"object_type": "DDLS",
"difficulty": 2,
"release_target": "v758",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "TABL",
"name": "{{P}}SALES",
"file": "seed/sales.tabl.asabap",
"description": "Sales line"
},
{
"type": "CLAS",
"name": "{{P}}SEED_SALES",
"file": "seed/seed_sales.clas.abap",
"run": true,
"description": "Seed data for {{P}}SALES"
}
],
"contract": [
{
"type": "DDLS",
"name": "{{P}}I_SALES_REPORT",
"params": [
{
"name": "P_FROM",
"type": "abap.dats"
},
{
"name": "P_TO",
"type": "abap.dats"
}
],
"fields": [
"PRODUCT",
"TOTAL_QTY",
"TOTAL_AMOUNT"
]
}
],
"out_of_scope": [
"{{P}}SALES",
"{{P}}SEED_SALES"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T16_HIDDEN",
"file": "hidden/t16_hidden.clas.abap",
"description": "T16 hidden tests"
}
],
"reference": [
{
"type": "DDLS",
"name": "{{P}}I_SALES_REPORT",
"file": "reference/i_sales_report.ddls.asddls",
"description": "Sales figures per product in a period"
},
{
"type": "CLAS",
"name": "{{P}}T16_TEST",
"file": "reference/t16_test.clas.abap",
"description": "T16 own tests"
}
],
"craft_checks": []
}

View File

@@ -0,0 +1,19 @@
{
"id": "G0025",
"pool": "eval",
"object_type": "DDLS",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,88 @@
CLASS {{p}}t16_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA go_env TYPE REF TO if_cds_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS source_a_rows FOR TESTING.
METHODS source_b_rows FOR TESTING.
METHODS all_partners FOR TESTING.
METHODS same_id_twice FOR TESTING.
METHODS name_and_city FOR TESTING.
METHODS only_in_a FOR TESTING.
METHODS only_in_b FOR TESTING.
ENDCLASS.
CLASS {{p}}t16_hidden IMPLEMENTATION.
METHOD class_setup.
go_env = cl_cds_test_environment=>create( i_for_entity = '{{P}}I_PARTNER_ALL' ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
DATA lt_a TYPE STANDARD TABLE OF {{p}}part_a WITH EMPTY KEY.
DATA lt_b TYPE STANDARD TABLE OF {{p}}part_b WITH EMPTY KEY.
go_env->clear_doubles( ).
lt_a = VALUE #( ( partner_id = 'P1' name = 'Alpha GmbH' city = 'Berlin' )
( partner_id = 'P2' name = 'Beta AG' city = 'Hamburg' ) ).
lt_b = VALUE #( ( partner_id = 'P2' name = 'Beta AG' city = 'Hamburg' )
( partner_id = 'P3' name = 'Gamma KG' city = 'Munich' ) ).
go_env->insert_test_data( lt_a ).
go_env->insert_test_data( lt_b ).
ENDMETHOD.
METHOD source_a_rows.
SELECT partner_id, source FROM {{p}}i_partner_all
WHERE source = 'A' ORDER BY partner_id INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_rows ) ).
cl_abap_unit_assert=>assert_equals( exp = 'P1' act = lt_rows[ 1 ]-partner_id ).
cl_abap_unit_assert=>assert_equals( exp = 'P2' act = lt_rows[ 2 ]-partner_id ).
ENDMETHOD.
METHOD source_b_rows.
SELECT partner_id, source FROM {{p}}i_partner_all
WHERE source = 'B' ORDER BY partner_id INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_rows ) ).
cl_abap_unit_assert=>assert_equals( exp = 'P2' act = lt_rows[ 1 ]-partner_id ).
cl_abap_unit_assert=>assert_equals( exp = 'P3' act = lt_rows[ 2 ]-partner_id ).
ENDMETHOD.
METHOD all_partners.
SELECT COUNT(*) FROM {{p}}i_partner_all INTO @DATA(lv_count).
cl_abap_unit_assert=>assert_equals( exp = 4 act = lv_count ).
ENDMETHOD.
METHOD same_id_twice.
SELECT source FROM {{p}}i_partner_all
WHERE partner_id = 'P2' ORDER BY source INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_rows ) ).
cl_abap_unit_assert=>assert_equals( exp = 'A' act = lt_rows[ 1 ]-source ).
cl_abap_unit_assert=>assert_equals( exp = 'B' act = lt_rows[ 2 ]-source ).
ENDMETHOD.
METHOD name_and_city.
SELECT SINGLE name, city FROM {{p}}i_partner_all
WHERE partner_id = 'P3' AND source = 'B' INTO @DATA(ls_row).
cl_abap_unit_assert=>assert_equals( exp = 'Gamma KG' act = ls_row-name ).
cl_abap_unit_assert=>assert_equals( exp = 'Munich' act = ls_row-city ).
ENDMETHOD.
METHOD only_in_a.
SELECT source FROM {{p}}i_partner_all
WHERE partner_id = 'P1' INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_rows ) ).
cl_abap_unit_assert=>assert_equals( exp = 'A' act = lt_rows[ 1 ]-source ).
ENDMETHOD.
METHOD only_in_b.
SELECT source FROM {{p}}i_partner_all
WHERE partner_id = 'P3' INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_rows ) ).
cl_abap_unit_assert=>assert_equals( exp = 'B' act = lt_rows[ 1 ]-source ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,19 @@
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'All partners'
@Metadata.ignorePropagatedAnnotations: true
define view entity {{P}}I_PARTNER_ALL
as select from {{p}}part_a as a
{
key a.partner_id as partner_id,
a.name as name,
a.city as city,
cast('A' as abap.char(1)) as source
}
union all
select from {{p}}part_b as b
{
key b.partner_id as partner_id,
b.name as name,
b.city as city,
cast('B' as abap.char(1)) as source
}

View File

@@ -0,0 +1,45 @@
CLASS {{p}}t16_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA go_env TYPE REF TO if_cds_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS both_sources FOR TESTING.
METHODS same_partner_twice FOR TESTING.
ENDCLASS.
CLASS {{p}}t16_test IMPLEMENTATION.
METHOD class_setup.
go_env = cl_cds_test_environment=>create( i_for_entity = '{{P}}I_PARTNER_ALL' ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD setup.
DATA lt_a TYPE STANDARD TABLE OF {{p}}part_a WITH EMPTY KEY.
DATA lt_b TYPE STANDARD TABLE OF {{p}}part_b WITH EMPTY KEY.
go_env->clear_doubles( ).
lt_a = VALUE #( ( partner_id = 'A1' name = 'One' city = 'Bonn' ) ).
lt_b = VALUE #( ( partner_id = 'A1' name = 'One' city = 'Bonn' )
( partner_id = 'B1' name = 'Two' city = 'Kiel' ) ).
go_env->insert_test_data( lt_a ).
go_env->insert_test_data( lt_b ).
ENDMETHOD.
METHOD both_sources.
SELECT source FROM {{p}}i_partner_all ORDER BY source INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 3 act = lines( lt_rows ) ).
ENDMETHOD.
METHOD same_partner_twice.
SELECT source FROM {{p}}i_partner_all
WHERE partner_id = 'A1' ORDER BY source INTO TABLE @DATA(lt_rows).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_rows ) ).
cl_abap_unit_assert=>assert_equals( exp = 'A' act = lt_rows[ 1 ]-source ).
cl_abap_unit_assert=>assert_equals( exp = 'B' act = lt_rows[ 2 ]-source ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,11 @@
@EndUserText.label : 'Partner list A'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}part_a {
key client : abap.clnt not null;
key partner_id : abap.char(10) not null;
name : abap.char(40);
city : abap.char(40);
}

View File

@@ -0,0 +1,11 @@
@EndUserText.label : 'Partner list B'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{p}}part_b {
key client : abap.clnt not null;
key partner_id : abap.char(10) not null;
name : abap.char(40);
city : abap.char(40);
}

View File

@@ -0,0 +1,24 @@
CLASS {{p}}seed_partner DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS {{p}}seed_partner IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA lt_a TYPE STANDARD TABLE OF {{p}}part_a WITH EMPTY KEY.
DATA lt_b TYPE STANDARD TABLE OF {{p}}part_b WITH EMPTY KEY.
lt_a = VALUE #( ( client = sy-mandt partner_id = 'P1' name = 'Alpha GmbH' city = 'Berlin' )
( client = sy-mandt partner_id = 'P2' name = 'Beta AG' city = 'Hamburg' ) ).
lt_b = VALUE #( ( client = sy-mandt partner_id = 'P2' name = 'Beta AG' city = 'Hamburg' )
( client = sy-mandt partner_id = 'P3' name = 'Gamma KG' city = 'Munich' ) ).
MODIFY {{p}}part_a FROM TABLE @lt_a.
MODIFY {{p}}part_b FROM TABLE @lt_b.
out->write( 'Partner seed data written.' ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,37 @@
# 1. Goal
Two tables hold partner lists. A report and an OData service must read one
single list of all partners. Each row must show from which table it comes.
# 2. Open questions
None.
# 3. Context
- Table {{P}}PART_A (partner list A), package $TMP. Key: PARTNER_ID (CHAR 10).
Fields: NAME (CHAR 40), CITY (CHAR 40).
- Table {{P}}PART_B (partner list B), package $TMP. It has the same structure.
- A partner ID can be in both tables.
# 4. Contract
- Create the CDS view entity {{P}}I_PARTNER_ALL in package $TMP.
- Elements, with these names: PARTNER_ID (key), NAME, CITY, SOURCE.
- SOURCE is one character.
- No authorization check (#NOT_REQUIRED).
# 5. Business rules
1. The view shows every partner of {{P}}PART_A and every partner of
{{P}}PART_B. No partner is missing.
2. SOURCE is 'A' for a row from {{P}}PART_A. SOURCE is 'B' for a row from
{{P}}PART_B.
3. NAME and CITY are the values of the table that the row comes from.
4. If the same PARTNER_ID is in both tables, the view shows two rows for this
partner. One row has SOURCE 'A'. The other row has SOURCE 'B'.
5. A partner that is only in one table is shown one time.
# 6. Constraints
- Release target: 8.16.
- Out of scope: do not change {{P}}PART_A, {{P}}PART_B and {{P}}SEED_PARTNER.
# 7. Acceptance
- The view is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests with CL_CDS_TEST_ENVIRONMENT in a global test class.

View File

@@ -0,0 +1,73 @@
{
"id": "G0025",
"category": "B",
"object_type": "DDLS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "TABL",
"name": "{{P}}PART_A",
"file": "seed/part_a.tabl.asabap",
"description": "Partner list A"
},
{
"type": "TABL",
"name": "{{P}}PART_B",
"file": "seed/part_b.tabl.asabap",
"description": "Partner list B"
},
{
"type": "CLAS",
"name": "{{P}}SEED_PARTNER",
"file": "seed/seed_partner.clas.abap",
"description": "Fills both partner tables",
"run": true
}
],
"contract": [
{
"type": "DDLS",
"name": "{{P}}I_PARTNER_ALL",
"fields": [
"PARTNER_ID",
"NAME",
"CITY",
"SOURCE"
]
}
],
"out_of_scope": [
"{{P}}PART_A",
"{{P}}PART_B",
"{{P}}SEED_PARTNER"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T16_HIDDEN",
"file": "hidden/t16_hidden.clas.abap",
"description": "T16 hidden tests"
}
],
"reference": [
{
"type": "DDLS",
"name": "{{P}}I_PARTNER_ALL",
"file": "reference/i_partner_all.ddls.asddls",
"description": "All partners of both lists with a source element"
},
{
"type": "CLAS",
"name": "{{P}}T16_TEST",
"file": "reference/t16_test.clas.abap",
"description": "T16 own tests"
}
],
"craft_checks": []
}

View File

@@ -0,0 +1,37 @@
{
"id": "G0026",
"pool": "eval",
"object_type": "FUNC",
"category": "A",
"attempts": [
{
"stage": "bundle",
"errors": [
"reference/t14_test.clas.abap line 81: syntax error for release v758 (abaplint): Statement does not exist in the configured ABAP version(or a parser error), \"cl_abap_unit_assert\""
],
"by_check": {
"abaplint": 1
}
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "bundle",
"errors": [
"reference/t14_test.clas.abap line 81: syntax error for release v758 (abaplint): Statement does not exist in the configured ABAP version(or a parser error), \"cl_abap_unit_assert\""
],
"by_check": {
"abaplint": 1
}
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,149 @@
CLASS {{p}}t14_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES: BEGIN OF ty_result,
time_fee TYPE decfloat34,
distance_fee TYPE decfloat34,
surcharge TYPE decfloat34,
discount TYPE decfloat34,
total TYPE decfloat34,
END OF ty_result.
METHODS price
IMPORTING iv_class TYPE string
iv_minutes TYPE i
iv_km TYPE i
iv_member TYPE abap_bool DEFAULT abap_false
iv_weekend TYPE abap_bool DEFAULT abap_false
EXPORTING es_result TYPE ty_result
ev_subrc TYPE sysubrc.
METHODS time_fee_blocks FOR TESTING.
METHODS distance_fee_free_km FOR TESTING.
METHODS weekend_surcharge FOR TESTING.
METHODS weekday_no_surcharge FOR TESTING.
METHODS member_discount FOR TESTING.
METHODS minimum_total FOR TESTING.
METHODS minimum_total_member FOR TESTING.
METHODS invalid_duration FOR TESTING.
METHODS invalid_distance FOR TESTING.
METHODS invalid_class FOR TESTING.
ENDCLASS.
CLASS {{p}}t14_hidden IMPLEMENTATION.
METHOD price.
CLEAR es_result.
CALL FUNCTION '{{P}}RENT_PRICE'
EXPORTING
iv_vehicle_class = iv_class
iv_minutes = iv_minutes
iv_km = iv_km
iv_member = iv_member
iv_weekend = iv_weekend
IMPORTING
ev_time_fee = es_result-time_fee
ev_distance_fee = es_result-distance_fee
ev_surcharge = es_result-surcharge
ev_discount = es_result-discount
ev_total = es_result-total
EXCEPTIONS
invalid_duration = 1
invalid_distance = 2
invalid_class = 3
OTHERS = 4.
ev_subrc = sy-subrc.
ENDMETHOD.
METHOD time_fee_blocks.
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = 0
IMPORTING es_result = DATA(ls_15) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 3 ) act = ls_15-time_fee ).
price( EXPORTING iv_class = `S` iv_minutes = 16 iv_km = 0
IMPORTING es_result = DATA(ls_16) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 6 ) act = ls_16-time_fee ).
price( EXPORTING iv_class = `L` iv_minutes = 61 iv_km = 0
IMPORTING es_result = DATA(ls_61) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 40 ) act = ls_61-time_fee ).
ENDMETHOD.
METHOD distance_fee_free_km.
price( EXPORTING iv_class = `L` iv_minutes = 60 iv_km = 10
IMPORTING es_result = DATA(ls_free) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls_free-distance_fee ).
price( EXPORTING iv_class = `L` iv_minutes = 60 iv_km = 14
IMPORTING es_result = DATA(ls_paid) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 1 ) act = ls_paid-distance_fee ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 33 ) act = ls_paid-total ).
ENDMETHOD.
METHOD weekend_surcharge.
price( EXPORTING iv_class = `M` iv_minutes = 30 iv_km = 0 iv_weekend = abap_true
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '2.50' ) act = ls-surcharge ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '12.50' ) act = ls-total ).
ENDMETHOD.
METHOD weekday_no_surcharge.
price( EXPORTING iv_class = `M` iv_minutes = 30 iv_km = 0
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls-surcharge ).
ENDMETHOD.
METHOD member_discount.
" 6.50 * 15 % = 0.975 -> 0.98
price( EXPORTING iv_class = `M` iv_minutes = 15 iv_km = 11
iv_weekend = abap_true iv_member = abap_true
IMPORTING es_result = DATA(ls_half) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '0.98' ) act = ls_half-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '5.52' ) act = ls_half-total ).
" 10.75 * 15 % = 1.6125 -> 1.61
price( EXPORTING iv_class = `M` iv_minutes = 30 iv_km = 13 iv_member = abap_true
IMPORTING es_result = DATA(ls_round) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '1.61' ) act = ls_round-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '9.14' ) act = ls_round-total ).
" 7.50 * 15 % = 1.125 -> 1.13
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = 28 iv_member = abap_true
IMPORTING es_result = DATA(ls_up) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '1.13' ) act = ls_up-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '6.37' ) act = ls_up-total ).
ENDMETHOD.
METHOD minimum_total.
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = 0
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 3 ) act = ls-time_fee ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 5 ) act = ls-total ).
ENDMETHOD.
METHOD minimum_total_member.
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = 0 iv_member = abap_true
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '0.45' ) act = ls-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 5 ) act = ls-total ).
ENDMETHOD.
METHOD invalid_duration.
price( EXPORTING iv_class = `S` iv_minutes = 0 iv_km = 0
IMPORTING ev_subrc = DATA(lv_subrc) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_subrc ).
ENDMETHOD.
METHOD invalid_distance.
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = -1
IMPORTING ev_subrc = DATA(lv_subrc) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_subrc ).
ENDMETHOD.
METHOD invalid_class.
price( EXPORTING iv_class = `X` iv_minutes = 15 iv_km = 0
IMPORTING ev_subrc = DATA(lv_subrc) ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = lv_subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,158 @@
CLASS {{p}}cl_rent DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES ty_class TYPE string.
TYPES ty_money TYPE decfloat34.
TYPES ty_err_code TYPE c LENGTH 1.
CONSTANTS: BEGIN OF c_err,
ok TYPE ty_err_code VALUE ' ',
duration TYPE ty_err_code VALUE 'D',
distance TYPE ty_err_code VALUE 'K',
bad_class TYPE ty_err_code VALUE 'C',
END OF c_err.
TYPES: BEGIN OF ty_request,
vehicle_class TYPE ty_class,
minutes TYPE i,
km TYPE i,
member TYPE abap_bool,
weekend TYPE abap_bool,
END OF ty_request.
TYPES: BEGIN OF ty_result,
err_code TYPE ty_err_code,
time_fee TYPE ty_money,
distance_fee TYPE ty_money,
surcharge TYPE ty_money,
discount TYPE ty_money,
total TYPE ty_money,
END OF ty_result.
CLASS-METHODS calculate
IMPORTING is_request TYPE ty_request
RETURNING VALUE(rs_result) TYPE ty_result.
PRIVATE SECTION.
CONSTANTS: BEGIN OF c_block_price,
small TYPE ty_money VALUE '3.00',
medium TYPE ty_money VALUE '5.00',
large TYPE ty_money VALUE '8.00',
END OF c_block_price.
CONSTANTS c_block_minutes TYPE i VALUE 15.
CONSTANTS c_free_km TYPE i VALUE 10.
CONSTANTS c_km_price TYPE ty_money VALUE '0.25'.
CONSTANTS c_weekend_share TYPE ty_money VALUE '0.25'.
CONSTANTS c_member_share TYPE ty_money VALUE '0.15'.
CONSTANTS c_min_total TYPE ty_money VALUE '5.00'.
CONSTANTS c_decimals TYPE i VALUE 2.
CLASS-METHODS validate
IMPORTING is_request TYPE ty_request
RETURNING VALUE(rv_err_code) TYPE ty_err_code.
CLASS-METHODS time_fee
IMPORTING is_request TYPE ty_request
RETURNING VALUE(rv_fee) TYPE ty_money.
CLASS-METHODS distance_fee
IMPORTING is_request TYPE ty_request
RETURNING VALUE(rv_fee) TYPE ty_money.
CLASS-METHODS block_price
IMPORTING is_request TYPE ty_request
RETURNING VALUE(rv_price) TYPE ty_money.
CLASS-METHODS block_count
IMPORTING iv_minutes TYPE i
RETURNING VALUE(rv_blocks) TYPE i.
CLASS-METHODS round_money
IMPORTING iv_value TYPE ty_money
RETURNING VALUE(rv_value) TYPE ty_money.
ENDCLASS.
CLASS {{p}}cl_rent IMPLEMENTATION.
METHOD calculate.
rs_result-err_code = validate( is_request ).
IF rs_result-err_code <> c_err-ok.
RETURN.
ENDIF.
rs_result-time_fee = time_fee( is_request ).
rs_result-distance_fee = distance_fee( is_request ).
IF is_request-weekend = abap_true.
rs_result-surcharge = round_money( rs_result-time_fee * c_weekend_share ).
ELSE.
rs_result-surcharge = 0.
ENDIF.
DATA(lv_gross) = rs_result-time_fee + rs_result-distance_fee + rs_result-surcharge.
IF is_request-member = abap_true.
rs_result-discount = round_money( lv_gross * c_member_share ).
ELSE.
rs_result-discount = 0.
ENDIF.
rs_result-total = round_money( lv_gross - rs_result-discount ).
IF rs_result-total < c_min_total.
rs_result-total = c_min_total.
ENDIF.
ENDMETHOD.
METHOD validate.
IF is_request-minutes < 1.
rv_err_code = c_err-duration.
ELSEIF is_request-km < 0.
rv_err_code = c_err-distance.
ELSEIF is_request-vehicle_class <> 'S'
AND is_request-vehicle_class <> 'M'
AND is_request-vehicle_class <> 'L'.
rv_err_code = c_err-bad_class.
ELSE.
rv_err_code = c_err-ok.
ENDIF.
ENDMETHOD.
METHOD time_fee.
rv_fee = block_count( is_request-minutes ) * block_price( is_request ).
ENDMETHOD.
METHOD distance_fee.
DATA(lv_chargeable_km) = is_request-km - c_free_km.
IF lv_chargeable_km > 0.
rv_fee = lv_chargeable_km * c_km_price.
ELSE.
rv_fee = 0.
ENDIF.
ENDMETHOD.
METHOD block_price.
CASE is_request-vehicle_class.
WHEN 'S'.
rv_price = c_block_price-small.
WHEN 'M'.
rv_price = c_block_price-medium.
WHEN 'L'.
rv_price = c_block_price-large.
WHEN OTHERS.
rv_price = 0.
ENDCASE.
ENDMETHOD.
METHOD block_count.
rv_blocks = ( iv_minutes + c_block_minutes - 1 ) DIV c_block_minutes.
ENDMETHOD.
METHOD round_money.
rv_value = round( val = iv_value
dec = c_decimals
mode = cl_abap_math=>round_half_up ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,87 @@
CLASS {{p}}lcl_rent_test DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS FINAL.
PRIVATE SECTION.
METHODS price_for
IMPORTING iv_class TYPE string
iv_minutes TYPE i
iv_km TYPE i
iv_member TYPE abap_bool DEFAULT abap_false
iv_weekend TYPE abap_bool DEFAULT abap_false
RETURNING VALUE(rs_result) TYPE {{p}}cl_rent=>ty_result.
METHODS time_fee_blocks FOR TESTING.
METHODS distance_fee_free_km FOR TESTING.
METHODS weekend_surcharge FOR TESTING.
METHODS member_discount FOR TESTING.
METHODS minimum_total FOR TESTING.
METHODS invalid_input FOR TESTING.
ENDCLASS.
CLASS {{p}}lcl_rent_test IMPLEMENTATION.
METHOD price_for.
DATA(ls_request) = VALUE {{p}}cl_rent=>ty_request(
vehicle_class = iv_class
minutes = iv_minutes
km = iv_km
member = iv_member
weekend = iv_weekend ).
rs_result = {{p}}cl_rent=>calculate( ls_request ).
ENDMETHOD.
METHOD time_fee_blocks.
DATA(ls_15) = price_for( iv_class = `S` iv_minutes = 15 iv_km = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 3 ) act = ls_15-time_fee ).
DATA(ls_16) = price_for( iv_class = `S` iv_minutes = 16 iv_km = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 6 ) act = ls_16-time_fee ).
DATA(ls_61) = price_for( iv_class = `L` iv_minutes = 61 iv_km = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 40 ) act = ls_61-time_fee ).
ENDMETHOD.
METHOD distance_fee_free_km.
DATA(ls_free) = price_for( iv_class = `L` iv_minutes = 60 iv_km = 10 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls_free-distance_fee ).
DATA(ls_paid) = price_for( iv_class = `L` iv_minutes = 60 iv_km = 14 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 1 ) act = ls_paid-distance_fee ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 33 ) act = ls_paid-total ).
ENDMETHOD.
METHOD weekend_surcharge.
DATA(ls) = price_for( iv_class = `M` iv_minutes = 30 iv_km = 0 iv_weekend = abap_true ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '2.50' ) act = ls-surcharge ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '12.50' ) act = ls-total ).
ENDMETHOD.
METHOD member_discount.
DATA(ls_half) = price_for( iv_class = `M` iv_minutes = 15 iv_km = 11
iv_weekend = abap_true iv_member = abap_true ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '0.98' ) act = ls_half-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '5.52' ) act = ls_half-total ).
DATA(ls_up) = price_for( iv_class = `S` iv_minutes = 15 iv_km = 28 iv_member = abap_true ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '1.13' ) act = ls_up-discount ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '6.37' ) act = ls_up-total ).
ENDMETHOD.
METHOD minimum_total.
DATA(ls) = price_for( iv_class = `S` iv_minutes = 15 iv_km = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 3 ) act = ls-time_fee ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 5 ) act = ls-total ).
ENDMETHOD.
METHOD invalid_input.
cl_abap_unit_assert=>assert_equals(
exp = {{p}}cl_rent=>c_err-duration
act = price_for( iv_class = `S` iv_minutes = 0 iv_km = 0 )-err_code ).
cl_abap_unit_assert=>assert_equals(
exp = {{p}}cl_rent=>c_err-distance
act = price_for( iv_class = `S` iv_minutes = 15 iv_km = -1 )-err_code ).
cl_abap_unit_assert=>assert_equals(
exp = {{p}}cl_rent=>c_err-bad_class
act = price_for( iv_class = `X` iv_minutes = 15 iv_km = 0 )-err_code ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,30 @@
CLASS {{p}}cx_rent_invalid DEFINITION
PUBLIC
INHERITING FROM cx_static_check
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING textid LIKE textid OPTIONAL
previous LIKE previous OPTIONAL
iv_reason TYPE string OPTIONAL.
METHODS get_reason
RETURNING VALUE(rv_reason) TYPE string.
PRIVATE SECTION.
DATA mv_reason TYPE string.
ENDCLASS.
CLASS {{p}}cx_rent_invalid IMPLEMENTATION.
METHOD constructor.
super->constructor( textid = textid previous = previous ).
mv_reason = iv_reason.
ENDMETHOD.
METHOD get_reason.
rv_reason = mv_reason.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,43 @@
FUNCTION {{p}}rent_price
IMPORTING
iv_vehicle_class TYPE string
iv_minutes TYPE i
iv_km TYPE i
iv_member TYPE abap_bool
iv_weekend TYPE abap_bool
EXPORTING
ev_time_fee TYPE decfloat34
ev_distance_fee TYPE decfloat34
ev_surcharge TYPE decfloat34
ev_discount TYPE decfloat34
ev_total TYPE decfloat34
EXCEPTIONS
invalid_duration
invalid_distance
invalid_class.
DATA ls_request TYPE {{p}}cl_rent=>ty_request.
ls_request-vehicle_class = iv_vehicle_class.
ls_request-minutes = iv_minutes.
ls_request-km = iv_km.
ls_request-member = iv_member.
ls_request-weekend = iv_weekend.
DATA(ls_result) = {{p}}cl_rent=>calculate( ls_request ).
CASE ls_result-err_code.
WHEN {{p}}cl_rent=>c_err-duration.
RAISE invalid_duration.
WHEN {{p}}cl_rent=>c_err-distance.
RAISE invalid_distance.
WHEN {{p}}cl_rent=>c_err-bad_class.
RAISE invalid_class.
WHEN OTHERS.
ev_time_fee = ls_result-time_fee.
ev_distance_fee = ls_result-distance_fee.
ev_surcharge = ls_result-surcharge.
ev_discount = ls_result-discount.
ev_total = ls_result-total.
ENDCASE.
ENDFUNCTION.

View File

@@ -0,0 +1,90 @@
CLASS {{p}}t14_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES: BEGIN OF ty_result,
time_fee TYPE decfloat34,
distance_fee TYPE decfloat34,
surcharge TYPE decfloat34,
discount TYPE decfloat34,
total TYPE decfloat34,
END OF ty_result.
METHODS price
IMPORTING iv_class TYPE string
iv_minutes TYPE i
iv_km TYPE i
iv_member TYPE abap_bool DEFAULT abap_false
iv_weekend TYPE abap_bool DEFAULT abap_false
EXPORTING es_result TYPE ty_result
ev_subrc TYPE sysubrc.
METHODS time_fee_blocks FOR TESTING.
METHODS distance_fee_free_km FOR TESTING.
METHODS weekend_surcharge FOR TESTING.
METHODS member_discount FOR TESTING.
METHODS minimum_total FOR TESTING.
METHODS invalid_duration FOR TESTING.
ENDCLASS.
CLASS {{p}}t14_test IMPLEMENTATION.
METHOD price.
CLEAR es_result.
CALL FUNCTION '{{P}}RENT_PRICE'
EXPORTING
iv_vehicle_class = iv_class
iv_minutes = iv_minutes
iv_km = iv_km
iv_member = iv_member
iv_weekend = iv_weekend
IMPORTING
ev_time_fee = es_result-time_fee
ev_distance_fee = es_result-distance_fee
ev_surcharge = es_result-surcharge
ev_discount = es_result-discount
ev_total = es_result-total
EXCEPTIONS
invalid_duration = 1
invalid_distance = 2
invalid_class = 3
OTHERS = 4.
ev_subrc = sy-subrc.
ENDMETHOD.
METHOD time_fee_blocks.
price( EXPORTING iv_class = `S` iv_minutes = 16 iv_km = 0
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 6 ) act = ls-time_fee ).
ENDMETHOD.
METHOD distance_fee_free_km.
price( EXPORTING iv_class = `L` iv_minutes = 60 iv_km = 14
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 1 ) act = ls-distance_fee ).
ENDMETHOD.
METHOD weekend_surcharge.
price( EXPORTING iv_class = `M` iv_minutes = 30 iv_km = 0 iv_weekend = abap_true
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '2.50' ) act = ls-surcharge ).
ENDMETHOD.
METHOD member_discount.
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = 28 iv_member = abap_true
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '1.13' ) act = ls-discount ).
ENDMETHOD.
METHOD minimum_total.
price( EXPORTING iv_class = `S` iv_minutes = 15 iv_km = 0
IMPORTING es_result = DATA(ls) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 5 ) act = ls-total ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 3 ) act = ls-time_fee ).
ENDMETHOD.
METHOD invalid_duration.
price( EXPORTING iv_class = `S` iv_minutes = 0 iv_km = 0
IMPORTING ev_subrc = DATA(lv_subrc) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,25 @@
FUNCTION {{p}}rent_sample
IMPORTING
iv_vehicle_class TYPE string
iv_minutes TYPE i
iv_km TYPE i
iv_member TYPE abap_bool
iv_weekend TYPE abap_bool
EXPORTING
ev_time_fee TYPE decfloat34
ev_distance_fee TYPE decfloat34
ev_surcharge TYPE decfloat34
ev_discount TYPE decfloat34
ev_total TYPE decfloat34
EXCEPTIONS
invalid_duration
invalid_distance
invalid_class.
CLEAR: ev_time_fee,
ev_distance_fee,
ev_surcharge,
ev_discount,
ev_total.
ENDFUNCTION.

View File

@@ -0,0 +1,81 @@
# 1. Goal
Calculate the price of a finished car-sharing rental. A booking program calls
one function module for each finished rental. The function module must have
the same interface as the sample function module, because the booking program
calls all pricing modules in the same way.
# 2. Open questions
None.
# 3. Context
- Function group {{P}}FG_RENT exists in package $TMP.
- Function module {{P}}RENT_SAMPLE exists in this function group. It is the
sample pricing module. Its interface is the interface that all pricing
modules must use.
- All rental data is in the input parameters. No database access is necessary.
# 4. Contract
Create the function module {{P}}RENT_PRICE in function group {{P}}FG_RENT.
The interface is:
IMPORTING
iv_vehicle_class TYPE string
iv_minutes TYPE i
iv_km TYPE i
iv_member TYPE abap_bool
iv_weekend TYPE abap_bool
EXPORTING
ev_time_fee TYPE decfloat34
ev_distance_fee TYPE decfloat34
ev_surcharge TYPE decfloat34
ev_discount TYPE decfloat34
ev_total TYPE decfloat34
EXCEPTIONS
invalid_duration
invalid_distance
invalid_class
This interface is the same as the interface of {{P}}RENT_SAMPLE.
Put the calculation logic in a new class in package $TMP. The function module
must only call this class. The function module must not contain the
calculation itself. The class is your own design decision. The function module
maps the result of the class to its exporting parameters and exceptions.
# 5. Business rules
1. Check the input in this sequence:
a. If iv_minutes is less than 1, raise INVALID_DURATION.
b. If iv_km is less than 0, raise INVALID_DISTANCE.
c. If iv_vehicle_class is not 'S', not 'M' and not 'L', raise INVALID_CLASS.
2. Charge the rental duration in started 15-minute blocks. A duration of 15
minutes is one block. A duration of 16 minutes is two blocks.
3. The price of one block depends on the vehicle class: 'S' costs 3.00, 'M'
costs 5.00 and 'L' costs 8.00. The time fee is the number of blocks
multiplied by the block price.
4. The first 10 kilometres of a rental are free. Each further kilometre costs
0.25. The distance fee is the number of kilometres above 10 multiplied by
0.25.
5. If iv_weekend is true, the weekend surcharge is 25 % of the time fee.
Otherwise the weekend surcharge is 0.
6. The gross amount is the time fee plus the distance fee plus the weekend
surcharge.
7. If iv_member is true, the member discount is 15 % of the gross amount.
Otherwise the member discount is 0.
8. The total price is the gross amount minus the member discount. If this
value is less than 5.00, the total price is 5.00.
9. Round the weekend surcharge, the member discount and the total price half
up to 2 decimals.
10. Return the time fee in ev_time_fee, the distance fee in ev_distance_fee,
the weekend surcharge in ev_surcharge, the member discount in ev_discount
and the total price in ev_total.
# 6. Constraints
- Release target: ABAP Platform 2025 (SAP_BASIS 816).
- Coding standards: Clean ABAP. Keep every method below 40 statements.
- Out of scope: do not change {{P}}RENT_SAMPLE.
# 7. Acceptance
- The function module {{P}}RENT_PRICE is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests for the new class as local test classes.
- Write ABAP Unit tests for the function module in a global test class.

View File

@@ -0,0 +1,111 @@
{
"id": "G0026",
"category": "A",
"object_type": "FUNC",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "FUGR",
"name": "{{P}}FG_RENT",
"description": "Rental pricing"
},
{
"type": "FUNC",
"name": "{{P}}RENT_SAMPLE",
"functionGroup": "{{P}}FG_RENT",
"file": "seed/rent_sample.func.abap",
"description": "Sample pricing module"
}
],
"contract": [
{
"type": "FUNC",
"name": "{{P}}RENT_PRICE",
"functionGroup": "{{P}}FG_RENT",
"params": [
{
"name": "iv_vehicle_class",
"type": "string"
},
{
"name": "iv_minutes",
"type": "i"
},
{
"name": "iv_km",
"type": "i"
},
{
"name": "iv_member",
"type": "abap_bool"
},
{
"name": "iv_weekend",
"type": "abap_bool"
},
{
"name": "ev_time_fee",
"type": "decfloat34"
},
{
"name": "ev_distance_fee",
"type": "decfloat34"
},
{
"name": "ev_surcharge",
"type": "decfloat34"
},
{
"name": "ev_discount",
"type": "decfloat34"
},
{
"name": "ev_total",
"type": "decfloat34"
}
]
}
],
"out_of_scope": [
"{{P}}RENT_SAMPLE"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T14_HIDDEN",
"file": "hidden/t14_hidden.clas.abap",
"description": "T14 hidden tests"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}CL_RENT",
"file": "reference/cl_rent.clas.abap",
"testclasses_file": "reference/cl_rent.clas.testclasses.abap",
"description": "Rental price calculation"
},
{
"type": "FUNC",
"name": "{{P}}RENT_PRICE",
"functionGroup": "{{P}}FG_RENT",
"file": "reference/rent_price.func.abap",
"description": "Rental pricing module"
},
{
"type": "CLAS",
"name": "{{P}}T14_TEST",
"file": "reference/t14_test.clas.abap",
"description": "T14 own tests"
}
],
"craft_checks": [
"method_length"
]
}