Compare commits
24 Commits
224ba1a5f8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4261054eac | ||
|
|
8ef85c2713 | ||
|
|
a465c33e3f | ||
|
|
4002d889d5 | ||
|
|
c5f1a36df0 | ||
|
|
c2c3803b1b | ||
|
|
0c0e07fe96 | ||
|
|
7f03849a86 | ||
|
|
f5611cbf56 | ||
|
|
542fc8ec31 | ||
|
|
1ddb9a7c65 | ||
|
|
01f3372e2c | ||
|
|
9c753a37ff | ||
|
|
5c07026bbb | ||
|
|
6984fc7917 | ||
|
|
eb13453097 | ||
|
|
dc8d99a913 | ||
|
|
301c221d8e | ||
|
|
59496cb08b | ||
|
|
4432ac896c | ||
|
|
976274d48a | ||
|
|
55f4068330 | ||
|
|
b119f1afac | ||
|
|
454db6626c |
57
docs/bf16-memory.md
Normal file
57
docs/bf16-memory.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# bf16 memory test and GPU choice (2026-10-06, estimates, nothing was run)
|
||||
|
||||
**No GPU job is started without Kral's go. Decision 2026-10-06: wait. Run the memory test when the training data is near the size of the first SFT run** (the real sample lengths and the
|
||||
count then decide the flavor; today there are 36 train samples). Script: `train/hf_train_bf16.py` (`--memory-test --sweep 16000,32000,48000,64000`: no data needed, 3 optimizer steps per length,
|
||||
stops at the first OOM, uploads the result as `memtest_*.json` to the output repo). **The length limit (48k or 64k) is decided by this test** (decision 3): the builder keeps `--max-tokens 48000`
|
||||
until then; 64k would bring back the long CDS trajectories (today 5 of 19 are over 48k).
|
||||
|
||||
## Model (from config.json of Qwen3.8-27B)
|
||||
64 layers (48 linear attention, 16 full attention), hidden 5120, MLP 17408, vocab 248320, 27.8 B parameters. LoRA rank 16 on q/k/v/o, gate/up/down, in_proj_qkv, in_proj_z, out_proj: **107 M trainable parameters**.
|
||||
|
||||
## Memory by component (GB; estimate, not a measurement)
|
||||
| component | 48k | 64k | how |
|
||||
|---|---|---|---|
|
||||
| weights bf16 | 51.8 | 51.8 | 27.8 B x 2 bytes |
|
||||
| LoRA weights + grads + 8-bit Adam | 1.0 | 1.0 | 107 M x 10 bytes |
|
||||
| layer inputs for the backward pass | 29.3 on the GPU, about 0 with the Unsloth CPU offload (then 29 GB host RAM) | 39.1 / about 0 (host RAM 39 GB) | seq x 5120 x 2 bytes x 64 layers |
|
||||
| recompute peak of one layer | 9.2 | 11.3 | MLP tensors seq x 17408 x 2 bytes x 4 plus 3 GB for attention (assumption) |
|
||||
| logits and loss | 3 chunked, 67 full logits | 3 chunked, 89 full logits | full logits: seq x 248320 x (bf16 + fp32 upcast + grad) |
|
||||
|
||||
## Fit by GPU (92 % of the card counted as usable)
|
||||
| seq | checkpoints | loss | estimated peak GB | 1x A100 80 GB | 1x RTX PRO 6000 96 GB | 1x H200 141 GB | 2x H200 282 GB (model parallel) |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 16k | CPU offload (Unsloth) | chunked | 61 | fits | fits | fits | fits |
|
||||
| 16k | CPU offload (Unsloth) | full logits | 80 | no | fits | fits | fits |
|
||||
| 16k | on the GPU | chunked | 71 | fits | fits | fits | fits |
|
||||
| 16k | on the GPU | full logits | 90 | no | tight | fits | fits |
|
||||
| 32k | CPU offload (Unsloth) | chunked | 63 | fits | fits | fits | fits |
|
||||
| 32k | CPU offload (Unsloth) | full logits | 104 | no | no | fits | fits |
|
||||
| 32k | on the GPU | chunked | 82 | no | fits | fits | fits |
|
||||
| 32k | on the GPU | full logits | 124 | no | no | fits | fits |
|
||||
| 48k | CPU offload (Unsloth) | chunked | 65 | fits | fits | fits | fits |
|
||||
| 48k | CPU offload (Unsloth) | full logits | 129 | no | no | fits | fits |
|
||||
| 48k | on the GPU | chunked | 94 | no | tight | fits | fits |
|
||||
| 48k | on the GPU | full logits | 158 | no | no | no | fits |
|
||||
| 64k | CPU offload (Unsloth) | chunked | 67 | fits | fits | fits | fits |
|
||||
| 64k | CPU offload (Unsloth) | full logits | 153 | no | no | no | fits |
|
||||
| 64k | on the GPU | chunked | 106 | no | no | fits | fits |
|
||||
| 64k | on the GPU | full logits | 192 | no | no | no | fits |
|
||||
|
||||
Reading:
|
||||
- **The loss is the main risk, not the weights.** With full logits only the H200 (141 GB, CPU offload) fits 32k and 48k, and not 64k. The run needs a fused or chunked cross entropy
|
||||
(Unsloth has one for the architectures it patches; whether it covers `qwen3_5` is unknown). The memory test shows it at once: if 16000 already fails on an H200, the loss is the cause.
|
||||
Fallback (not built): hidden states, then the loss over the 32 % labeled positions only, in chunks of 4k.
|
||||
- **A100 80 GB (2.50 USD/hour)** only with the CPU offload and a chunked loss (about 65 GB at 48k, 67 GB at 64k: no margin). **H200 141 GB (5 USD/hour)** fits with margin;
|
||||
**RTX PRO 6000 96 GB (2.75 USD/hour)** fits with the offload and the chunked loss.
|
||||
- Multi-GPU (`a100x4`, `h200x2`): only with model parallelism, one card works at a time; not before the single card test.
|
||||
|
||||
## Order when the test is run
|
||||
1. Memory test on **h200** (about 40 minutes with 64k, about 3.5 USD): sweep 16000,32000,48000,64000, offload on. The limit is the largest length that fits with margin.
|
||||
2. If the loss is the problem: build the chunked loss (about 2 hours), repeat.
|
||||
3. Real run with `--s2-epochs 3 --s2-loss-share 0.6` (stage 1 epochs are computed: stage 2 carries 60 % of the loss tokens; weights of the own-test class are applied).
|
||||
|
||||
## Time and cost (estimate from the nf4 run: 221 tokens/s on an A100 at 6.75k tokens per document; bf16 faster, an H200 about 2 to 2.5 times an A100)
|
||||
Today's data: stage 2 is 36 samples (0.94 M tokens per epoch, 3 epochs = 2.8 M tokens) and, by the 60 % rule, about 0.2 epochs of stage 1 (0.5 M tokens): 3.3 M tokens in total.
|
||||
A100 about 3.1 h (about 8 USD), H200 about 1.4 h (about 7 USD).
|
||||
With three times the stage 2 data (the size that the restart plan aims at): 2.8 M tokens per epoch, 8.5 M in 3 epochs, plus about 0.6 epochs of stage 1 (1.5 M): 10 M tokens,
|
||||
H200 about 4.3 h (about 21 USD), A100 about 9 h (about 23 USD). The real number comes from the memory test (`step_seconds`).
|
||||
55
docs/data-analysis-stage2.md
Normal file
55
docs/data-analysis-stage2.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Stage 2 data analysis (accepted DeepSeek trajectories), 2026-10-06
|
||||
|
||||
Script `train/analyze.py`, numbers in `runs/analysis/analysis.json`. Base: 90 accepted trajectories of 119 runs (acceptance 76 %).
|
||||
|
||||
## 1. Repair taxonomy: are the three step-3 errors covered?
|
||||
|
||||
133 failed writes occurred inside accepted trajectories; each of the three common errors appears and is repaired:
|
||||
|
||||
| error (roadmap step 3) | failed writes | trajectories | repaired in the same trajectory | example |
|
||||
|---|---|---|---|---|
|
||||
| `TYPE c LENGTH n` in a method signature | 10 | 3 | 3 | Unable to interpret "4". Possible causes of error include incorrect spellings or comma errors. |
|
||||
| reserved word as a field or parameter name | 18 | 16 | 16 | Field "VALUE" is unknown. |
|
||||
| name longer than 30 characters | 25 | 23 | 23 | The name "SORTS_BY_TOTAL_DESC_THEN_VARIETY" is longer than the allowed 30 characters. |
|
||||
|
||||
Reading: the long names (23 trajectories) and the reserved words (16, by a text match: the label is generous, it also counts "Field VALUE is unknown") are well covered.
|
||||
**`TYPE c LENGTH` in a signature is thin: 3 trajectories** (10 failed writes, all repaired). The SAP message is only "save operation failed" or, with the proxy hint,
|
||||
`Statement does not exist ... METHODS`. This is the error that the proxy syntaxCheck exists for; after the reset the error-targeted slots ("named-type") should be
|
||||
run first for CLAS and FUNC to get more of these. Other frequent errors (top list in the json): the testclasses include is not reachable with `sap_push_element` (9 times),
|
||||
"save operation failed" without detail (30 with the hint), "Field X is unknown", "Test method can be defined only in test classes".
|
||||
|
||||
## 2. Behaviors that Qwen lacks (series A, all 20 runs) versus the teacher (accepted trajectories)
|
||||
|
||||
| behavior | DeepSeek (accepted) | Qwen series A |
|
||||
|---|---|---|
|
||||
| runs with at least one failed write | 60 of 90 | 14 of 20 |
|
||||
| **repair after the first error** (a different source that is written successfully) | **59 of 60 (98 %)** | **6 of 14 (43 %)** |
|
||||
| median calls from the first error to the repair | 1 | 2.0 |
|
||||
| searches/reads before the first write (median / p90) | 3.0 / 19 | 4.0 / 15 |
|
||||
| longest series of `sap_search_object` calls | 7 | **61** |
|
||||
| runs with the same source pushed again | 2 of 90 | **11 of 20** |
|
||||
| runs without any write | 4 | 4 |
|
||||
|
||||
The teacher repairs after the first error in almost every case, one call later. It writes after a median of 3 reads. The longest search run of the teacher is 7, of Qwen 61.
|
||||
These are exactly the two behaviors the SFT must teach; both are in the data (repair share 60 to 65 % of accepted trajectories).
|
||||
Note: the Qwen figure includes failed runs, the teacher figure only accepted ones (a teacher run that never repaired is not accepted), so part of the gap is selection.
|
||||
For a fair view the rejected teacher runs would be added; they are in `runs/traj/` (29 runs), not analysed here.
|
||||
|
||||
## 3. Near duplicates
|
||||
|
||||
Pairwise comparison of the code that each accepted trajectory wrote (5-word shingles, prefix removed) and of the final reports: **no pair above 0.8 (code) or 0.85 (report)**, also none
|
||||
between the two trajectories of one task. The data is not repetitive at this level.
|
||||
|
||||
## 4. empty_response (13 of 119 runs, 11 %)
|
||||
|
||||
By kind: {'CLAS': 2, 'DDLS': 6, 'PROG': 3, 'TABL': 2} (CDS 6 of 13). The last call before the empty turn was a read in 10 of 13 cases ({'sap_push_source': 1, 'sap_syntax_check': 1, 'sap_pull_source': 4, 'sap_object_structure': 1, 'sap_search_object': 1, 'sap_sql_query': 4, 'sap_check_object': 1}), with small results (50 to 6000 characters) and
|
||||
contexts from 13k to 63k tokens: it is not a big tool result and not the context size. In every case the turn used the whole output limit (32000 tokens) on reasoning with no content and no tool call;
|
||||
in 7 of 13 runs two or three turns in a row did that (the retry with the same prompt and temperature reproduces it). Over all 1997 turns: p50 1109 tokens, p90 7862,
|
||||
the legitimate long turns (accepted runs, content produced) reach up to 30078; only 3 legitimate turns were longer than 24000, 10 longer than 20000, 22 longer than 16000.
|
||||
Fix: `docs/empty-response.md`.
|
||||
|
||||
## 5. What it means for the data
|
||||
|
||||
- Keep the repair behavior and the "write soon" behavior: they are present. Weight the error-targeted slots (named-type) up after the reset.
|
||||
- Do not rely on `TYPE c LENGTH` repairs: only 3 examples.
|
||||
- The DDLS share of empty runs (6 of 13) explains much of the CDS rejection.
|
||||
23
docs/empty-response.md
Normal file
23
docs/empty-response.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# empty_response fix (2026-10-06)
|
||||
|
||||
**Problem.** 13 of 119 DeepSeek runs (11 %) ended with `empty_response`: one turn used the whole output limit (32000 tokens) for reasoning,
|
||||
with no content and no tool call, and the retry (same prompt, same temperature, up to two) did it again in 7 of 13 runs. Each such run costs up to
|
||||
3 x 32000 output tokens and is lost (DDLS 6, PROG 3, CLAS 2, TABL 2). Analysis: `docs/data-analysis-stage2.md`, section 4.
|
||||
|
||||
**What the data says.** The empty turn comes after a small read result (50 to 6000 characters), in contexts of 13k to 63k tokens, not after a large result. Turns
|
||||
over all runs: p50 1.1k, p90 7.9k tokens; legitimate long turns (large test classes) reach 30k; only 3 of 1997 turns longer than 24k were legitimate.
|
||||
|
||||
**Implemented (harness side only, `harness/agents.py`, `harness/trajectories.py`)**
|
||||
1. Output cap per turn **24000** (was 32000; saves a quarter of every runaway turn, costs 3 of 1997 legitimate turns).
|
||||
2. **One retry at temperature 0.8** (was two retries at 0.2): another sample instead of the same runaway.
|
||||
3. **Stream guard** (off until verified): the request is streamed; when a turn has produced only reasoning for `STREAM_GUARD` tokens (suggested 9000) and no content and
|
||||
no tool call, the stream is cut and the turn counts as empty (retry as in 2). Saves most of the cost of a runaway turn. Tested with a fake streaming server (text, tool call,
|
||||
runaway: cut after 1500 estimated tokens in 0.01 s). **Not tested on the cloud model**: it needs the Ollama cloud stream to carry the reasoning in `delta.reasoning`
|
||||
(or `reasoning_content` / `thinking`) and tool calls as streamed deltas; if the stream cannot be parsed the agent falls back to the normal request. Usage of a cut turn is estimated
|
||||
(characters / 3.2) and marked `estimated` in the record.
|
||||
4. Not implemented: "one object per write call" rule and splitting large test classes. Both change what the model sees (system prompt or task) and so the training distribution and
|
||||
the comparison with the baseline (same system prompt). Option if 1 to 3 are not enough: a hint in the task spec of CDS tasks only.
|
||||
|
||||
**Verify after the reset (5 runs, DDLS and PROG first because they had most empty runs).** Run with `STREAM_GUARD=9000 python3 -m harness.pipeline` or set the constant:
|
||||
check that `empty_response` stays below 5 % over 40 runs, that no accepted run was cut wrongly (`cut_by_stream_guard` in `metadata.turn_usage` followed by a good turn is fine),
|
||||
and that the cost per run does not rise. If the stream breaks (tool calls missing), unset `STREAM_GUARD`: items 1 and 2 stay.
|
||||
84
docs/epod-lock-leak.md
Normal file
84
docs/epod-lock-leak.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# EPOD: stale lock after a write (3 cases; the third one gives the cause)
|
||||
|
||||
Status 2026-10-05 evening. For Kral, to fix in the EPOD server (or to decide that it is not worth it).
|
||||
|
||||
## What happened (the two real cases)
|
||||
|
||||
| | Case 1 | Case 2 |
|
||||
|---|---|---|
|
||||
| When | 15:09, run 200102, task G1034 | 19:25, run 200273, task G1091 |
|
||||
| Object | class `Z4AEE0SQ_STORAGE_PRICER` | table `Z4AJ50UB_PO_HEAD` (seed table of the task) |
|
||||
| Situation | normal run; the second `sap_push_source includeType=testclasses` returned success (two activation warnings); the next `sap_push_element` failed `[LOCK] ... User KESELI is currently editing`, twice; teardown through ADT deletion said `You are already editing` | I started a second controller by mistake (same run number, same prefix, same seed table) and then killed both controller processes while the seed was installed. Teardown/deletion of the table said `You are already editing` |
|
||||
| Lock gone | only after SM12 (Kral) | only after SM12 (Kral) |
|
||||
|
||||
In both cases the lock outlived the MCP session and the run (hours). `ENQUEUE_READ` (see below) showed nothing after the SM12 delete.
|
||||
|
||||
## Case 3 and the probable cause (2026-10-06): the MCP server (Eclipse) restarted while a write was running
|
||||
|
||||
At 21:43 on 2026-10-05 Kral restarted Eclipse (the EPOD MCP server runs inside it). Three runs were writing at that moment. One of them (run 202781, task G1927) left this entry in the
|
||||
enqueue table (read with the reader class below, 2026-10-06 06:15):
|
||||
|
||||
```
|
||||
GNAME=SEOCLSENQ GARG=ZCL_Z4CGT1HJ_JOB_COST_TEST====... GMODE=X GOBJ=ESEOCLASS GCLIENT=001 GUNAME=KESELI
|
||||
GUSR=20261005194600195642000500vhcala4hci_A4H_00... GUSE=1 GTHOST=vhcala4hci_A4H_00 GTWP=05 GTDATE=20261005 GTTIME=194600 (server time, 2 hours behind CEST: 21:46)
|
||||
```
|
||||
Exclusive lock (mode X) on the class, owner user KESELI, created at 21:46, three minutes after the restart, by a write that was in flight; it was still there the next morning.
|
||||
Its object was not cleaned up because the model had named it `ZCL_<run prefix>_JOB_COST_TEST` (prefix inside the name; the teardown looked only for names that start with the prefix). When the pipeline
|
||||
restarted at 22:27 it ran the same task with the same run number, so the new run found the old locked class: `[LOCK] ... User KESELI is currently editing` on every `sap_push_element` (the run looped and scored 75).
|
||||
Earlier I wrote that the restart did not leak a lock: that was wrong, I had only cleaned the objects whose names start with the prefix.
|
||||
|
||||
So the cause is probably: **a write call is in flight when the MCP server process stops (restart, crash, kill of the whole server); the lock of that write stays in the enqueue table** (the stateful ADT session that owns it is gone, and nothing removes the lock).
|
||||
This also fits case 2 (two controllers killed in the middle of a write) better than the client-side kill tests, which did not reproduce it: killing the *client* does not stop the server's write.
|
||||
It cannot be tested from the client side without restarting Eclipse; for Kral: start a write (a class with a large test include), restart Eclipse in the middle, read the enqueue table (below) and look at SM12.
|
||||
|
||||
## What we tried to reproduce (A4H, probe objects, no cloud, 2026-10-05)
|
||||
|
||||
Every test: write through MCP, interfere, wait 3 s, read the enqueue table, write the same object again, delete it.
|
||||
None left a lock. Scripts: `scripts_probe/` (`killtest.py`, `killtabl.py`, `killtwo.py`, `killcreate.py`, `deltest.py`, `lockprobe.py`).
|
||||
|
||||
1. Client killed (`SIGKILL`) 0.03 to 0.8 s after `sap_push_source includeType=testclasses` was sent (the write needs about 0.7 s): 9 kills, no lock. The server finishes the write.
|
||||
2. Client killed during `sap_push_source TABL` (DDL, activation): 6 kills at 0.5 to 4.5 s (the write takes about 0.7 s): no lock.
|
||||
3. Two clients write the same table at the same time and both are killed: 8 delays (0.05 to 0.7 s): no lock.
|
||||
4. Two clients both `sap_create_object` + `sap_push_source` the same table and both are killed (Case 2 as exactly as possible): 8 delays (0.05 to 1.0 s): no lock.
|
||||
5. ADT deletion of the object while a write on it runs: the deletion is refused (`You are already editing`) or wins the race, no lock stays (6 delays).
|
||||
6. Earlier: 25 write cycles alone; 60 cycles overlapping 2690 `sap_run_unit_test` calls of another session; 40 cycles of testclasses write + `sap_check_object` (ATC, unit tests, coverage) + `sap_object_members` + second testclasses write + `sap_push_element` with a second session on `sap_check_object`; `sap_push_element` on a method of the test class (error text only, no lock).
|
||||
|
||||
So neither "client killed during a write" nor "two writers on one object" nor "delete during a write" leaks a lock on A4H in a short test. The two real cases may need a long running or slow call (the real runs were under load from 3 generators and 1 to 2 trajectory workers, calls then take seconds), or a state of the ADT stateful session that the probes did not reach.
|
||||
|
||||
## How to see the enqueue state (exact method)
|
||||
|
||||
A class with `IF_OO_ADT_CLASSRUN` calls `ENQUEUE_READ` and is run with `sap_run_class` (class `ZPROBE0EQ_LOCKS`, in `$TMP`; source in `scripts_probe/lockprobe.py`):
|
||||
|
||||
```abap
|
||||
CALL FUNCTION 'ENQUEUE_READ'
|
||||
EXPORTING gclient = sy-mandt gname = '' garg = '' guname = ''
|
||||
IMPORTING subrc = lv_subrc
|
||||
TABLES enq = lt_enq "TYPE STANDARD TABLE OF seqg3
|
||||
EXCEPTIONS communication_failure = 1 system_failure = 2 OTHERS = 3.
|
||||
```
|
||||
|
||||
A normal lock of a running write looks like this (seen in the 0.6 s test while a pipeline run was writing):
|
||||
`GNAME=SEOCLSENQ GARG=Z4AJW0UK_CL_BERTH_FEE_TEST====...` (lock object of the class include, owner user KESELI).
|
||||
After every probe the table was empty. A stale lock would show up as an entry that stays after the MCP session is closed:
|
||||
read the table, close the session, read again.
|
||||
|
||||
There is no function module on A4H to delete an enqueue entry (`TFDIR`: only `ENQUEUE_READ`, no `ENQUEUE_DELETE`): SM12 is the only way to release a stale lock, so every reproduction costs one SM12 delete.
|
||||
|
||||
## What the harness does about it today
|
||||
|
||||
- A write result with `[LOCK]` ("currently editing") is an infrastructure event: the run is not accepted, the trajectory workers go from 2 to 1, three equal events stop the pipeline.
|
||||
- A failed teardown (`You are already editing`) is listed as a leftover (dashboard); the object needs an SM12 delete, then the harness deletes it.
|
||||
- One controller only (flock on `runs/pipeline/controller.lock`), so two controllers cannot install the same run number twice any more (this was Case 2).
|
||||
- Never kill a controller while it runs: it is drained through the budget guard or a STOP flag.
|
||||
|
||||
## Also found: leftovers with the prefix inside the name
|
||||
27 classes named `ZCL_<prefix>_...` / `ZCX_<prefix>_...` were left in A4H from earlier runs (the teardown searched only for names that start with the prefix). They showed up in
|
||||
`sap_inactive_objects` and searches of later runs and in 57 of 90 accepted trajectories. Fixed in the harness (2026-10-06, `harness/proxy.py`, `harness/runner.py`, `harness/sweep.py`).
|
||||
|
||||
## Wish for EPOD
|
||||
|
||||
0. **Release the enqueue locks of the server's own ADT sessions when the MCP server stops or starts** (a lock of user KESELI with the server's work process that has no live ADT session behind it).
|
||||
1. Release the lock in a `finally` path of every write tool (also after a failed or warned activation).
|
||||
2. Release all locks of an MCP session when the session ends (`DELETE /mcp`) and when the connection breaks.
|
||||
3. Return a clear error with the lock owner and the age of the lock, and a tool to release the locks of the caller's own session.
|
||||
4. A short idle timeout for the stateful ADT session (the lock lasted hours).
|
||||
44
docs/eval-spotcheck-new-kinds.md
Normal file
44
docs/eval-spotcheck-new-kinds.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Eval seti: yeni türler için slot planı ve Kral'ın kontrol sayfası
|
||||
|
||||
Durum: hazırlık (2026-10-06). Üretim 12 Ekim sıfırlamasından sonra, **eğitim görevlerinden önce** (`docs/restart-12-oktober.md`, adım 0).
|
||||
Amaç: eğitimden sonra INTF, TABL, STRU, MSAG ve istisna sınıfı görevlerini ölçebilmek. Şu an eval adaylarında (130 kabul) sadece CLAS, FUNC, DDLS, PROG var.
|
||||
|
||||
## 1. Slotlar (`python3 -m harness.evalset plan-new`)
|
||||
|
||||
Eğitim karışımıyla aynı paylar (110 görevlik sette INTF 8, TABL 9, STRU 2, MSAG 3, istisna 3); %30 fazla aday, çünkü ret ve ampirik süzgeç göreve göre eler.
|
||||
|
||||
| tür | kategori | slot | kimlikler | not |
|
||||
|---|---|---|---|---|
|
||||
| INTF | A | 10 | G0200–G0209 | arayüz: sabitler, tipler, metotlar; gizli test arayüzü uygulayan yerel sınıf kullanır |
|
||||
| TABL | B | 11 | G0210–G0220 | tablo: alanlar, anahtar, not null; gizli test RTTI + `cl_osql_test_environment` |
|
||||
| STRU | B | 3 | G0221–G0223 | yapı: alanlar, tipler; RTTI |
|
||||
| MSAG | D | 4 | G0224–G0227 | mesaj sınıfı: numara, metin, yer tutucu; `MESSAGE ... INTO` |
|
||||
| istisna (CLAS) | D | 4 | G0228–G0231 | `CX_` sınıfı: öznitelik, metin, zincir |
|
||||
| K (serbest metin) | K | 5 | G0232–G0236 | INTF 1, TABL 2, MSAG 1, istisna 1; `evalset run-new-k`, tool isimleri EPOD'daki gibi (`generic_v0` kullanılmıyor, varsayım: bunu Kral onaylar) |
|
||||
|
||||
Her ikinci-üçüncü slot zor (zorluk 3). Her slot eğitim havuzuyla örtüşme kontrolünden geçer (eğitimdeki görevlere çok benziyorsa model yeniden yazar).
|
||||
Maliyet tahmini: 32 slot x yaklaşık 0,15 ledger = 5 ledger, yaklaşık 2 usage; K varyantları 1 ledger'dan az.
|
||||
|
||||
## 2. Otomatik kapılar (Claude, senin işin değil)
|
||||
oracle 100 ve null 0, mutasyon denetimi (INTF/TABL/STRU/MSAG/istisna için yeni mutantlar), eğitim havuzuyla örtüşme, ampirik süzgeç (DeepSeek ve yerel Qwen: seri A düzeneği eval görevlerini de koşturur),
|
||||
sonra `docs/eval-inceleme.md` kontrol listesiyle Claude incelemesi (`python3 -m harness.review G0200`).
|
||||
|
||||
## 3. Senin kontrolün (yaklaşık 10 görev, yaklaşık 45 dakika)
|
||||
Kimler: Claude'un **işaretlediği** görevler + her yeni türden **1 örnek** (5 görev) + 1 K varyantı. Her görev için `python3 -m harness.review <id>` tek ekranda spec, sözleşme, gizli test isimleri, mutasyon sonucu ve üretim geçmişini gösterir.
|
||||
Kararın: `python3 -m harness.review <id> --set accept|fix|flag|reject --note "..."`.
|
||||
|
||||
Genel sorular (listedeki 1–11): spec belirsiz mi, her kuralın testi var mı, kontrat yeterli mi, zanaat serbest mi, gerçekçi mi, sızıntı var mı.
|
||||
**Yeni türlere özel sorular (12–16):**
|
||||
|
||||
| # | Soru | tür |
|
||||
|---|---|---|
|
||||
| 12 | Spec her alanı, uzunluğu, anahtarı ve not-null'ı tek anlamlı veriyor mu? Gizli test **bayt/karakter** karışıklığına düşmüyor mu (RTTI `length` bayttır)? | TABL, STRU |
|
||||
| 13 | Tablo testi gerçek davranışı ölçüyor mu (aynı anahtarla ikinci INSERT `sy-subrc 4`), yoksa sadece alan adlarına mı bakıyor? | TABL |
|
||||
| 14 | Arayüz testi imza uyuşmazlığını yakalıyor mu (uygulayan yerel sınıf yalnızca imza doğruysa derlenir) ve sabit değerlerini okuyor mu? | INTF |
|
||||
| 15 | Mesaj metinleri, numaralar ve yer tutucular spec'te **birebir** yazılı mı; test `MESSAGE ID ... INTO` ile son metni mi karşılaştırıyor? | MSAG |
|
||||
| 16 | İstisna: öznitelik, metin ve (varsa) önceki istisna zinciri spec'te tanımlı mı; test fırlat-yakala ile ölçüyor mu? Gerçekten "istisna tasarımı" mı gerekiyor? | istisna |
|
||||
|
||||
## 4. Sonuç tablosu (üretimden sonra doldurulur)
|
||||
| id | tür | otomatik kapılar | Claude incelemesi | Kral | not |
|
||||
|---|---|---|---|---|---|
|
||||
| | | | | | |
|
||||
29
docs/foreign-objects-report.md
Normal file
29
docs/foreign-objects-report.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Other runs' objects in tool results: which runs are affected (2026-10-06)
|
||||
|
||||
Cause: the model sometimes names its own helper or test class `ZCL_<run prefix>_...` (prefix inside the name). The teardown looked only for names that start with the prefix, so such
|
||||
classes stayed in A4H and showed up in `sap_inactive_objects`, searches and sometimes in a read of the next runs. Fixed on 2026-10-06 (proxy hides them, teardown finds them, `harness/sweep.py`).
|
||||
Scan: `train/foreign_scan.py` (read only; result `runs/analysis/foreign_scan.json`). A run is "affected" when a tool result contains a name of another run; a "foreign read" is a read tool call on such an object.
|
||||
|
||||
| group | runs | with foreign names in a tool result | with a foreign read | tools |
|
||||
|---|---|---|---|---|
|
||||
| official baseline Qwen (MacBook, 11 tasks) | 11 | 0 | 0 | - |
|
||||
| Devstral baseline (dropped) | 11 | 0 | 0 | - |
|
||||
| older Qwen baselines (archive) | 8 | 0 | 0 | - |
|
||||
| smoke | 1 | 0 | 0 | - |
|
||||
| eval empirical filter (DeepSeek on eval candidates) | 178 | 25 | 4 | sap_inactive_objects 23, sap_search_object 4, sap_pull_source 5 |
|
||||
| eval generation validation (oracle, null, mutants) | 1344 | 0 | 0 | - |
|
||||
| early pilot runs | 16 | 1 | 0 | sap_inactive_objects 1 |
|
||||
| training trajectories (DeepSeek) | 117 | 67 | 15 | sap_inactive_objects 66, sap_search_object 16, sap_pull_source 31, sap_run_unit_test 7, sap_element_info 1, sap_check_object 1 |
|
||||
| series A (local Qwen) | 20 | 3 | 0 | sap_inactive_objects 2, sap_search_object 1 |
|
||||
|
||||
## Reading
|
||||
- **Official baseline (Qwen, 11 tasks, 2026-10-04): not affected** (0 of 11; also not Devstral 0 of 11, the older Qwen baselines 0 of 8, smoke 0 of 1). At that time A4H had few leftovers,
|
||||
and none of the 11 baseline runs called `sap_inactive_objects` (checked: 0 calls). **The baseline numbers stay as they are. Nothing was changed.**
|
||||
- **Eval generation (1344 validation runs: oracle, null, mutants): not affected** (they call no list or search tool).
|
||||
- **Eval empirical filter (DeepSeek on eval candidates): 25 of 178 runs saw foreign names, 4 read one**
|
||||
(G0183, G0180, G0143, G0185; scores 80, 85, 85, 85; all four are accepted in the review). The empirical filter decides which eval candidates look too easy or too hard; whether the read changed a result there
|
||||
was not examined (a read costs a few calls, the scores are in the normal range). Not changed; if Opus wants to be strict: rerun these four tasks after the reset (4 runs).
|
||||
- **Early pilot runs:** 1 of 16 saw a foreign name in an inactive list.
|
||||
- **Training trajectories (DeepSeek, 117 runs incl. rejected): 67 saw foreign names, 15 read one.** Of the accepted ones, 9 had a foreign read: they are moved back to the pending pool
|
||||
(`train/requeue_foreign.py`, rows in `runs/traj/summary_excluded.jsonl`) and run again after the reset in the normal order. List results of the other accepted ones are scrubbed in the builder.
|
||||
- **Series A (local Qwen): 3 of 20 saw foreign names (inactive list, one search), 0 reads.** The scores (3 of 20) are not affected by a read; the series ran before the fix.
|
||||
68
docs/local-qwen-run.md
Normal file
68
docs/local-qwen-run.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Series A: local Qwen 3.8 27B on the new kinds (2026-10-05 22:24 to 2026-10-06 04:00)
|
||||
|
||||
Setup: `docs/remote-model.md`. Model served on the MacBook (`/Users/I301710/models/Qwen3.8-27B-4bit`, mlx-lm 0.32.0), harness, A4H and MCP on the Mac mini.
|
||||
Settings as the official baseline: thinking off, max_tokens 16384, loop guard 3, tool budget 60, temperature 0.2, same system prompt.
|
||||
Acceptance: score at least 80, end reason `report`. One task at a time. 20 tasks in 4.7 hours of run time (the 12 hour window was not used up);
|
||||
no server pause, no infrastructure outage, no harness error.
|
||||
|
||||
## Result per kind
|
||||
|
||||
| kind | accepted / runs | mean score | failure types |
|
||||
|---|---|---|---|
|
||||
| INTF | 1/3 | 33.3 | loop 2, not_active 2, contract 2, hidden_tests_not_run 2 |
|
||||
| TABL | 0/3 | 0.0 | contract 3, hidden_tests_not_run 3, loop 2, not_active 2, tool_budget 1 |
|
||||
| STRU | 0/3 | 25.0 | loop 2, not_active 2, contract 2, hidden_tests_not_run 2, tool_budget 1 |
|
||||
| MSAG | 2/3 | 66.7 | tool_budget 1, not_active 1, contract 1, hidden_tests_not_run 1 |
|
||||
| EXC | 0/3 | 0.0 | loop 3, not_active 3, contract 3, hidden_tests_not_run 3 |
|
||||
| DDLS | 0/5 | 31.0 | loop 3, not_active 3, contract 3, hidden_tests_not_run 3, tool_budget 1, low_score 1 |
|
||||
|
||||
Total: **3 of 20 accepted (15 %)**. Failure types over all runs: {'loop': 12, 'not_active': 13, 'contract': 14, 'hidden_tests_not_run': 14, 'tool_budget': 4, 'low_score': 1}.
|
||||
|
||||
## Reading
|
||||
|
||||
- **Loops are the failure.** 12 of the 17 failed runs ended with `end_reason loop` (9 times the same source pushed three times in a row, 3 times
|
||||
the same read call with the same result). 10 of the 12 are short (1 to 6 minutes, 7 to 24 calls), two are long (59 and 71 minutes, 41 and 29 calls): Qwen pushes a source, gets an activation or save error
|
||||
and pushes the same source again instead of repairing it. This is the weakness the baseline showed (loop 7 of 11) and the main target of the SFT.
|
||||
Typical errors it did not repair: `The component C_ZONE has been declared multiple times`, `The statement 50 is unexpected`,
|
||||
`Syntax error in <table>: DDL source could not be saved`, `FRIENDS ... expected after GLOBAL`, `Test method can be defined only in test classes`.
|
||||
- Tool budget (60 calls), 4 runs, two different patterns the loop guard does not catch (the arguments differ each time): **three runs did nothing but `sap_search_object`**
|
||||
(STRU G1947: 60 searches, MSAG G1940: 60, DDLS G1012: 56 searches, no write at all), and TABL G1901 pushed 49 different sources in a row (51 writes, never active).
|
||||
The loop guard only sees identical calls; these are search and rewrite loops with changing arguments. A teacher trajectory that stops searching
|
||||
and writes, and one that repairs after the first activation error, are what the SFT must show.
|
||||
- Only INTF G1917 (10 calls) and MSAG G1902 and G1945 (13 and 19 calls) were solved cleanly: the small, regular tasks.
|
||||
- Two runs reached 75 to 80 points but not an accepted state (STRU G1903 loop at the end, DDLS G1011 loop, DDLS G1031 low score after 56 calls and 64 minutes).
|
||||
- Proxy syntax hint (local abaplint) was added in 12 calls (STRU G1903: 9, EXC G1124: 3). It did not help here: both runs still looped (the hint names a line and
|
||||
a parser message, Qwen pushed the same source again).
|
||||
- Speed: mean 14 minutes per run, median 5; the slow ones (STRU G1903 71 min, DDLS G1031 64 min, TABL G1910 59 min) are long
|
||||
generations, not pauses.
|
||||
- Compare with DeepSeek on the same kinds: see the pipeline summaries in `train/STATE.md` (teacher acceptance about 76 %).
|
||||
|
||||
## Per task
|
||||
|
||||
| task | kind | accepted | score | end | calls | minutes | failure types |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| G1900 | INTF | no | 0 | loop | 7 | 6 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1913 | INTF | no | 0 | loop | 8 | 5 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1917 | INTF | accepted | 100.0 | report | 10 | 7 | - |
|
||||
| G1901 | TABL | no | 0 | tool_budget | 60 | 15 | tool_budget, contract, hidden_tests_not_run |
|
||||
| G1910 | TABL | no | 0 | loop | 41 | 59 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1911 | TABL | no | 0 | loop | 15 | 2 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1903 | STRU | no | 75.0 | loop | 29 | 71 | loop |
|
||||
| G1946 | STRU | no | 0 | loop | 24 | 4 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1947 | STRU | no | 0 | tool_budget | 60 | 5 | tool_budget, not_active, contract, hidden_tests_not_run |
|
||||
| G1902 | MSAG | accepted | 100.0 | report | 19 | 9 | - |
|
||||
| G1940 | MSAG | no | 0 | tool_budget | 60 | 6 | tool_budget, not_active, contract, hidden_tests_not_run |
|
||||
| G1945 | MSAG | accepted | 100.0 | report | 13 | 3 | - |
|
||||
| G1096 | EXC | no | 0 | loop | 14 | 3 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1104 | EXC | no | 0 | loop | 15 | 5 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1124 | EXC | no | 0 | loop | 8 | 1 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1000 | DDLS | no | 0 | loop | 8 | 2 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1011 | DDLS | no | 80.0 | loop | 18 | 4 | loop |
|
||||
| G1012 | DDLS | no | 0 | tool_budget | 60 | 5 | tool_budget, not_active, contract, hidden_tests_not_run |
|
||||
| G1020 | DDLS | no | 0 | loop | 11 | 3 | loop, not_active, contract, hidden_tests_not_run |
|
||||
| G1031 | DDLS | no | 75.0 | report | 56 | 64 | low_score |
|
||||
|
||||
## Files
|
||||
|
||||
`runs/local_qwen/` (not in git): `results.jsonl`, `summary.json`, `state.json`, `preflight.json`, `runs/` (records), and `accepted.jsonl`
|
||||
with the 3 accepted trajectories (`metadata.use_for_sft = false`, series `local_qwen_A`): not for the first SFT.
|
||||
20
docs/own-test-mutation.md
Normal file
20
docs/own-test-mutation.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Own-test mutation score of the accepted trajectories (item D, metadata only)
|
||||
|
||||
Module `harness/owntests.py`, results `runs/traj/<run>/own_test_mutation.json` (not in git), report by `train/own_test_report.py`.
|
||||
The model's own unit tests (testclasses include or global test classes) are run against the faulty references of the task (`faulty/`, mutants that the hidden tests kill).
|
||||
The correct reference must pass the model's tests first (otherwise the tests encode model specific behavior). score = killed / (killed + survived).
|
||||
**Metadata only: the acceptance filter is not changed.** The builder takes the score through `--hook hooks_example:own_test_weight` (field `own_test_mutation`).
|
||||
|
||||
## Result
|
||||
90 accepted trajectories looked at: {'scored': 63, 'not_supported': 5, 'no_own_tests': 22}.
|
||||
- Scored: 63; the model's tests also passed on the correct reference: 53 (the others are not reliable: a test that fails on the correct solution kills every mutant).
|
||||
- Mean score over the reliable ones: 0.99; median 1.00; distribution {'1.0': 50, '>=0.75': 3} (n = 53).
|
||||
- By kind (mean, n): {CLAS: 1.00 (45), DDLS: 0.90 (5), FUNC: 1.00 (3)}
|
||||
- `no_own_tests`: 22 trajectories were accepted without any own test (at most 85 points); `no_mutants`: 0; `not_supported` (PROG: tests are inside the program): 5.
|
||||
|
||||
## Weakest (score under 0.75, tests pass on the reference)
|
||||
|
||||
|
||||
## Use
|
||||
Not used for filtering yet. Candidates for a later rule (Kral + Opus decide): drop or down-weight trajectories with a reliable score under 0.5 or with `tests_pass_on_reference` false;
|
||||
prefer trajectories without own tests last. The hook example shows where the weight goes (`train/hooks_example.py`).
|
||||
97
docs/remote-model.md
Normal file
97
docs/remote-model.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Local Qwen on the MacBook, harness on the Mac mini (series A)
|
||||
|
||||
Roles: the **MacBook only serves the model** (MLX, 4-bit). The harness, A4H, the MCP server, the records and the dashboard
|
||||
stay on the **Mac mini**. The series is `python3 -m harness.localqwen` (see below). Written 2026-10-05.
|
||||
|
||||
## 1. Start the server on the MacBook (step by step)
|
||||
|
||||
1. **Plug the MacBook in and keep the lid open.** Close all heavy apps (the model needs about 16 GB, the prompt cache up to 6 GB).
|
||||
2. **Check that this is the official baseline build** (same 4-bit weights as the baseline of 2026-10-04):
|
||||
```sh
|
||||
cd ~/models/Qwen3.8-27B-4bit
|
||||
shasum -a 256 config.json model.safetensors.index.json tokenizer.json chat_template.jinja
|
||||
```
|
||||
The values must be:
|
||||
```
|
||||
14b65a0ee06517060a6bbd979bb1a8ff54e7b304b1a1f01d54344b88b8285e85 config.json
|
||||
13b840162b4cb35c66fef7df072f7dbb4717908204364f5e5d9f9655a2758fa8 model.safetensors.index.json
|
||||
06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523 tokenizer.json
|
||||
c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041 chat_template.jinja
|
||||
```
|
||||
Full check (about 2 minutes, the weights): `shasum -a 256 model-0000*-of-00003.safetensors`
|
||||
```
|
||||
6cc1508e96fb5d0865dfd5753a79f4ec60651bf3e2a82844a7e8ae9c60528c0d model-00001-of-00003.safetensors
|
||||
83f2a20ca8058f486a3634a27faf99587f4cd3c156a83dee34fb99e6ac178670 model-00002-of-00003.safetensors
|
||||
31b8c91ef899f79efaaa69e3d2c096f6e2ebeb2ff20e29222abbd9ebc79e560a model-00003-of-00003.safetensors
|
||||
```
|
||||
(These are the hashes of `mlx-community/Qwen3.8-27B-4bit` on the Mac mini, `~/models/Qwen3.8-27B-4bit`.) If one differs, stop.
|
||||
3. **mlx-lm 0.32.0 on the MacBook** (the baseline flags need it; an older server answers `unrecognized arguments: --temp ...`). Check:
|
||||
`~/qwen-venv/bin/python -c "import mlx_lm; print(mlx_lm.__version__)"` must print `0.32.0`. If there is no such venv:
|
||||
```sh
|
||||
# with uv (recommended): brew install uv (once)
|
||||
uv venv --python 3.11 ~/qwen-venv
|
||||
uv pip install --python ~/qwen-venv/bin/python "mlx-lm==0.32.0"
|
||||
# without uv (needs Python 3.10 or newer, check python3 --version; Homebrew: brew install python@3.11):
|
||||
python3.11 -m venv ~/qwen-venv && ~/qwen-venv/bin/pip install "mlx-lm==0.32.0"
|
||||
```
|
||||
The start script uses `~/qwen-venv` first and refuses to start with an older server (clear message, nothing half started).
|
||||
4. **Get the start script** `train/serve_remote.sh` onto the MacBook. Either, if the repo is on the MacBook, `cd ~/projects/abap-llm/harness && git pull`
|
||||
(path `train/serve_remote.sh`), or copy it: `scp erhankeseli@192.168.178.40:~/projects/abap-llm/harness/train/serve_remote.sh ~/serve_remote.sh`
|
||||
(192.168.178.40 is the Mac mini; use `.29` if that is its other address, check with `ifconfig` on the mini), then `chmod +x ~/serve_remote.sh`.
|
||||
The script uses the same flags as the baseline (`train/serve.sh`): temp 0.2, top-p 0.95, top-k 20, min-p 0, max tokens 32768, prompt cache 4 / 6 GB;
|
||||
only the host is `0.0.0.0` (port 8080), and `caffeinate -dimsu -w <server pid>` keeps the MacBook awake as long as the server lives.
|
||||
5. **Start it** (a terminal window on the MacBook, leave it open; or in the background):
|
||||
```sh
|
||||
nohup ~/projects/abap-llm/harness/train/serve_remote.sh > ~/qwen_server.log 2>&1 &
|
||||
```
|
||||
(use `~/serve_remote.sh` if you copied it). The first start loads the model for about 1 minute. macOS may ask
|
||||
"Do you want the application Python to accept incoming network connections?": **Allow**.
|
||||
6. **Check on the MacBook:** `tail -f ~/qwen_server.log` shows `Starting httpd at 0.0.0.0 on port 8080`, and
|
||||
`curl -s http://127.0.0.1:8080/v1/models` returns the model path.
|
||||
7. **Find the MacBook IP** (the harness needs it): `ipconfig getifaddr en0` (Wi-Fi; if empty try `en1`, or look at System Settings > Wi-Fi > Details).
|
||||
Both Macs must be in the same network (the mini is 192.168.178.x).
|
||||
8. **Check from the Mac mini** (replace the IP): `curl -s http://<MacBook IP>:8080/v1/models`. It must show
|
||||
`/Users/I301710/models/Qwen3.8-27B-4bit`.
|
||||
9. **Start the series on the Mac mini:**
|
||||
```sh
|
||||
cd ~/projects/abap-llm/harness
|
||||
python3 -m harness.localqwen --base-url http://<MacBook IP>:8080/v1 --wait 600
|
||||
```
|
||||
(`--wait 600`: waits up to 10 minutes for the server at the start. Run it with `nohup ... > runs/local_qwen.log 2>&1 &`.)
|
||||
The 12 hour window starts with the first run, not with the command.
|
||||
|
||||
## 2. Stop
|
||||
|
||||
- **The series** (Mac mini): `pkill -f harness.localqwen` stops it, but the run in progress is cut and its objects stay in A4H. Better: wait for the
|
||||
window to end (hard stop, clean) or ask Claude to stop it cleanly.
|
||||
- **The server** (MacBook): `kill $(cat ~/qwen_server.pid)` (`caffeinate -w` ends when the server ends), or `pkill -f mlx_lm.server`.
|
||||
Check: `curl -s -m 3 http://127.0.0.1:8080/v1/models` gives no answer.
|
||||
- Closing the lid or a sleeping MacBook stops the server: the series then pauses by itself and resumes when the server answers (below).
|
||||
|
||||
## 3. What the harness does (series A)
|
||||
|
||||
- **Before the first run:** the server must answer, the served model path must end in `Qwen3.8-27B-4bit`, and a 16-token probe must work.
|
||||
The answer is saved in `runs/local_qwen/preflight.json` (model path, speed of the probe). The weights themselves cannot be
|
||||
checked remotely: the shasum comparison in step 2 is the proof of the same build.
|
||||
- **Tasks and order** (`--plan-only` prints them): 3 x INTF, 3 x TABL, 3 x STRU, 3 x MSAG, 3 x exception, then 5 x DDLS (accepted
|
||||
training tasks, no K variants, lowest ids). One task at a time.
|
||||
- **Settings as the official baseline:** thinking off (`enable_thinking=false` per request), max_tokens 16384, loop guard 3,
|
||||
tool budget 60 (CDS too), temperature 0.2, same system prompt, at most 80 turns.
|
||||
- **Window:** 12 hours from the first run. At the end there is a hard stop, also in the middle of a run: the model request is dropped,
|
||||
the run is not scored (no failure), its objects are deleted on A4H (the teardown needs about 20 s).
|
||||
- **Server does not answer:** during a request the server is pinged every 30 s (3 misses in a row); between requests a failed
|
||||
request is checked with a ping. Then the run ends cleanly (objects deleted, no scoring, folder `runs/local_qwen/_paused/`), the series
|
||||
waits (a check every 30 s), and when the server answers again the same task starts again. The time of the pause counts in the 12 hours.
|
||||
This is not counted as a failure.
|
||||
- **Output** (`runs/local_qwen/`): `state.json` (the dashboard card), `results.jsonl` (one line per task), `summary.json` (at the end: pass rate and
|
||||
failure types per kind), `accepted.jsonl` (accepted trajectories, **not for the first SFT**: `metadata.use_for_sft = false`, `series = local_qwen_A`),
|
||||
`runs/` (one folder per run with `record.json`, trajectory, report). Resumable: start it again with the same arguments.
|
||||
- **Acceptance** is the same filter as for the DeepSeek trajectories: score at least 80, end reason `report`, no harness error text.
|
||||
Failure types per run: `loop`, `tool_budget`, `empty_response`, `model_error`, `not_active`, `contract`, `hidden_tests_not_run`,
|
||||
`hidden_tests_failed`, `out_of_scope_changed`, `atc_priority1`, `release_syntax`, `low_score`.
|
||||
- **Dashboard:** the card "Series A: local Qwen on the MacBook" in `runs/dashboard/index.html` (5-minute refresh).
|
||||
|
||||
## 4. Speed and cost
|
||||
|
||||
About 12 tokens per second with the 4-bit model (`train/README.md`); a task needs 20 to 40 minutes, so the 20 tasks take 7 to 13 hours:
|
||||
at the slow end the window ends before the last DDLS tasks. No cloud cost: the ledger is not touched (the model is not a `:cloud` model).
|
||||
89
docs/restart-12-oktober.md
Normal file
89
docs/restart-12-oktober.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Restart plan for 12 October 2026 (cloud work)
|
||||
|
||||
Decision (Kral + Opus, 2026-10-05): when the budget guard stops, no cloud work (generation, trajectories) before the
|
||||
Ollama reset on 12 October. Until then only no-cloud work. This is the plan for the restart.
|
||||
|
||||
## 1. Before the start (5 minutes, no cloud call)
|
||||
|
||||
1. Panel value after the reset (usage USD). Then:
|
||||
`python3 -m harness.restart_plan --panel <value>`
|
||||
It prints the `.env` values and the state below with live numbers. Put them in `.env`:
|
||||
`BUDGET_CYCLE_START=2026-10-12`, `BUDGET_LIMIT_USD=<printed>`, `BUDGET_RESERVE_USD=8` (the 3 usage reserve for the
|
||||
second teacher test stays). Ratio for the guard: 1.2 ledger per usage (trajectory runs; generation is about 2, so the
|
||||
guard is on the safe side). Give the panel value again after about 2 hours and recompute (`python3 -m harness.dashboard panel <v>`).
|
||||
2. A4H up (`docker ps`), MCP answers, no stale lock: `python3 scripts_probe/lockprobe.py` must print 0 entries.
|
||||
3. One controller only: `python3 -m harness.pipeline` refuses to start a second one (`runs/pipeline/controller.lock`).
|
||||
Remove `runs/pipeline/STOP` and `STOPPED.txt` if they exist.
|
||||
4. Dashboard: `python3 -m harness.dashboard` (5-minute page, `runs/dashboard/index.html`).
|
||||
|
||||
## 1b. Step 0: eval tasks for the new kinds (before any training task)
|
||||
|
||||
`python3 -m harness.evalset plan-new` shows 32 slots (INTF 10, TABL 11, STRU 3, MSAG 4, exception 4; ids G0200 to G0231), then `python3 -m harness.evalset run-new`
|
||||
(about 5 ledger, about 2 usage), then `python3 -m harness.evalset run-new-k` (5 K variants). Reason: the eval set (130 accepted candidates) has no INTF, TABL, STRU, MSAG
|
||||
or exception task, so the trained model could not be measured on them. Each slot is checked against the training pool. Review: `docs/eval-spotcheck-new-kinds.md`
|
||||
(Kral checks about 10 tasks). Run it before the training tasks so that the training tasks can be checked against the finished eval tasks.
|
||||
|
||||
## 1c. Step 0b: rerun of four empirical filter runs (Kral + Opus 2026-10-06)
|
||||
|
||||
Four eval candidates saw or read leftover objects of other runs in the first empirical filter (`docs/foreign-objects-report.md`): **G0183, G0180, G0143, G0185** (DeepSeek, scores 80, 85, 85, 85).
|
||||
After step 0 (eval generation for the new kinds), with the fixed proxy:
|
||||
|
||||
`python3 -m harness.empirical --model deepseek-v4.1-flash:cloud --run-base 450000 --rerun G0183 G0180 G0143 G0185`
|
||||
|
||||
This writes only to `runs/emp_rerun/` (4 runs, about 1 ledger) and prints old and new filter decision per task. `empirical.json` and the eval set are **not** touched. Decision rules (step F):
|
||||
easy candidate = score 95 or more and at most 20 tool calls; flag = score under 50 (a strong model fails although the reference passes); else normal. **If a decision changes for one of the four, report it to Kral and
|
||||
Opus first; the eval set is changed only after their answer.** If nothing changes, say so in `docs/eval-inceleme.md` and leave the old results.
|
||||
|
||||
## 2. Order of work (what the controller does by itself)
|
||||
|
||||
Only kinds below their target share are generated and run (`harness/mix.py`, `below_target`); the kind with the biggest
|
||||
deficit goes first. Target (percent of accepted tasks and of accepted trajectories): CLAS 28, INTF 7, DDLS 25, FUNC 15,
|
||||
PROG 10, TABL 8, STRU 2, MSAG 2.5, exception 2.5.
|
||||
|
||||
1. Trajectories for the new-type tasks that wait (INTF, TABL, STRU, MSAG, exception, PROG), then DDLS.
|
||||
2. Second attempts: a task whose first attempt failed, or was accepted without a repair, gets one more attempt (at most two
|
||||
attempts, at most two accepted trajectories per task). Failed DDLS and failed new-type tasks come first because they are
|
||||
below target.
|
||||
3. Generation (3 workers): the kind with the biggest deficit; a kind with more than 8 waiting tasks is not generated; a kind with
|
||||
6 or more tries and under 20 % accepted is skipped. 20 % error-targeted slots, 30 % hard slots.
|
||||
4. CLAS and FUNC generation and trajectories wait until they are at or below their target share (CLAS is far above).
|
||||
5. K variants (free text) resume at 10 % of the other accepted tasks when their kinds are below target.
|
||||
|
||||
## 3. How much is needed (numbers of 2026-10-05 20:00, live: `restart_plan`)
|
||||
|
||||
CLAS has 49 accepted trajectories. At a 28 % share that is a total of 175 accepted trajectories, so about 105 more are
|
||||
needed, all on other kinds: INTF 12, DDLS about 35, FUNC about 15, PROG about 17, TABL 14, STRU 3, MSAG 4, exception 4.
|
||||
Tasks needed (accepted, with about 1.3 trajectory runs per accepted trajectory): TABL +9, INTF +10, STRU +3, MSAG +4,
|
||||
DDLS +20, PROG +5. About 130 trajectory runs at 0.25 ledger = 33 ledger = 27 usage, plus the generation (about 6 usage).
|
||||
Second attempts are part of the 130 runs. If the new reset gives 60 usage, this fits with room for the second teacher test.
|
||||
|
||||
## 4. Checks on the first runs of each new type (do not skip)
|
||||
|
||||
- The first 5 runs of INTF, TABL, STRU, MSAG, exception: records complete (the proxy must pass `sap_push_message`),
|
||||
Qwen conversion works (`train/to_qwen.py` on `accepted.jsonl`: tool call round trip), no harness event.
|
||||
- Acceptance per kind in the summary; a kind under 20 % after 6 tries is skipped automatically: read why (prompt, harness, or the
|
||||
teacher cannot do it) before starting it again.
|
||||
- DDLS: 8 of 17 runs accepted on 2026-10-05; the rejections were budget (60 calls, now 100) and empty responses (32k output limit).
|
||||
If empty responses stay high, lower the output limit for CDS runs or retry once more.
|
||||
|
||||
## 5. Settings that stay
|
||||
|
||||
- Trajectory workers 2 (a harness event sets 1); stop rules: acceptance under 50 % over the last 30 runs, the same harness
|
||||
error three times, the budget guard. Generation deadline 2026-10-10 18:00 has passed: set `--gen-deadline` and
|
||||
`--traj-deadline` on the controller command (for example `--gen-deadline 2026-10-20T18:00 --traj-deadline 2026-10-21T23:30`).
|
||||
- Tool budget 100 for CDS tasks (eval stays 60). 20 tool schemas in every sample. Token note: p95 48k, max 72k.
|
||||
- Summary every 50 accepted trajectories in `train/STATE.md` and `docs/yol-haritasi.md`, with a commit.
|
||||
|
||||
## 5b. Check on 11 October (the last work before the reset; Claude does it when Kral asks)
|
||||
1. `git status` clean and pushed; `python3 -m harness.restart_plan --panel <value>` prints the numbers (panel value from Kral, expected 0 after the reset).
|
||||
2. Services: A4H up, MCP answers, `python3 scripts_probe/lockprobe.py` shows no stale lock (only ATC runtime and debugger listener entries), `python3 -m harness.sweep` shows no leftover.
|
||||
3. `.env`: `BUDGET_CYCLE_START=2026-10-12`, `BUDGET_LIMIT_USD`, `BUDGET_RESERVE_USD=8`; no `STOP` flag, no `STOPPED.txt`; no running controller (`runs/pipeline/controller.lock`).
|
||||
4. Order of the first hour: step 0 eval slots for the new kinds (`evalset plan-new`, `run-new`, `run-new-k`), then the pending tasks of the kinds below target. The 9 trajectories that were
|
||||
moved back (`runs/traj/summary_excluded.jsonl`: FUNC 3, DDLS 5, TABL 1) are pending again and run in the normal order.
|
||||
5. Settings to confirm: workers 2, `STREAM_GUARD` unset for the first runs and then 9000 for 5 DDLS/PROG runs (`docs/empty-response.md`), tool budget 100 for CDS.
|
||||
6. Not started and not to be started before Kral's go: the bf16 memory test (wait until the data is near the size of the first SFT run), second A4H, multi-host.
|
||||
|
||||
## 6. After the restart
|
||||
|
||||
Open decisions: second A4H (not started, multi-host code later), bf16 memory test at 48k, the second teacher test (the
|
||||
reserve), the EPOD requests in `docs/epod-lock-leak.md` and `docs/epod-syntax-hint.md`.
|
||||
77
docs/stage2-build.md
Normal file
77
docs/stage2-build.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Stage 2 training set, build of 2026-10-06 09:03:41
|
||||
|
||||
Builder: `train/build_stage2.py` (output `runs/stage2_data/`, data card `README.md`, report `build_report.json`; this page: `train/build_doc.py`). Hook for item D: `train/hooks_example.py`.
|
||||
HF dataset (private): `erhankeseli/abap-stage2-data`. The local Qwen trajectories (series A) are not read.
|
||||
|
||||
## Result
|
||||
81 accepted DeepSeek trajectories (after the scrub of other runs' leftover objects and the drop of trajectories that read such an object) -> 81 after the eval overlap check
|
||||
-> 75 after the 48k limit (none cut) -> CLAS cap 35 %: 14 CLAS kept, **35 CLAS in reserve** (`stage2_reserve.jsonl`)
|
||||
-> **36 train + 4 valid samples** (0.94 M + 0.09 M tokens, 0.32 M loss tokens in train, p50 25079, p95 41724, max 45118, repair share 0.86).
|
||||
Dropped:
|
||||
- DDLS: {'over_48k': 5}
|
||||
- INTF: {'over_48k': 1}
|
||||
Loss mask checked on every train sample: no span contains a tool result, the system turn or a user turn; loss share about 32 % of the tokens; no token straddles a span boundary.
|
||||
|
||||
## Not good enough yet (kinds under the minimum of 25)
|
||||
| kind | samples | families | short |
|
||||
|---|---|---|---|
|
||||
| CLAS | 14 | 14 | 11 |
|
||||
| INTF | 2 | 2 | 23 |
|
||||
| DDLS | 9 | 9 | 16 |
|
||||
| FUNC | 8 | 8 | 17 |
|
||||
| PROG | 5 | 5 | 20 |
|
||||
| TABL | 2 | 2 | 23 |
|
||||
| STRU | 0 | 0 | 25 |
|
||||
| MSAG | 0 | 0 | 25 |
|
||||
| EXC | 0 | 0 | 25 |
|
||||
|
||||
- **STRU, MSAG, exception: 0 trajectories**; INTF, TABL, PROG only a few. The data is CLAS, DDLS and FUNC. Nothing is filled with copies; the restart plan (new kinds first) has to fix this.
|
||||
- **DDLS loses trajectories to the 48k limit and to the drop of reads of other runs' objects.** The long CDS trajectories (own CDS test class, many reads) are exactly the ones over the limit.
|
||||
Options: raise the limit to 64k (the memory test decides), or generate CDS tasks with shorter runs. Not decided.
|
||||
- **Reads of another run's object:** the test system held leftover objects of earlier runs (the model's own `ZCL_<prefix>_...` classes); the proxy hides them since 2026-10-06. In the older trajectories their names are removed from list results (scrub) and trajectories in which the model read such an object are dropped (`--keep-foreign-reads` keeps them).
|
||||
- Validation: 4 samples ({'CLAS': 1, 'FUNC': 1, 'DDLS': 1, 'PROG': 1}); the new kinds have no validation sample. After the restart the valid set must be rebuilt.
|
||||
- Eval overlap: no accepted task overlaps an eval task (spec cosine 0.75, rules 0.60, names 0.60).
|
||||
|
||||
## Stage 1 : stage 2 ratio (DECIDED 2026-10-06: the 60 % rule)
|
||||
Stage 1 train: 374 documents, 2.53 M tokens (all tokens carry loss). Stage 2 train today: 36 samples, 0.32 M loss tokens per epoch. Loss tokens per option (today's data):
|
||||
|
||||
| stage 1 epochs | stage 2 epochs | stage 1 loss tokens | stage 2 loss tokens | stage 2 share | total tokens seen* |
|
||||
|---|---|---|---|---|---|
|
||||
| 2 | 3 | 5.05 M | 0.95 M | 16 % | 6.0 M |
|
||||
| 1 | 3 | 2.53 M | 0.95 M | 27 % | 3.5 M |
|
||||
| 0.5 | 3 | 1.26 M | 0.95 M | 43 % | 2.2 M |
|
||||
| 0.33 | 3 | 0.83 M | 0.95 M | 53 % | 1.8 M |
|
||||
| 1 | 3 | 2.53 M | 2.85 M | 53 % | 5.4 M |
|
||||
|
||||
(*stage 1 tokens plus all stage 2 tokens per epoch; last row: stage 2 with three times today's data, as expected after the restart.)
|
||||
|
||||
**Proposal: stage 2 for 3 epochs always, stage 1 so that its loss tokens are about two thirds of the stage 2 loss tokens (stage 2 = 60 % of the loss).**
|
||||
Reasons: (1) stage 2 is the behavior we want (repair after the first error: the teacher does it in 98 % of the cases, Qwen in 43 %; write after a few reads; the new kinds), and its samples are long and rare; stage 1 is domain knowledge in document form and acts as a
|
||||
regularizer, so it should not dominate the gradient. (2) The earlier plan of 2 epochs of stage 1 (748 steps) would give 5.1 M stage 1 loss tokens against about 1 M of stage 2: the model would mostly learn documents again.
|
||||
(3) With a few dozen samples more than 3 to 4 epochs of stage 2 risks memorizing them; the valid loss is too thin to catch it, so watch the train loss curve and use the checkpoints.
|
||||
(4) The rule scales with the data: with today's data it means about 0.3 epochs of stage 1, with three times the stage 2 data about 1 epoch. `--s1-epochs` takes fractions.
|
||||
**Decision (Kral + Opus 2026-10-06): this rule.** `train/hf_train_bf16.py --s2-loss-share 0.6` (default) computes the stage 1 epochs from the weighted stage 2 loss tokens.
|
||||
|
||||
## Own-test weights (decided 2026-10-06: down-weight, do not drop; `train/hooks_example.py`, `own_test_weight`)
|
||||
A weight w is the number of copies per epoch (0.5 = a copy in every second epoch on average). The acceptance filter is unchanged.
|
||||
|
||||
| class of the trajectory | weight | reason |
|
||||
|---|---|---|
|
||||
| own tests pass on the correct reference and kill 3 of 4 mutants or more (score >= 0.75) | 1.0 | strong tests, the behavior we want |
|
||||
| reliable, score 0.5 to 0.75 | 0.75 | weaker tests |
|
||||
| reliable, score under 0.5 | 0.5 | tests that miss most faults |
|
||||
| own tests fail on the correct reference (unreliable) | 0.5 | may encode model specific behavior |
|
||||
| no own tests (accepted at 85 points at most) | 0.5 | writing tests is part of the behavior to teach |
|
||||
| no signal (PROG, no mutants, not scored) | 1.0 | neither good nor bad |
|
||||
|
||||
In today's 36 train samples: 15 reliable at 1.0, 4 without signal at 1.0, **15 without own tests at 0.5, 2 unreliable at 0.5**: the expected stage 2 loss tokens per epoch fall from 0.32 M to 0.24 M, which gives about 0.2 epochs of stage 1.
|
||||
Check at the real build: the CLAS cap picks repair trajectories first, and many of them have no own tests; if the share of down-weighted samples stays near half, the weights need a second look.
|
||||
|
||||
## Other decisions of 2026-10-06
|
||||
- **CLAS cap 35 %** stays a build setting (`--clas-cap`); review at the real build (with so little non-CLAS data it throws good CLAS samples into the reserve).
|
||||
- **Length limit:** the memory test decides (`docs/bf16-memory.md`: sweep 16k, 32k, 48k, 64k); the builder keeps 48k until then.
|
||||
- **Foreign names:** list results scrubbed; trajectories with a foreign read are dropped and their tasks go back to the pending pool (`docs/foreign-objects-report.md`).
|
||||
- **Memory test:** waits until the data is near the size of the first SFT run.
|
||||
|
||||
## Also built
|
||||
`train/hf_train_bf16.py` (bf16, loss mask, mixing, memory test), `docs/bf16-memory.md` (memory table by GPU, estimates), `train/hooks_example.py` (hook for item D, weights).
|
||||
22
epod_tests/README.md
Normal file
22
epod_tests/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# EPOD acceptance tests (for the EPOD server developer)
|
||||
|
||||
Small test set for the two server changes that the harness work asked for (details: `docs/epod-syntax-hint.md`, `docs/epod-lock-leak.md`)
|
||||
and for the parallel-call behavior (`harness/loadtest.py` numbers). Standard library only, Python 3.9+, no harness code needed.
|
||||
It uses probe objects `ZEPODT_*` in `$TMP` on a **test system**.
|
||||
|
||||
```sh
|
||||
cd epod_tests
|
||||
export MCP_URL=http://127.0.0.1:3000/mcp MCP_TOKEN=... # the MCP server
|
||||
export A4H_URL=http://localhost:50000 A4H_USER=... A4H_PASSWORD=... # only for the cleanup (ADT deletion); without it the tests list the probe objects
|
||||
python3 run_tests.py # all; --only T1 T3 for some
|
||||
```
|
||||
|
||||
| test | passes when | state of the server on 2026-10-06 |
|
||||
|---|---|---|
|
||||
| T1 syntax_hint | a rejected write (`TYPE c LENGTH 4` in a method signature) returns syntax messages with a line, not only "save operation failed" | FAIL expected (not implemented; the harness proxy adds abaplint messages) |
|
||||
| T2 no_lock_after_kill | a client killed with SIGKILL during a write (4 delays) leaves no lock: the next write works | PASS (not reproducible on A4H) |
|
||||
| T3 parallel_reads | 6 clients search + syntax check in parallel without `Concurrent call detected` | PASS |
|
||||
| T4 parallel_writes | 3 clients create + write in parallel without `Concurrent call detected` | FAIL expected (the shared connection; 15 to 50 lock hits per 45 s in the harness load test) |
|
||||
| T5 lock_error_text | informational | PASS |
|
||||
|
||||
A change in the server is done when T1 and T4 pass and T2 and T3 stay green. T1 accepts any answer that carries a line number or a `syntaxCheck` object with messages (the proxy format is in `docs/epod-syntax-hint.md`).
|
||||
98
epod_tests/epod_client.py
Normal file
98
epod_tests/epod_client.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Minimal MCP client and ADT deletion for the EPOD acceptance tests (standard library only, Python 3.9+)."""
|
||||
import base64
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.request
|
||||
from xml.sax.saxutils import quoteattr
|
||||
|
||||
URL = os.environ.get("MCP_URL", "http://127.0.0.1:3000/mcp")
|
||||
TOKEN = os.environ.get("MCP_TOKEN", "")
|
||||
SYSTEM = os.environ.get("MCP_SYSTEM_ID") # optional: Eclipse project name when several systems are connected
|
||||
|
||||
|
||||
class Mcp:
|
||||
def __init__(self, timeout=300):
|
||||
self.sid, self.n, self.timeout = None, 0, timeout
|
||||
|
||||
def _post(self, body, method="POST"):
|
||||
h = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
|
||||
if TOKEN:
|
||||
h["Authorization"] = "Bearer " + TOKEN
|
||||
if self.sid:
|
||||
h["Mcp-Session-Id"] = self.sid
|
||||
req = urllib.request.Request(URL, json.dumps(body).encode() if body is not None else None, h, method=method)
|
||||
with urllib.request.urlopen(req, timeout=self.timeout) as r:
|
||||
sid = r.headers.get("Mcp-Session-Id")
|
||||
raw = r.read().decode()
|
||||
self.sid = sid or self.sid
|
||||
if "data:" in raw[:40]:
|
||||
raw = "".join(l[5:].strip() for l in raw.splitlines() if l.startswith("data:"))
|
||||
return json.loads(raw) if raw.strip() else None
|
||||
|
||||
def open(self):
|
||||
self.n += 1
|
||||
self._post({"jsonrpc": "2.0", "id": self.n, "method": "initialize", "params": {
|
||||
"protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "epod-acceptance", "version": "1"}}})
|
||||
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._post(None, "DELETE")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self.open()
|
||||
|
||||
def __exit__(self, *a):
|
||||
self.close()
|
||||
|
||||
def call(self, tool, args):
|
||||
"""Returns (is_error, text). 'Concurrent call detected' is returned as it is (the tests count it)."""
|
||||
self.n += 1
|
||||
if SYSTEM:
|
||||
args = dict(args, systemId=SYSTEM)
|
||||
res = self._post({"jsonrpc": "2.0", "id": self.n, "method": "tools/call", "params": {"name": tool, "arguments": args}})
|
||||
if "error" in res:
|
||||
return True, json.dumps(res["error"])
|
||||
r = res["result"]
|
||||
return bool(r.get("isError")), "\n".join(c.get("text", "") for c in r.get("content", []))
|
||||
|
||||
|
||||
class Adt:
|
||||
"""Deletion through the ADT deletion API (needs A4H_URL, A4H_USER, A4H_PASSWORD; A4H_CLIENT default 001)."""
|
||||
|
||||
def __init__(self):
|
||||
self.base = os.environ.get("A4H_URL", "").rstrip("/")
|
||||
self.client = os.environ.get("A4H_CLIENT", "001")
|
||||
self.auth = "Basic " + base64.b64encode(("%s:%s" % (os.environ.get("A4H_USER", ""), os.environ.get("A4H_PASSWORD", ""))).encode()).decode()
|
||||
self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
||||
self.csrf = None
|
||||
|
||||
def available(self):
|
||||
return bool(self.base and os.environ.get("A4H_USER"))
|
||||
|
||||
def _req(self, path, body=None, headers=None):
|
||||
h = {"Authorization": self.auth}
|
||||
if self.csrf:
|
||||
h["x-csrf-token"] = self.csrf
|
||||
h.update(headers or {})
|
||||
req = urllib.request.Request("%s%s%ssap-client=%s" % (self.base, path, "&" if "?" in path else "?", self.client), body.encode() if body else None, h)
|
||||
with self.opener.open(req, timeout=120) as r:
|
||||
return dict(r.headers), r.read().decode()
|
||||
|
||||
def delete(self, uris):
|
||||
if not self.available() or not uris:
|
||||
return {}
|
||||
hdr, _ = self._req("/sap/bc/adt/discovery", headers={"x-csrf-token": "fetch", "Accept": "*/*"})
|
||||
self.csrf = hdr.get("x-csrf-token") or hdr.get("X-CSRF-Token")
|
||||
objs = "".join("<del:object adtcore:uri=%s><del:transportNumber/></del:object>" % quoteattr(u) for u in uris)
|
||||
body = ('<?xml version="1.0" encoding="UTF-8"?><del:deletionRequest xmlns:del="http://www.sap.com/adt/deletion" '
|
||||
'xmlns:adtcore="http://www.sap.com/adt/core">' + objs + "</del:deletionRequest>")
|
||||
_, text = self._req("/sap/bc/adt/deletion/delete", body, {"Content-Type": "application/vnd.sap.adt.deletion.request.v1+xml",
|
||||
"Accept": "application/vnd.sap.adt.deletion.response.v1+xml"})
|
||||
return {m.group(1): 'isDeleted="true"' in m.group(0) for m in re.finditer(r'<del:object\b[^>]*adtcore:uri="([^"]+)"[^>]*>', text)}
|
||||
32
epod_tests/epod_tests_result.json
Normal file
32
epod_tests/epod_tests_result.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"id": "T1",
|
||||
"name": "syntax_hint",
|
||||
"pass": false,
|
||||
"detail": "the failed write returned: {\"success\":false,\"error\":\"[WRITE] An error occured during the save operation. The changes were not stored.\"}"
|
||||
},
|
||||
{
|
||||
"id": "T2",
|
||||
"name": "no_lock_after_kill",
|
||||
"pass": true,
|
||||
"detail": "4 kills (0.05 to 0.7 s), the next write always worked"
|
||||
},
|
||||
{
|
||||
"id": "T3",
|
||||
"name": "parallel_reads",
|
||||
"pass": true,
|
||||
"detail": "372 rounds, 0 'Concurrent call detected'"
|
||||
},
|
||||
{
|
||||
"id": "T4",
|
||||
"name": "parallel_writes",
|
||||
"pass": false,
|
||||
"detail": "27 create+write rounds with 3 clients, 12 'Concurrent call detected'"
|
||||
},
|
||||
{
|
||||
"id": "T5",
|
||||
"name": "lock_error_text",
|
||||
"pass": true,
|
||||
"detail": "informational: see docs/epod-lock-leak.md (no way to create a lock on purpose)"
|
||||
}
|
||||
]
|
||||
174
epod_tests/run_tests.py
Normal file
174
epod_tests/run_tests.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""EPOD acceptance tests (2026-10-06). Run against an EPOD MCP server on a test system (probe objects ZEPODT_*, package $TMP).
|
||||
|
||||
MCP_URL=http://127.0.0.1:3000/mcp MCP_TOKEN=... [A4H_URL=http://localhost:50000 A4H_USER=... A4H_PASSWORD=...] python3 run_tests.py [--only T1 T2 ...]
|
||||
|
||||
Each test prints PASS or FAIL with the reason and writes the result to epod_tests_result.json. Tests:
|
||||
T1 syntax_hint a rejected write ('save operation failed') comes with syntax messages (line and text) docs/epod-syntax-hint.md
|
||||
T2 no_lock_after_kill a client killed during a write leaves no lock (the next write and the deletion work) docs/epod-lock-leak.md
|
||||
T3 parallel_reads 6 clients read/ATC/syntax-check in parallel without 'Concurrent call detected'
|
||||
T4 parallel_writes 3 clients create/write/activate in parallel without 'Concurrent call detected' (a fix: per system or per session lock)
|
||||
T5 lock_error_text a write on a locked object names the lock owner (informational)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from epod_client import Adt, Mcp # noqa: E402
|
||||
|
||||
CLS = """CLASS {n} DEFINITION PUBLIC FINAL CREATE PUBLIC.
|
||||
PUBLIC SECTION.
|
||||
METHODS run RETURNING VALUE(rv) TYPE i.
|
||||
ENDCLASS.
|
||||
CLASS {n} IMPLEMENTATION.
|
||||
METHOD run.
|
||||
rv = 1.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
"""
|
||||
BAD = """CLASS {n} DEFINITION PUBLIC FINAL CREATE PUBLIC.
|
||||
PUBLIC SECTION.
|
||||
METHODS run IMPORTING iv_zone TYPE c LENGTH 4 RETURNING VALUE(rv) TYPE i.
|
||||
ENDCLASS.
|
||||
CLASS {n} IMPLEMENTATION.
|
||||
METHOD run.
|
||||
rv = 1.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
"""
|
||||
TC = """CLASS ltc DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
METHODS t1 FOR TESTING.
|
||||
ENDCLASS.
|
||||
CLASS ltc IMPLEMENTATION.
|
||||
METHOD t1.
|
||||
cl_abap_unit_assert=>assert_equals( act = 1 exp = 1 ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
"""
|
||||
created = []
|
||||
RUN = time.strftime("%H%M%S")
|
||||
|
||||
|
||||
def new_class(m, tag):
|
||||
name = "ZEPODT_%s_%s" % (tag, RUN)
|
||||
m.call("sap_create_object", {"objectType": "CLAS", "objectName": name, "packageName": "$TMP", "description": "epod acceptance test"})
|
||||
created.append("/sap/bc/adt/oo/classes/" + name.lower())
|
||||
return name
|
||||
|
||||
|
||||
def ok(text):
|
||||
return '"success":true' in text.replace(" ", "")
|
||||
|
||||
|
||||
def t1_syntax_hint():
|
||||
with Mcp() as m:
|
||||
n = new_class(m, "T1")
|
||||
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": n, "source": BAD.format(n=n.lower())})
|
||||
low = t.lower()
|
||||
has_detail = ("syntaxcheck" in low or "syntax" in low and "line" in low) and "save operation" in low or "line 3" in low or '"line":3' in low.replace(" ", "")
|
||||
return bool(has_detail), "the failed write returned: " + t[:300]
|
||||
|
||||
|
||||
def t2_no_lock_after_kill():
|
||||
victim = ("import sys,json,os\nsys.path.insert(0,%r)\nfrom epod_client import Mcp\na=json.loads(sys.argv[1])\nm=Mcp().open()\nprint('SENT',flush=True)\nm.call('sap_push_source',a)\n"
|
||||
% os.path.dirname(os.path.abspath(__file__)))
|
||||
bad = []
|
||||
for delay in (0.05, 0.2, 0.4, 0.7):
|
||||
with Mcp() as m:
|
||||
n = new_class(m, "T2")
|
||||
m.call("sap_push_source", {"objectType": "CLAS", "objectName": n, "source": CLS.format(n=n.lower())})
|
||||
p = subprocess.Popen([sys.executable, "-c", victim, json.dumps({"objectType": "CLAS", "objectName": n, "includeType": "testclasses", "source": TC})],
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
p.stdout.readline()
|
||||
time.sleep(delay)
|
||||
try:
|
||||
os.kill(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
p.wait()
|
||||
time.sleep(3)
|
||||
with Mcp() as m:
|
||||
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": n, "includeType": "testclasses", "source": TC + "* again\n"})
|
||||
if not ok(t):
|
||||
bad.append((delay, t[:160]))
|
||||
return not bad, "after the kill the next write failed at: %s" % bad if bad else "4 kills (0.05 to 0.7 s), the next write always worked"
|
||||
|
||||
|
||||
def parallel(n_clients, seconds, work):
|
||||
out, end = [], time.time() + seconds
|
||||
|
||||
def run(i):
|
||||
locks = calls = 0
|
||||
with Mcp() as m:
|
||||
while time.time() < end:
|
||||
t = work(m, i)
|
||||
calls += 1
|
||||
locks += "Concurrent call detected" in t
|
||||
out.append((calls, locks))
|
||||
th = [threading.Thread(target=run, args=(i,)) for i in range(n_clients)]
|
||||
[x.start() for x in th]
|
||||
[x.join() for x in th]
|
||||
return sum(c for c, _ in out), sum(l for _, l in out)
|
||||
|
||||
|
||||
def t3_parallel_reads():
|
||||
def work(m, i):
|
||||
return m.call("sap_search_object", {"query": "CL_ABAP_CHAR_UTIL*", "objType": "CLAS"})[1] + \
|
||||
m.call("sap_syntax_check", {"objectType": "CLAS", "objectName": "CL_ABAP_CHAR_UTILITIES"})[1]
|
||||
calls, locks = parallel(6, 15, work)
|
||||
return locks == 0, "%d rounds, %d 'Concurrent call detected'" % (calls, locks)
|
||||
|
||||
|
||||
def t4_parallel_writes():
|
||||
cnt = {"n": 0}
|
||||
|
||||
def work(m, i):
|
||||
cnt["n"] += 1
|
||||
name = "ZEPODT_P%d_%s%03d" % (i, RUN, cnt["n"] % 1000)
|
||||
t = m.call("sap_create_object", {"objectType": "CLAS", "objectName": name, "packageName": "$TMP", "description": "parallel"})[1]
|
||||
created.append("/sap/bc/adt/oo/classes/" + name.lower())
|
||||
return t + m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "source": CLS.format(n=name.lower())})[1]
|
||||
calls, locks = parallel(3, 25, work)
|
||||
return locks == 0, "%d create+write rounds with 3 clients, %d 'Concurrent call detected'" % (calls, locks)
|
||||
|
||||
|
||||
def t5_lock_error_text():
|
||||
return True, "informational: see docs/epod-lock-leak.md (no way to create a lock on purpose)"
|
||||
|
||||
|
||||
TESTS = [("T1", "syntax_hint", t1_syntax_hint), ("T2", "no_lock_after_kill", t2_no_lock_after_kill), ("T3", "parallel_reads", t3_parallel_reads),
|
||||
("T4", "parallel_writes", t4_parallel_writes), ("T5", "lock_error_text", t5_lock_error_text)]
|
||||
|
||||
|
||||
def main():
|
||||
only = set(sys.argv[sys.argv.index("--only") + 1:]) if "--only" in sys.argv else None
|
||||
result = []
|
||||
for tid, name, fn in TESTS:
|
||||
if only and tid not in only:
|
||||
continue
|
||||
t0 = time.time()
|
||||
try:
|
||||
passed, why = fn()
|
||||
except Exception as e: # noqa: BLE001
|
||||
passed, why = False, "error: %r" % e
|
||||
print("%-4s %-22s %s (%.0f s) %s" % (tid, name, "PASS" if passed else "FAIL", time.time() - t0, why[:230]), flush=True)
|
||||
result.append({"id": tid, "name": name, "pass": passed, "detail": why[:600]})
|
||||
adt = Adt()
|
||||
if adt.available() and created:
|
||||
try:
|
||||
gone = adt.delete(created)
|
||||
print("cleanup: %d of %d probe objects deleted" % (sum(gone.values()), len(created)))
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("cleanup failed:", repr(e)[:200])
|
||||
elif created:
|
||||
print("delete these probe objects yourself (SE80 or ADT): ZEPODT_* in $TMP (%d objects)" % len(created))
|
||||
json.dump(result, open("epod_tests_result.json", "w"), indent=1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,9 +4,20 @@ import os
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import threading
|
||||
import urllib.error
|
||||
|
||||
from .ledger import add_usage, check_budget
|
||||
from .proxy import BudgetExceeded
|
||||
|
||||
|
||||
class ServerDown(Exception):
|
||||
"""The model server does not answer (remote local model). The run ends cleanly and is not a result."""
|
||||
|
||||
|
||||
class WindowEnd(Exception):
|
||||
"""The time window of the series is over (hard stop, also in the middle of a request)."""
|
||||
|
||||
SYSTEM_PROMPT = """You are an ABAP developer. You implement a plan on an SAP system with the tools.
|
||||
|
||||
Rules:
|
||||
@@ -53,6 +64,8 @@ class OracleAgent:
|
||||
ident["functionGroup"] = o["functionGroup"]
|
||||
proxy.call("sap_create_object", dict(ident, packageName="$TMP",
|
||||
description=o.get("description", o["name"])[:60]))
|
||||
if o.get("messages"): # message class: messages are written with sap_push_message
|
||||
proxy.call("sap_push_message", {"objectName": o["name"], "messages": o["messages"]})
|
||||
if o.get("source"):
|
||||
proxy.call("sap_push_source", dict(ident, source=o["source"]))
|
||||
if o.get("testclasses_source"):
|
||||
@@ -70,7 +83,8 @@ class LlmAgent:
|
||||
"""
|
||||
|
||||
def __init__(self, model, base_url=None, api_key=None, max_turns=80, temperature=0.2,
|
||||
max_seconds=None, max_tokens=None, chat_template_kwargs=None, loop_guard=None):
|
||||
max_seconds=None, max_tokens=None, chat_template_kwargs=None, loop_guard=None,
|
||||
deadline=None, watch=False, empty_retries=2, retry_temperature=None, stream_guard=None):
|
||||
self.model = model
|
||||
self.name = f"llm:{model}"
|
||||
self.base_url = (base_url or os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")).rstrip("/")
|
||||
@@ -82,15 +96,22 @@ class LlmAgent:
|
||||
# Runaway reasoning: 35 of 1965 DeepSeek turns produced 393k output tokens and no content (46 % of the
|
||||
# run cost, 2026-10-03). Normal turns: p95 17.5k, max 82k. A cut turn is retried (see run).
|
||||
self.max_tokens = max_tokens or (32000 if ":cloud" in (model or "") else None)
|
||||
self.empty_retries = 2
|
||||
self.empty_retries = empty_retries
|
||||
# empty_response fix (2026-10-06): a retry after an empty turn can use another temperature (the same sample often
|
||||
# runs away again: 7 of 13 empty runs had two or three capped turns in a row); stream_guard = reasoning tokens
|
||||
# after which a streamed turn without any content or tool call is cut and counted as an empty turn
|
||||
self.retry_temperature = retry_temperature
|
||||
self.stream_guard = stream_guard
|
||||
self.loop_guard = loop_guard # end the run after this many identical pushes in a row (None = off)
|
||||
self.deadline = deadline # absolute time (time.time()) of the hard stop, or None
|
||||
self.watch = watch # remote model: ping the server during a request, end the run when it is gone
|
||||
self.end_reason = None
|
||||
self.messages, self.tools, self.reasoning, self.turn_usage = [], [], [], [] # for the trajectory record
|
||||
self.chat_template_kwargs = chat_template_kwargs # local server only, e.g. {"enable_thinking": False}
|
||||
|
||||
def _chat(self, messages, tools):
|
||||
def _chat(self, messages, tools, temperature=None):
|
||||
body = {"model": self.model, "messages": messages, "tools": tools,
|
||||
"temperature": self.temperature, "parallel_tool_calls": False}
|
||||
"temperature": temperature if temperature is not None else self.temperature, "parallel_tool_calls": False}
|
||||
if self.max_tokens:
|
||||
body["max_tokens"] = self.max_tokens
|
||||
if self.chat_template_kwargs:
|
||||
@@ -99,19 +120,110 @@ class LlmAgent:
|
||||
{"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"})
|
||||
last = None
|
||||
if self.stream_guard and not (self.watch or self.deadline):
|
||||
try:
|
||||
return self._chat_stream(dict(body), messages)
|
||||
except Exception as e: # noqa: BLE001 streaming not usable (server, parse): the normal request below
|
||||
last = e
|
||||
for attempt in range(4): # model server errors (HTTP 5xx, timeouts): retry with backoff
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=self.request_timeout) as r:
|
||||
data = json.loads(r.read().decode())
|
||||
return data["choices"][0]["message"], data.get("usage", {})
|
||||
if self.watch or self.deadline:
|
||||
data = self._post_watched(req)
|
||||
else:
|
||||
with urllib.request.urlopen(req, timeout=self.request_timeout) as r:
|
||||
data = json.loads(r.read().decode())
|
||||
return data["choices"][0]["message"], data.get("usage", {})
|
||||
except (ServerDown, WindowEnd):
|
||||
raise
|
||||
except Exception as e: # noqa: BLE001
|
||||
last = e
|
||||
code = getattr(e, "code", None)
|
||||
if code is not None and code < 500 and code != 429:
|
||||
raise
|
||||
if self.deadline and time.time() >= self.deadline:
|
||||
raise WindowEnd()
|
||||
if self.watch and not self._ping():
|
||||
raise ServerDown(str(e)[:200])
|
||||
time.sleep(10 * (attempt + 1))
|
||||
raise RuntimeError(f"model request failed after retries: {last}")
|
||||
|
||||
def _chat_stream(self, body, messages):
|
||||
"""Streamed request. A turn that has produced only reasoning for `stream_guard` tokens (about 3.2 characters per
|
||||
token) and no content and no tool call is cut and returned as an empty turn (the run loop retries it)."""
|
||||
body["stream"] = True
|
||||
body["stream_options"] = {"include_usage": True}
|
||||
req = urllib.request.Request(f"{self.base_url}/chat/completions", json.dumps(body).encode(),
|
||||
{"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"})
|
||||
content, reasoning, calls, usage, cut = "", 0, {}, {}, False
|
||||
with urllib.request.urlopen(req, timeout=self.request_timeout) as r:
|
||||
for raw in r:
|
||||
line = raw.decode("utf-8", "replace").strip()
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
data = line[5:].strip()
|
||||
if data == "[DONE]":
|
||||
break
|
||||
chunk = json.loads(data)
|
||||
if chunk.get("usage"):
|
||||
usage = chunk["usage"]
|
||||
for ch in chunk.get("choices") or []:
|
||||
d = ch.get("delta") or {}
|
||||
content += d.get("content") or ""
|
||||
reasoning += len(d.get("reasoning") or d.get("reasoning_content") or d.get("thinking") or "")
|
||||
for tc in d.get("tool_calls") or []:
|
||||
c = calls.setdefault(tc.get("index", 0), {"id": tc.get("id") or "", "type": "function",
|
||||
"function": {"name": "", "arguments": ""}})
|
||||
c["id"] = c["id"] or tc.get("id") or ""
|
||||
f = tc.get("function") or {}
|
||||
c["function"]["name"] += f.get("name") or ""
|
||||
a = f.get("arguments")
|
||||
c["function"]["arguments"] += a if isinstance(a, str) else json.dumps(a) if a else ""
|
||||
if not content.strip() and not calls and reasoning / 3.2 >= self.stream_guard:
|
||||
cut = True
|
||||
break
|
||||
if cut:
|
||||
est = {"prompt_tokens": int(len(json.dumps(messages)) / 3.5), "completion_tokens": int(reasoning / 3.2),
|
||||
"estimated": True, "cut_by_stream_guard": True}
|
||||
return {"role": "assistant", "content": ""}, est
|
||||
msg = {"role": "assistant", "content": content or None}
|
||||
if calls:
|
||||
msg["tool_calls"] = [calls[k] for k in sorted(calls)]
|
||||
return msg, usage
|
||||
|
||||
def _ping(self, timeout=10):
|
||||
try:
|
||||
urllib.request.urlopen(urllib.request.Request(f"{self.base_url}/models"), timeout=timeout).read()
|
||||
return True
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
def _post_watched(self, req):
|
||||
"""The request runs in a thread; this thread watches the deadline and (remote model) the server: a hard stop
|
||||
or a dead server ends the wait at once, also in the middle of a long generation."""
|
||||
box = {}
|
||||
|
||||
def work():
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=self.request_timeout) as r:
|
||||
box["data"] = json.loads(r.read().decode())
|
||||
except BaseException as e: # noqa: BLE001
|
||||
box["err"] = e
|
||||
th = threading.Thread(target=work, daemon=True)
|
||||
th.start()
|
||||
last_ping, fails = time.time(), 0
|
||||
while th.is_alive():
|
||||
th.join(5)
|
||||
if self.deadline and time.time() >= self.deadline:
|
||||
raise WindowEnd()
|
||||
if self.watch and th.is_alive() and time.time() - last_ping >= 30:
|
||||
last_ping = time.time()
|
||||
fails = 0 if self._ping() else fails + 1
|
||||
if fails >= 3:
|
||||
raise ServerDown("no answer to 3 pings in a row during a request")
|
||||
if "err" in box:
|
||||
raise box["err"]
|
||||
return box["data"]
|
||||
|
||||
def run(self, task, proxy):
|
||||
tools = [{"type": "function", "function": {"name": t["name"],
|
||||
"description": t.get("description", ""),
|
||||
@@ -127,14 +239,27 @@ class LlmAgent:
|
||||
self.end_reason = "max_turns"
|
||||
start = time.time()
|
||||
for _ in range(self.max_turns):
|
||||
if self.deadline and time.time() >= self.deadline:
|
||||
final = "Stopped: time window over."
|
||||
self.end_reason = "window_end"
|
||||
break
|
||||
if self.max_seconds and time.time() - start > self.max_seconds:
|
||||
final = f"Stopped: time budget exceeded ({self.max_seconds} s)."
|
||||
self.end_reason = "time_budget"
|
||||
break
|
||||
try:
|
||||
msg, usage = self._chat(messages, tools)
|
||||
msg, usage = self._chat(messages, tools,
|
||||
self.retry_temperature if (empty and self.retry_temperature) else None)
|
||||
add_usage(self.model, usage, kind="run", ref=proxy.prefix)
|
||||
self.turn_usage.append(usage)
|
||||
except WindowEnd:
|
||||
final = "Stopped: time window over."
|
||||
self.end_reason = "window_end"
|
||||
break
|
||||
except ServerDown as e:
|
||||
final = f"Stopped: model server not reachable ({e})."
|
||||
self.end_reason = "server_down"
|
||||
break
|
||||
except RuntimeError as e:
|
||||
final = f"Stopped: {e}"
|
||||
self.end_reason = "model_error"
|
||||
|
||||
@@ -17,6 +17,7 @@ import time
|
||||
import urllib.request
|
||||
|
||||
from .adt_client import load_env
|
||||
from . import mix
|
||||
from .ledger import _env_budget, spent
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -242,6 +243,104 @@ def agg_table(title, evs, rows, key, note=""):
|
||||
% (E(title), E(key.replace("_", " ")), trs, "<div class=note>%s</div>" % E(note) if note else ""))
|
||||
|
||||
|
||||
def local_card():
|
||||
"""Card for series A (local Qwen on the MacBook): state.json of harness.localqwen. Empty string without a series."""
|
||||
path = os.path.join(ROOT, "runs", "local_qwen", "state.json")
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
try:
|
||||
st = json.load(open(path))
|
||||
except ValueError:
|
||||
return ""
|
||||
now = time.time()
|
||||
url = st.get("base_url") or ""
|
||||
up = ping(url + "/models", timeout=4) if url else False # the dashboard asks the MacBook itself
|
||||
status = st.get("status", "?")
|
||||
left = st.get("window_start", now) + st.get("window_hours", 12) * 3600 - now
|
||||
cur = st.get("current")
|
||||
plan, res = st.get("plan", []), st.get("results", [])
|
||||
chips = "<span class='chip %s'>server %s</span> <span class='chip %s'>%s</span>" % (
|
||||
"ok" if up else "er", "answers" if up else "does not answer",
|
||||
{"running": "ok", "paused": "wa", "finished": "gr", "window_over": "gr"}.get(status, "gr"), E(status))
|
||||
rows = ""
|
||||
per = {}
|
||||
for r in res:
|
||||
k = per.setdefault(r["kind"], {"runs": 0, "ok": 0, "scores": [], "fail": {}})
|
||||
k["runs"] += 1
|
||||
k["ok"] += r["accepted"]
|
||||
if r.get("score") is not None:
|
||||
k["scores"].append(r["score"])
|
||||
for f in r.get("failure_types", []):
|
||||
k["fail"][f] = k["fail"].get(f, 0) + 1
|
||||
planned = {}
|
||||
for p_ in plan:
|
||||
planned[p_["kind"]] = planned.get(p_["kind"], 0) + 1
|
||||
for kind in [x for x in ("INTF", "TABL", "STRU", "MSAG", "EXC", "DDLS") if x in planned or x in per]:
|
||||
k = per.get(kind, {"runs": 0, "ok": 0, "scores": [], "fail": {}})
|
||||
mean = "%.0f" % (sum(k["scores"]) / len(k["scores"])) if k["scores"] else "-"
|
||||
rate = "%.0f%%" % (100.0 * k["ok"] / k["runs"]) if k["runs"] else "-"
|
||||
fails = ", ".join("%s %d" % (f, n) for f, n in sorted(k["fail"].items(), key=lambda x: -x[1])) or "-"
|
||||
rows += ("<tr><td>%s</td><td class=n>%d/%d</td><td class=n>%d</td><td class=n>%s</td><td class=n>%s</td><td>%s</td></tr>"
|
||||
% (kind, k["runs"], planned.get(kind, 0), k["ok"], rate, mean, E(fails)))
|
||||
if cur:
|
||||
elapsed = (now - cur["start"]) / 60
|
||||
curtxt = "%s (%s, %s), run %d min%s" % (cur["task"], cur["kind"], cur["category"], elapsed,
|
||||
", restart %d" % cur["restarts"] if cur.get("restarts") else "")
|
||||
else:
|
||||
curtxt = "none (%s)" % status
|
||||
evs = "".join("<div class=note>%s %s %s: %s</div>" % (time.strftime("%H:%M", time.localtime(e["t"])), E(e["kind"]),
|
||||
E(e["task"]), E(e["text"])) for e in st.get("events", [])[-3:])
|
||||
hdr = ("<div class=card style='grid-column:1/-1'><h2>Series A: local Qwen 3.8 27B on the MacBook (thinking off, 16384 tokens, loop guard 3, budget 60)</h2>"
|
||||
"<div class=b>%s model <span class=mono>%s</span></div>"
|
||||
"<div class=b><table><tr><td>Current task</td><td>%s</td><td>Done / planned</td><td class=n>%d / %d</td>"
|
||||
"<td>Time left in the window</td><td class=n>%s</td><td>Paused</td><td class=n>%.0f min</td></tr></table></div>"
|
||||
% (chips, E(os.path.basename(str(st.get("model_id", "")))), E(curtxt), len(res), len(plan),
|
||||
("%dh %02dm" % (left // 3600, (left % 3600) // 60)) if left > 0 else "over", st.get("paused_seconds", 0) / 60))
|
||||
tbl = ("<div class=b><table><tr><th>kind</th><th class=n>runs/planned</th><th class=n>accepted</th><th class=n>pass rate</th>"
|
||||
"<th class=n>mean score</th><th>failure types</th></tr>%s</table></div>%s"
|
||||
"<div class=note>Accepted trajectories of this series are kept apart (runs/local_qwen/accepted.jsonl), not for the first SFT.</div></div>" % (rows, evs))
|
||||
return hdr + tbl
|
||||
|
||||
|
||||
def work_card():
|
||||
"""Progress of the no-cloud work packages (runs/dashboard/work.json, updated by Claude after each item)."""
|
||||
path = os.path.join(OUT_DIR, "work.json")
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
try:
|
||||
w = json.load(open(path))
|
||||
except ValueError:
|
||||
return ""
|
||||
cls = {"done": "ok", "in progress": "wa", "waiting": "gr", "parked": "gr", "blocked": "er"}
|
||||
rows = "".join("<tr><td><b>%s</b></td><td>%s</td><td><span class='chip %s'>%s</span></td><td>%s</td></tr>" % (
|
||||
E(i["id"]), E(i["text"]), cls.get(i["status"], "gr"), E(i["status"]), E(i.get("note", ""))) for i in w.get("items", []))
|
||||
return ("<div class=card style='grid-column:1/-1'><h2>%s</h2><div class=b><table><tr><th>item</th><th>work</th><th>status</th><th>result</th></tr>%s</table></div>"
|
||||
"<div class=note>updated %s</div></div>" % (E(w.get("title", "")), rows, time.strftime("%H:%M", time.localtime(w.get("updated", 0)))))
|
||||
|
||||
|
||||
def mix_table(rows, evs):
|
||||
kt = mix.accepted_task_counts()
|
||||
kr = {}
|
||||
for r, e in zip(rows, evs):
|
||||
k = mix.kind_of_task_dir(r["task"])
|
||||
a = kr.setdefault(k, [0, 0])
|
||||
a[0] += 1
|
||||
a[1] += e["accepted"]
|
||||
tt, tr = max(sum(kt.values()), 1), max(sum(v[1] for v in kr.values()), 1)
|
||||
ss = sum(mix.TYPE_SHARE.values())
|
||||
trs = ""
|
||||
for k, v in mix.TYPE_SHARE.items():
|
||||
tgt = 100.0 * v / ss
|
||||
pt, pr = 100.0 * kt.get(k, 0) / tt, 100.0 * kr.get(k, [0, 0])[1] / tr
|
||||
cls = "g" if abs(pt - tgt) < 4 else ("w" if abs(pt - tgt) < 10 else "r")
|
||||
trs += ("<tr><td>%s</td><td class=n>%.0f%%</td><td class=n>%d (%.0f%%)</td><td style='width:25%%'>%s</td>"
|
||||
"<td class=n>%d/%d (%.0f%%)</td></tr>" % (k, tgt, kt.get(k, 0), pt, bar(pt, max(tgt * 2, 1), cls),
|
||||
kr.get(k, [0, 0])[1], kr.get(k, [0, 0])[0], pr))
|
||||
return ("<div class=card><h2>Object type mix vs target</h2><div class=b><table><tr><th>kind</th><th class=n>target</th>"
|
||||
"<th class=n>tasks</th><th>vs target</th><th class=n>trajectories acc/runs</th></tr>%s</table></div>"
|
||||
"<div class=note>CLAS/INTF 35, CDS 25, FUNC 15, PROG 10, DDIC 10, MSAG + exception 5 (percent of accepted tasks).</div></div>" % trs)
|
||||
|
||||
|
||||
def gen_table(logs, key, title):
|
||||
agg = {}
|
||||
for l in logs:
|
||||
@@ -270,13 +369,17 @@ def render(d):
|
||||
tasks_with_run = {r["task"] for r in rows}
|
||||
backlog = sum(1 for l in logs if l.get("accepted") and l["id"] not in tasks_with_run)
|
||||
alive = any("harness.pipeline" in p for p in d["procs"])
|
||||
status, scls = ("RUNNING", "ok") if alive and not d["stopped"] else (("STOPPED", "er") if d["stopped"] else ("NOT RUNNING", "er"))
|
||||
if alive: # a STOPPED.txt of an earlier run does not count while a controller runs
|
||||
d["stopped"] = None
|
||||
status, scls = ("RUNNING", "ok") if alive else (("STOPPED", "er") if d["stopped"] else ("NOT RUNNING", "er"))
|
||||
hints = sum(e["hints"] for e in evs)
|
||||
ctx = sorted(e["ctx"] for e in acc if e["ctx"])
|
||||
next50 = (n_acc // 50 + 1) * 50
|
||||
est = d["panel_est"]
|
||||
pcls = "good" if est < 50 else ("warn" if est < 57 else "bad")
|
||||
eta = ("%.1f h (%s)" % (d["eta_h"], time.strftime("%a %H:%M", time.localtime(d["now"] + d["eta_h"] * 3600)))) if d["eta_h"] else "n/a"
|
||||
eta = ("%.1f h (%s)" % (d["eta_h"], time.strftime("%a %H:%M", time.localtime(d["now"] + d["eta_h"] * 3600)))) if d["eta_h"] and alive else "n/a"
|
||||
if not alive: # a stopped pipeline has no burn rate and no speed
|
||||
d["burn_ledger_h"] = d["rate_runs_h"] = d["rate_acc_h"] = None
|
||||
a30 = d["acc30"]
|
||||
a30s = "%.0f %%" % (100 * a30) if a30 is not None else "n/a (<30 runs)"
|
||||
cost_per = d["run_cost"] / n_acc if n_acc else 0
|
||||
@@ -350,8 +453,8 @@ def render(d):
|
||||
% ("ok" if d["a4h"] else "er", "up" if d["a4h"] else "down", "ok" if d["mcp"] else "er", "up" if d["mcp"] else "down", scls, status,
|
||||
E("\n".join(d["procs"]))))
|
||||
logc = "<div class=card><h2>Pipeline log</h2><div class=b><div class='log mono'>%s</div></div></div>" % E("\n".join(d["log_tail"]))
|
||||
body = (banner + "<div class=tiles>" + tiles + "</div><div class=grid>" + budget + prog + stops + sysc
|
||||
+ agg_table("Trajectories by category", evs, rows, "category") + agg_table("Trajectories by object type", evs, rows, "object_type")
|
||||
body = (banner + "<div class=tiles>" + tiles + "</div><div class=grid>" + work_card() + local_card() + budget + prog + stops + sysc
|
||||
+ mix_table(rows, evs) + agg_table("Trajectories by category", evs, rows, "category") + agg_table("Trajectories by object type", evs, rows, "object_type")
|
||||
+ gen_table(logs, "category", "Task generation by category") + gen_table(logs, "object_type", "Task generation by object type")
|
||||
+ tok + recent + logc + "</div>")
|
||||
stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(d["now"]))
|
||||
@@ -372,6 +475,10 @@ def write_once():
|
||||
tmp = OUT + ".tmp"
|
||||
open(tmp, "w").write(page)
|
||||
os.replace(tmp, OUT)
|
||||
pub = os.path.join(OUT_DIR, "public") # the only folder that is served on the local network
|
||||
os.makedirs(pub, exist_ok=True)
|
||||
open(os.path.join(pub, "index.html.tmp"), "w").write(page)
|
||||
os.replace(os.path.join(pub, "index.html.tmp"), os.path.join(pub, "index.html"))
|
||||
last = d["hist"][-1]
|
||||
with open(HISTORY, "a") as f:
|
||||
f.write(json.dumps(last) + "\n")
|
||||
|
||||
@@ -40,6 +40,43 @@ def candidates():
|
||||
return out
|
||||
|
||||
|
||||
def decision(score, calls):
|
||||
"""The filter decisions of step F: an easy candidate (DeepSeek >= 95 and <= 20 tool calls, tasks_gen/eval/easy_candidates.json),
|
||||
a flag (a strong model fails, score under 50, although the reference passes: the spec may be unclear), else normal."""
|
||||
if score is None:
|
||||
return "no result"
|
||||
if score >= 95 and (calls or 99) <= 20:
|
||||
return "easy candidate"
|
||||
return "flag: strong model fails" if score < 50 else "normal"
|
||||
|
||||
|
||||
def rerun(a):
|
||||
"""Rerun some tasks (after the proxy fix of 2026-10-06) WITHOUT touching empirical.json or the eval set: results go to runs/emp_rerun/,
|
||||
then the old and the new filter decision are printed. A changed decision is reported to Kral and Opus before anything in the eval set changes."""
|
||||
out_dir = os.path.join(ROOT, "runs", "emp_rerun")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
runner = Runner(POOL, out_dir)
|
||||
changed = []
|
||||
for i, tid in enumerate(a.tasks):
|
||||
old = _json(os.path.join(POOL, tid, "empirical.json"), {}).get(a.model, {})
|
||||
try:
|
||||
rep, run_dir = runner.run(tid, LlmAgent(a.model, a.base_url), a.run_base + i)
|
||||
except BudgetExceeded as e:
|
||||
print("BUDGET", e, flush=True)
|
||||
break
|
||||
h = rep.get("hidden_tests") or {}
|
||||
new = {"score": (rep.get("score") or {}).get("total"), "hidden": f"{h.get('passed')}/{h.get('total')}", "tool_calls": rep.get("tool_calls"),
|
||||
"end_reason": rep.get("end_reason"), "run_dir": os.path.relpath(run_dir, ROOT)}
|
||||
d_old, d_new = decision(old.get("score"), old.get("tool_calls")), decision(new["score"], new["tool_calls"])
|
||||
line = {"task": tid, "old": {k: old.get(k) for k in ("score", "hidden", "tool_calls")}, "new": new, "decision_old": d_old, "decision_new": d_new,
|
||||
"decision_changed": d_old != d_new}
|
||||
open(os.path.join(out_dir, "results.jsonl"), "a").write(json.dumps(line) + "\n")
|
||||
print(json.dumps(line), flush=True)
|
||||
if line["decision_changed"]:
|
||||
changed.append(tid)
|
||||
print("DECISION CHANGED for:", changed or "none", "(nothing in tasks_gen/eval was changed)", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
@@ -47,7 +84,12 @@ def main():
|
||||
ap.add_argument("--model", required=True)
|
||||
ap.add_argument("--base-url")
|
||||
ap.add_argument("--run-base", type=int, required=True)
|
||||
ap.add_argument("--rerun", action="store_true", help="rerun the named tasks into runs/emp_rerun/ and compare the filter decision; the eval set is not changed")
|
||||
a = ap.parse_args()
|
||||
if a.rerun:
|
||||
if not a.tasks:
|
||||
raise SystemExit("--rerun needs task ids")
|
||||
return rerun(a)
|
||||
runs_root = os.path.join(ROOT, "runs", "emp")
|
||||
os.makedirs(runs_root, exist_ok=True)
|
||||
runner = Runner(POOL, runs_root)
|
||||
|
||||
@@ -16,6 +16,7 @@ import sys
|
||||
|
||||
from .adt_client import load_env
|
||||
from .generator import ROOT, generate, make_k_variant
|
||||
from . import mix, overlap
|
||||
from .ledger import BudgetExceeded, spent
|
||||
|
||||
FIRST_ID = 100
|
||||
@@ -52,6 +53,73 @@ K_SOURCES = [("G0004", "free_text"), ("G0007", "incomplete"), ("G0026", "free_te
|
||||
RELEASES = ["v702", "v740sp05"]
|
||||
|
||||
|
||||
# New kinds for the eval set (Opus item F, 2026-10-06): the 130 eval candidates have only CLAS, FUNC, DDLS and PROG, so INTF, TABL, STRU, MSAG and
|
||||
# exception tasks cannot be measured after training. Shares as in the training mix (110 tasks: INTF 8, TABL 9, STRU 2, MSAG 3, exception 3); about
|
||||
# 30 percent more slots than the target because of rejections and the empirical filter. Generation after the reset, FIRST (before training tasks).
|
||||
NEW_FIRST_ID = 200
|
||||
NEW_RUN_BASE = 440000 # 40 per slot, below 466560
|
||||
NEW_KIND_SLOTS = [("INTF", "A", 10), ("TABL", "B", 11), ("STRU", "B", 3), ("MSAG", "D", 4), ("EXC", "D", 4)]
|
||||
NEW_K = [("INTF", "free_text"), ("TABL", "incomplete"), ("TABL", "free_text"), ("MSAG", "free_text"), ("EXC", "incomplete")]
|
||||
EXC_TOPICS = ["exception class (CX_...): a domain exception with context attributes and message texts",
|
||||
"exception class (CX_...): an exception hierarchy with a common super class",
|
||||
"exception class (CX_...): an exception that wraps a previous exception",
|
||||
"exception class (CX_...): an exception with a message class and parameters in the text"]
|
||||
|
||||
|
||||
def plan_new():
|
||||
out, n = [], 0
|
||||
for kind, cat, count in NEW_KIND_SLOTS:
|
||||
for i in range(count):
|
||||
out.append({"id": f"G{NEW_FIRST_ID + n:04d}", "kind": kind, "category": cat, "object_type": "CLAS" if kind == "EXC" else kind,
|
||||
"topic": EXC_TOPICS[i % len(EXC_TOPICS)] if kind == "EXC" else None,
|
||||
"difficulty": 3 if i % 3 == 2 else 2, "run_base": NEW_RUN_BASE + 40 * n})
|
||||
n += 1
|
||||
return out
|
||||
|
||||
|
||||
def run_new(only, model, base_url):
|
||||
"""Generate the new-kind eval candidates. The bundle is also checked against the training pool (no near duplicate of a training task)."""
|
||||
train = overlap.load_pool("train")
|
||||
for s in plan_new():
|
||||
if only and s["id"] not in only:
|
||||
continue
|
||||
if os.path.exists(os.path.join(ROOT, "tasks_gen", "eval", s["id"], "generation.json")):
|
||||
continue
|
||||
avoid = [g for g in accepted_goals() if g][-170:]
|
||||
topic = ((s["topic"] + ". ") if s["topic"] else "Choose a new, realistic business topic. ") + "Do not repeat these existing topics: " + "; ".join(avoid)
|
||||
|
||||
def extra(b, _t=train):
|
||||
return [f"Too close to task {i} (spec {sc['spec']:.2f}, rules {sc['core']:.2f}, names {sc['name']:.2f}). Choose another topic and other object names."
|
||||
for i, sc in overlap.check(overlap.load_bundle(b), _t)[:3]]
|
||||
try:
|
||||
log = generate(s["id"], "eval", s["object_type"], s["category"], s["difficulty"], model, base_url, s["run_base"], topic, extra_check=extra)
|
||||
except BudgetExceeded as e:
|
||||
print("BUDGET", e, flush=True)
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
log = {"id": s["id"], "error": str(e)[:500]}
|
||||
log.update(kind=s["kind"], spent_total=spent())
|
||||
print(json.dumps(log), flush=True)
|
||||
|
||||
|
||||
def run_new_k(model, base_url):
|
||||
"""K variants (free text or incomplete spec, EPOD tool names) of the first accepted eval task of each new kind."""
|
||||
plan = plan_new()
|
||||
first_k = NEW_FIRST_ID + len(plan)
|
||||
for j, (kind, style) in enumerate(NEW_K):
|
||||
src = next((s["id"] for s in plan if s["kind"] == kind and os.path.exists(os.path.join(ROOT, "tasks_gen", "eval", s["id"], "generation.json"))
|
||||
and json.load(open(os.path.join(ROOT, "tasks_gen", "eval", s["id"], "generation.json"))).get("accepted")), None)
|
||||
new_id = f"G{first_k + j:04d}"
|
||||
if not src or os.path.exists(os.path.join(ROOT, "tasks_gen", "eval", new_id, "generation.json")):
|
||||
continue
|
||||
try:
|
||||
log = make_k_variant(src, new_id, style, model, base_url, NEW_RUN_BASE + 40 * (len(plan) + j), tool_schema=None, pool="eval")
|
||||
except BudgetExceeded as e:
|
||||
print("BUDGET", e, flush=True)
|
||||
break
|
||||
print(json.dumps(log), flush=True)
|
||||
|
||||
|
||||
def accepted_goals():
|
||||
"""Goal lines of all generated tasks (eval and train), so the model does not repeat a topic."""
|
||||
out = []
|
||||
@@ -90,6 +158,18 @@ def plan():
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "plan"
|
||||
model, base_url = "deepseek-v4.1-flash:cloud", os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")
|
||||
if cmd == "plan-new":
|
||||
for x in plan_new():
|
||||
print(x)
|
||||
print(len(plan_new()), "slots,", len(NEW_K), "K variants after them")
|
||||
return
|
||||
if cmd == "run-new":
|
||||
run_new(set(sys.argv[2:]), model, base_url)
|
||||
return
|
||||
if cmd == "run-new-k":
|
||||
run_new_k(model, base_url)
|
||||
return
|
||||
slots = plan()
|
||||
if cmd == "plan":
|
||||
for s in slots:
|
||||
|
||||
@@ -24,7 +24,8 @@ ABAPLINT = os.path.join(ROOT, "node_modules", ".bin", "abaplint")
|
||||
MAX_OUT_TOKENS = 80000
|
||||
RUNS_PER_ATTEMPT = 10
|
||||
ABAPLINT_VERSIONS = {"v702", "v740sp05", "v750", "v758"}
|
||||
EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15"}
|
||||
EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15",
|
||||
"STRU": "T15", "MSAG": "T13"}
|
||||
CATEGORIES = {
|
||||
"A": "pure logic in a new class (language, OO design, Clean ABAP)",
|
||||
"B": "database access (ABAP SQL, CDS) with test doubles",
|
||||
@@ -50,6 +51,59 @@ CATEGORIES = {
|
||||
"hidden_tests is []. Do not write the gap in Open questions (write \"None.\")",
|
||||
}
|
||||
|
||||
# Extra format notes for object types that the main prompt does not describe (training mode, 2026-10-05).
|
||||
# The example bundle is of another type; these notes say how this type differs.
|
||||
TYPE_NOTES = {
|
||||
"INTF": """The model must create an INTERFACE (type INTF) as the main contract object: the methods, types and constants
|
||||
named in the spec. Contract entry: {"type": "INTF", "name": "{{P}}IF_X"}. Reference: the interface source
|
||||
("reference/x.intf.abap") and ONE global own-test class (type CLAS, source with a global class DEFINITION FOR TESTING;
|
||||
an interface has no test include). Hidden tests: one global test class with a local class that IMPLEMENTS the
|
||||
interface (it only compiles when every signature matches), calls the methods through the interface, reads the
|
||||
constants (values), and checks type lengths with RTTI (cl_abap_typedescr=>describe_by_name). The business rules give the
|
||||
exact constant values, parameter names and types. No other object is needed.""",
|
||||
"TABL": """The model must create a transparent DATABASE TABLE (type TABL) as the main contract object. The table name has at
|
||||
most 16 characters including the prefix: use a short name, for example {{P}}ORD (7 characters after the prefix at most).
|
||||
Source is DDL: annotations @EndUserText.label, @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE,
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT, @AbapCatalog.deliveryClass : #A, @AbapCatalog.dataMaintenance : #RESTRICTED,
|
||||
then "define table {{p}}ord { key client : abap.clnt not null; key item_id : abap.char(10) not null; ... }".
|
||||
Contract entry: {"type": "TABL", "name": "{{P}}ORD", "fields": ["client", "item_id", ...]} (all fields, lower case).
|
||||
The spec (Business rules) lists every field with its data type and length, the key fields, and not-null fields. Use
|
||||
only built-in types (abap.char, abap.numc, abap.dec, abap.int4, abap.dats). Avoid abap.curr and abap.quan; if the spec
|
||||
needs an amount or a quantity, the reference field needs a reference annotation in the form 'tablename.fieldname'
|
||||
(for example @Semantics.amount.currencyCode : '{{p}}ord.currency_code' on the amount and a field currency_code :
|
||||
abap.cuky in the same table; @Semantics.quantity.unitOfMeasure : '{{p}}ord.unit' with unit : abap.unit(3)); a
|
||||
reference without the table name fails with "Annotation with reference to unit code ... is uncomplete". Reference: the DDL file and ONE global own-test class
|
||||
(type CLAS). Hidden tests (one global class): RTTI on the table: cast cl_abap_typedescr=>describe_by_name( '{{P}}ORD' ) to
|
||||
cl_abap_structdescr and check get_ddic_field_list( ) (field names, key flags, LENG in characters, decimals, types; cl_abap_elemdescr->length is in BYTES: 2 per character), and a
|
||||
cl_osql_test_environment test (create( i_dependency_list = VALUE #( ( '{{P}}ORD' ) ) ), insert, select back; a second
|
||||
INSERT with the same key gives sy-subrc = 4). No seed is needed.""",
|
||||
"STRU": """The model must create a DDIC STRUCTURE (type STRU) as the main contract object. DDL source: annotations
|
||||
@EndUserText.label and @AbapCatalog.enhancement.category : #NOT_EXTENSIBLE, then
|
||||
"define structure {{p}}name { code : abap.char(4); amount : abap.dec(9,2); ... }" (include another structure with
|
||||
"include {{p}}other;" when the spec asks). Contract entry: {"type": "STRU", "name": "{{P}}NAME", "fields": [...]}
|
||||
(the field names in lower case). The Business rules list every field with data type and length. Reference: the DDL file
|
||||
and ONE global own-test class (type CLAS). Hidden tests (one global class): RTTI: cl_abap_typedescr=>describe_by_name(
|
||||
'{{P}}NAME' ) cast to cl_abap_structdescr; check components (names, length, decimals, type kind) and the order. RTTI units: cl_abap_elemdescr->length is in BYTES
|
||||
(2 bytes per character in Unicode), so a char(10) field has length 20; compare characters with output_length or with
|
||||
LENG of cl_abap_structdescr=>get_ddic_field_list( ) (characters), and decimals with ->decimals.""",
|
||||
"MSAG": """The model must create a MESSAGE CLASS (type MSAG) as the main contract object. The name has at most 20
|
||||
characters including the prefix. A message class has NO source file. In task.json the reference entry has no "file":
|
||||
{"type": "MSAG", "name": "{{P}}MSG", "description": "...", "messages": [{"msgno": "001", "text": "Order &1 is blocked"}, ...]}
|
||||
and the contract entry is {"type": "MSAG", "name": "{{P}}MSG", "messages": [{"msgno": "001"}, ...]} (the numbers that
|
||||
must exist). The Business rules give for each message the number, the exact text with the placeholders &1 to &4 (at
|
||||
most 72 characters), and when it is used (error, warning, information). Reference: the message class entry and ONE global
|
||||
own-test class (type CLAS) that reads the messages. Hidden tests (one global class): for each message use
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'A' 'B' INTO DATA(lv_text) and assert the final text; also check one
|
||||
placeholder substitution. The model may also be asked for an exception class that uses the message class (then both
|
||||
are contract entries and the exception class is a reference file).""",
|
||||
"EXC": """The main contract object is a class-based EXCEPTION class (CLAS, name {{P}}CX_...) that inherits from
|
||||
CX_STATIC_CHECK, CX_DYNAMIC_CHECK or CX_NO_CHECK. The spec asks for constants for the error cases, attributes with
|
||||
context data, a constructor with these parameters, and a message text (through IF_T100_DYN_MSG / IF_T100_MESSAGE with
|
||||
a message class that is a seed object, or through a redefined get_text). Hidden tests raise the exception from a small
|
||||
seed class, catch it and check the attributes and the text (get_text( )). Reference: the exception class with its
|
||||
testclasses_file or ONE global own-test class (an exception class has few methods).""",
|
||||
}
|
||||
|
||||
SYSTEM = """You write evaluation tasks for an ABAP developer model. Each task is a bundle of files.
|
||||
The model gets only spec.md and works on an SAP ABAP Platform 2025 system (SAP_BASIS 816, client 001)
|
||||
through ADT tools. The harness installs the seed objects, runs the model, then checks the result with
|
||||
@@ -220,7 +274,7 @@ def lint_files(b):
|
||||
m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), ""))
|
||||
if not m:
|
||||
continue
|
||||
want = {"table": "TABL", "structure": "TABL", "view": "DDLS"}[m.group(1).lower()]
|
||||
want = {"table": "TABL", "structure": "STRU", "view": "DDLS"}[m.group(1).lower()]
|
||||
if o.get("type") != want:
|
||||
errs.append(f"{k}: {o.get('name')} has type {o.get('type')}, but its source is "
|
||||
f"'define {m.group(1)}'; use type {want}")
|
||||
@@ -294,9 +348,24 @@ def check_bundle(b):
|
||||
errs.append(f"{k}: file {o[fk]} missing")
|
||||
if not o.get("name", "").startswith("{{P}}"):
|
||||
errs.append(f"{k}: name {o.get('name')} does not start with {{{{P}}}}")
|
||||
limit = 16 if o.get("type") == "TABL" else 26 if o.get("type") == "FUGR" else 30
|
||||
limit = {"TABL": 16, "FUGR": 26, "MSAG": 20}.get(o.get("type"), 30)
|
||||
if len(o.get("name", "").replace("{{P}}", "Z0000000_")) > limit:
|
||||
errs.append(f"{k}: name {o.get('name')} too long (max {limit})")
|
||||
for k in ("seed", "reference"):
|
||||
for o in t.get(k, []):
|
||||
if o.get("type") == "MSAG":
|
||||
msgs = o.get("messages") or []
|
||||
if k == "reference" and not msgs:
|
||||
errs.append(f"reference message class {o.get('name')} has no 'messages' list")
|
||||
for m in msgs:
|
||||
if not re.fullmatch(r"\d{1,3}", str(m.get("msgno", ""))) or not m.get("text") or len(m["text"]) > 72:
|
||||
errs.append(f"message class {o.get('name')}: message {m.get('msgno')} needs a 1 to 3 digit "
|
||||
"msgno and a text of at most 72 characters")
|
||||
for c in t.get("contract", []):
|
||||
if c.get("type") in ("TABL", "STRU") and not c.get("fields"):
|
||||
errs.append(f"contract {c.get('name')}: a {c['type']} entry needs the list 'fields'")
|
||||
if c.get("type") == "MSAG" and not c.get("messages"):
|
||||
errs.append(f"contract {c.get('name')}: a MSAG entry needs the list 'messages'")
|
||||
if not t.get("hidden_tests") and not stop:
|
||||
errs.append("no hidden test class")
|
||||
return errs
|
||||
@@ -399,9 +468,11 @@ def generate(task_id, pool, object_type, category, difficulty, model, base_url,
|
||||
pool_root = os.path.join(ROOT, "tasks_gen", pool)
|
||||
task_dir = os.path.join(pool_root, task_id)
|
||||
example = bundle_of(os.path.join(ROOT, "tasks", EXAMPLE_FOR[object_type]))
|
||||
note_key = "EXC" if (object_type == "CLAS" and topic and "exception class" in topic.lower()) else object_type
|
||||
ask = (f"Write one new task.\nObject type of the main contract object: {object_type}.\n"
|
||||
f"Skill category {category}: {CATEGORIES[category]}.\nDifficulty {difficulty} of 3.\n"
|
||||
+ (f"Topic idea: {topic}\n" if topic else "Choose a new, realistic business topic.\n")
|
||||
+ (("Format notes for this object type:\n" + TYPE_NOTES[note_key] + "\n") if note_key in TYPE_NOTES else "")
|
||||
+ "Here is an example bundle of a different task (same format):\n" + json.dumps(example))
|
||||
messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": ask}]
|
||||
log = {"id": task_id, "pool": pool, "object_type": object_type, "category": category, "attempts": []}
|
||||
|
||||
62
harness/infra.py
Normal file
62
harness/infra.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Infrastructure outages: the MCP server (ADT in Eclipse, 127.0.0.1:3000) or A4H (localhost:50000) is not reachable.
|
||||
|
||||
On 2026-10-05 21:43 the MCP server refused connections for a short time; three runs got `Connection refused` and the
|
||||
pipeline stopped because it counted three equal harness errors. An outage is not a model result and not a harness bug:
|
||||
the run is cleaned up, the work waits until the services answer, and the same run is started again.
|
||||
"""
|
||||
import errno
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
MCP_URL = os.environ.get("MCP_URL", "http://127.0.0.1:3000/mcp")
|
||||
A4H_URL = os.environ.get("A4H_URL", "http://localhost:50000").rstrip("/") + "/sap/public/ping"
|
||||
|
||||
|
||||
def is_outage(exc):
|
||||
"""True for a refused or lost connection to a local service (not for an HTTP error status)."""
|
||||
if isinstance(exc, urllib.error.HTTPError):
|
||||
return False
|
||||
if isinstance(exc, urllib.error.URLError):
|
||||
return is_outage(exc.reason) if isinstance(exc.reason, BaseException) else True
|
||||
return isinstance(exc, (ConnectionError, socket.timeout, TimeoutError)) or \
|
||||
(isinstance(exc, OSError) and exc.errno in (errno.ECONNREFUSED, errno.ECONNRESET, errno.EHOSTUNREACH, errno.EPIPE))
|
||||
|
||||
|
||||
def _answers(url):
|
||||
try:
|
||||
urllib.request.urlopen(url, timeout=6)
|
||||
return True
|
||||
except urllib.error.HTTPError:
|
||||
return True # 401 / 403 / 404: the server answers
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def up():
|
||||
return _answers(MCP_URL) and _answers(A4H_URL)
|
||||
|
||||
|
||||
def wait_until_up(max_seconds=3600, poll=30, log=print):
|
||||
"""Wait until MCP and A4H answer twice in a row (a restart shows a short flicker). False when the time is over."""
|
||||
end, ok_in_row = time.time() + max_seconds, 0
|
||||
while time.time() < end:
|
||||
ok_in_row = ok_in_row + 1 if up() else 0
|
||||
if ok_in_row >= 2:
|
||||
return True
|
||||
time.sleep(poll)
|
||||
return False
|
||||
|
||||
|
||||
def cleanup_prefix(prefix):
|
||||
"""Delete the A4H objects of an interrupted run (best effort; returns the number of objects found)."""
|
||||
from .mcp_client import McpClient
|
||||
from .runner import DELETE_ORDER, Runner, delete_uris
|
||||
with McpClient() as m:
|
||||
objs = Runner("", "")._objects_with_prefix(m, prefix.upper())
|
||||
objs.sort(key=lambda o: DELETE_ORDER.index(o["objectType"]) if o["objectType"] in DELETE_ORDER else 99)
|
||||
if objs:
|
||||
delete_uris([o["uri"] for o in objs])
|
||||
return len(objs)
|
||||
336
harness/localqwen.py
Normal file
336
harness/localqwen.py
Normal file
@@ -0,0 +1,336 @@
|
||||
"""Series A: the local Qwen 3.8 27B (4-bit, served on the MacBook) on training tasks of the new kinds (2026-10-05).
|
||||
|
||||
Harness, A4H, MCP server and records stay on the Mac mini; the MacBook only serves the model (docs/remote-model.md).
|
||||
|
||||
python3 -m harness.localqwen --base-url http://<MacBook LAN IP>:8080/v1 [--window-hours 12] [--plan-only]
|
||||
|
||||
Order: 3 tasks each of INTF, TABL, STRU, MSAG, exception, then 5 DDLS; one task at a time. Settings as the official
|
||||
baseline (docs/stage1-baseline.md): thinking off, max_tokens 16384, loop guard 3, tool budget 60, temperature 0.2, same
|
||||
system prompt. Time window: 12 hours from the first run, hard stop at the end also in the middle of a run (the run is
|
||||
not scored, its objects are deleted on A4H). If the server does not answer, the run ends cleanly, the series pauses and
|
||||
resumes when it answers again; such a run is not a result and not a failure.
|
||||
Output (runs/local_qwen/): state.json (dashboard), runs/, accepted.jsonl (accepted trajectories, NOT for the first SFT),
|
||||
results.jsonl, summary.json. Resumable: call again with the same arguments.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from .adt_client import load_env
|
||||
from .agents import LlmAgent
|
||||
from . import infra, mix
|
||||
from .runner import Runner
|
||||
from .task import prefix_for
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "train"))
|
||||
import accept as acc # noqa: E402
|
||||
|
||||
POOL = os.path.join(ROOT, "tasks_gen", "train")
|
||||
OUT = os.path.join(ROOT, "runs", "local_qwen")
|
||||
STATE = os.path.join(OUT, "state.json")
|
||||
RUN_BASE = 410000 # + task index * 10 + restart number (a digit must lead the 4-char base36 run: < 466560)
|
||||
EXPECTED_MODEL = "Qwen3.8-27B-4bit" # basename of the served model path (same build as the official baseline)
|
||||
PLAN_ORDER = [("INTF", 3), ("TABL", 3), ("STRU", 3), ("MSAG", 3), ("EXC", 3), ("DDLS", 5)]
|
||||
SETTINGS = {"max_tokens": 16384, "enable_thinking": False, "temperature": 0.2, "tool_call_budget": 60, "loop_guard": 3,
|
||||
"max_turns": 80, "one_task_at_a_time": True}
|
||||
MIN_SCORE = 80
|
||||
|
||||
|
||||
def now():
|
||||
return time.time()
|
||||
|
||||
|
||||
def http_json(url, timeout=10, body=None):
|
||||
req = urllib.request.Request(url, json.dumps(body).encode() if body else None,
|
||||
{"Content-Type": "application/json", "Authorization": "Bearer none"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def preflight(base_url, wait=0):
|
||||
"""Server answers, serves the expected build, can generate. Returns a dict (ok, model id, speed, models json)."""
|
||||
deadline = now() + wait
|
||||
while True:
|
||||
out = {"ok": False, "base_url": base_url, "time": time.strftime("%F %T")}
|
||||
try:
|
||||
models = http_json(base_url + "/models", timeout=10)
|
||||
ids = [m.get("id", "") for m in models.get("data", [])]
|
||||
out["models"] = ids
|
||||
out["model_id"] = ids[0] if ids else ""
|
||||
out["build_ok"] = bool(ids) and os.path.basename(ids[0].rstrip("/")) == EXPECTED_MODEL
|
||||
if out["build_ok"]:
|
||||
t0 = now()
|
||||
r = http_json(base_url + "/chat/completions", timeout=180,
|
||||
body={"model": out["model_id"], "messages": [{"role": "user", "content": "Say OK."}],
|
||||
"max_tokens": 16, "temperature": 0, "chat_template_kwargs": {"enable_thinking": False}})
|
||||
out["probe_seconds"] = round(now() - t0, 1)
|
||||
out["probe_reply"] = (r["choices"][0]["message"].get("content") or "")[:40]
|
||||
out["ok"] = True
|
||||
else:
|
||||
out["error"] = f"served model {ids} is not {EXPECTED_MODEL} (the official baseline build)"
|
||||
except Exception as e: # noqa: BLE001
|
||||
out["error"] = repr(e)[:200]
|
||||
if out["ok"] or now() >= deadline:
|
||||
return out
|
||||
time.sleep(15)
|
||||
|
||||
|
||||
def build_plan():
|
||||
"""3 x INTF, TABL, STRU, MSAG, EXC, then 5 DDLS: accepted training tasks, no K variants, lowest ids first."""
|
||||
import glob
|
||||
by_kind = {}
|
||||
for f in sorted(glob.glob(os.path.join(POOL, "G*", "generation.json"))):
|
||||
tid = os.path.basename(os.path.dirname(f))
|
||||
try:
|
||||
if not json.load(open(f)).get("accepted"):
|
||||
continue
|
||||
meta = json.load(open(os.path.join(POOL, tid, "task.json")))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if meta.get("category") == "K":
|
||||
continue
|
||||
k = mix.kind_of_task_dir(tid)
|
||||
by_kind.setdefault(k, []).append((tid, meta.get("category")))
|
||||
plan = []
|
||||
for kind, n in PLAN_ORDER:
|
||||
for tid, cat in by_kind.get(kind, [])[:n]:
|
||||
plan.append({"task": tid, "kind": kind, "category": cat})
|
||||
return plan
|
||||
|
||||
|
||||
def failure_types(rep, why):
|
||||
"""Why a run is not accepted (list of short labels)."""
|
||||
out = []
|
||||
er = rep.get("end_reason")
|
||||
if er and er != "report":
|
||||
out.append(er) # loop, tool_budget, empty_response, model_error, max_turns, ...
|
||||
g = rep.get("gates") or {}
|
||||
names = {"G1_active": "not_active", "G2_contract": "contract", "G3_hidden_runs": "hidden_tests_not_run",
|
||||
"G4_out_of_scope": "out_of_scope_changed", "G5_no_p1": "atc_priority1", "G6_release": "release_syntax"}
|
||||
out += [names[k] for k, v in g.items() if not v and k in names]
|
||||
h = rep.get("hidden_tests") or {}
|
||||
if h.get("total") and h.get("passed", 0) < h["total"]:
|
||||
out.append("hidden_tests_failed")
|
||||
if not out and any(w.startswith("score=") for w in why):
|
||||
out.append("low_score")
|
||||
if "no_final_report" in why and "no_report" not in out and er == "report":
|
||||
out.append("no_report")
|
||||
return out or ["other"]
|
||||
|
||||
|
||||
class Series:
|
||||
def __init__(self, a):
|
||||
self.a = a
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
self.state = json.load(open(STATE)) if os.path.exists(STATE) else {}
|
||||
self.stop_hb = threading.Event()
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# ---- state -------------------------------------------------------------------------------------------------
|
||||
def save(self):
|
||||
with self.lock:
|
||||
self.state["updated"] = now()
|
||||
tmp = STATE + ".tmp"
|
||||
json.dump(self.state, open(tmp, "w"), indent=1, default=str)
|
||||
os.replace(tmp, STATE)
|
||||
|
||||
def heartbeat(self):
|
||||
while not self.stop_hb.wait(30):
|
||||
try:
|
||||
ok = True
|
||||
http_json(self.a.base_url + "/models", timeout=8)
|
||||
except Exception: # noqa: BLE001
|
||||
ok = False
|
||||
self.state["server_ok"] = ok
|
||||
self.state["server_checked"] = now()
|
||||
self.save()
|
||||
|
||||
def window_end(self):
|
||||
return self.state["window_start"] + self.a.window_hours * 3600
|
||||
|
||||
# ---- one run ---------------------------------------------------------------------------------------------
|
||||
def one(self, item, idx, model_id):
|
||||
restarts = 0
|
||||
while True:
|
||||
run_no = RUN_BASE + idx * 10 + restarts
|
||||
self.state["current"] = {"task": item["task"], "kind": item["kind"], "category": item["category"], "start": now(),
|
||||
"run": run_no, "restarts": restarts}
|
||||
self.state["status"] = "running"
|
||||
self.save()
|
||||
agent = LlmAgent(model_id, self.a.base_url, max_tokens=SETTINGS["max_tokens"],
|
||||
chat_template_kwargs={"enable_thinking": False}, loop_guard=SETTINGS["loop_guard"],
|
||||
deadline=self.window_end(), watch=True)
|
||||
runner = Runner(POOL, os.path.join(OUT, "runs"))
|
||||
try:
|
||||
rep, run_dir = runner.run(item["task"], agent, run_no, teardown=True, tool_budget=SETTINGS["tool_call_budget"])
|
||||
except Exception as e: # noqa: BLE001 a harness exception is not a model result
|
||||
if infra.is_outage(e): # MCP or A4H is down: wait, clean up, run the same task again (not a failure)
|
||||
self.event("infra_outage", item, repr(e)[:150])
|
||||
self.state["status"] = "paused"
|
||||
self.save()
|
||||
infra.wait_until_up(max_seconds=max(60, self.window_end() - now()))
|
||||
try:
|
||||
infra.cleanup_prefix(prefix_for(run_no, item["task"]))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
for d in __import__("glob").glob(os.path.join(OUT, "runs", f"{run_no}_{item['task']}_*")):
|
||||
self.park(d, "_paused")
|
||||
if now() >= self.window_end():
|
||||
return {"window_over": True}
|
||||
restarts = min(restarts + 1, 9)
|
||||
continue
|
||||
self.event("harness_exception", item, repr(e)[:200])
|
||||
return {"task": item["task"], "kind": item["kind"], "category": item["category"], "harness_error": repr(e)[:200]}
|
||||
er = rep.get("end_reason")
|
||||
if er == "server_down":
|
||||
self.pause(item, run_dir)
|
||||
if now() >= self.window_end():
|
||||
return {"window_over": True}
|
||||
restarts = min(restarts + 1, 9)
|
||||
continue
|
||||
if er == "window_end":
|
||||
self.event("window_end_mid_run", item, "run stopped and cleaned up; not scored")
|
||||
self.park(run_dir, "_window_end")
|
||||
return {"window_over": True}
|
||||
return self.finish(item, rep, run_dir, agent)
|
||||
|
||||
def park(self, run_dir, folder):
|
||||
dst = os.path.join(OUT, folder)
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
shutil.move(run_dir, os.path.join(dst, os.path.basename(run_dir) + "_" + str(int(now()))))
|
||||
|
||||
def event(self, kind, item, text):
|
||||
self.state.setdefault("events", []).append({"t": now(), "kind": kind, "task": item["task"], "text": text})
|
||||
self.save()
|
||||
print(time.strftime("%F %T"), kind, item["task"], text, flush=True)
|
||||
|
||||
def pause(self, item, run_dir):
|
||||
self.event("server_down", item, "run ended cleanly (objects deleted); series paused")
|
||||
self.park(run_dir, "_paused")
|
||||
self.state["status"] = "paused"
|
||||
self.state["pause_start"] = now()
|
||||
self.save()
|
||||
while now() < self.window_end():
|
||||
pf = preflight(self.a.base_url)
|
||||
if pf["ok"]:
|
||||
break
|
||||
time.sleep(30)
|
||||
paused = now() - self.state.pop("pause_start", now())
|
||||
self.state["paused_seconds"] = self.state.get("paused_seconds", 0) + paused
|
||||
self.event("server_back", item, f"paused {paused / 60:.1f} min; the task is run again (no failure counted)")
|
||||
|
||||
def finish(self, item, rep, run_dir, agent):
|
||||
rec_path = os.path.join(run_dir, "record.json")
|
||||
rec = json.load(open(rec_path)) if os.path.exists(rec_path) else None
|
||||
row = {"harness_error": False, "setup_failed": bool(rep.get("setup_failed"))}
|
||||
ok, why = acc.judge(rec, row, MIN_SCORE) if rec else (False, ["no_record"])
|
||||
if rep.get("setup_failed"):
|
||||
ok, why = False, ["setup_failed"]
|
||||
res = {"task": item["task"], "kind": item["kind"], "category": item["category"], "accepted": ok,
|
||||
"score": (rep.get("score") or {}).get("total"), "end_reason": rep.get("end_reason"),
|
||||
"tool_calls": rep.get("tool_calls"), "seconds": rep.get("seconds"), "agent_seconds": rep.get("agent_seconds"),
|
||||
"hidden": "%s/%s" % ((rep.get("hidden_tests") or {}).get("passed"), (rep.get("hidden_tests") or {}).get("total")),
|
||||
"activation_failures": rep.get("activation_failures"), "syntax_hints": rep.get("syntax_hints"),
|
||||
"failure_types": [] if ok else failure_types(rep, why), "reasons": why,
|
||||
"run_dir": os.path.relpath(run_dir, ROOT), "teardown_ok": isinstance(rep.get("teardown"), dict)
|
||||
and all(v.get("deleted") for v in rep["teardown"].values())}
|
||||
if ok:
|
||||
rec["metadata"]["use_for_sft"] = False # not for the first SFT (Kral 2026-10-05)
|
||||
rec["metadata"]["series"] = "local_qwen_A"
|
||||
rec["metadata"]["served_model"] = agent.model
|
||||
with open(os.path.join(OUT, "accepted.jsonl"), "a") as f:
|
||||
f.write(json.dumps({"id": f"{item['task']}_r{rep.get('run')}", "task": rec["task"], "teacher": agent.model,
|
||||
"metadata": rec["metadata"], "messages": rec["messages"], "tools": rec["tools"]}) + "\n")
|
||||
with open(os.path.join(OUT, "results.jsonl"), "a") as f:
|
||||
f.write(json.dumps(res) + "\n")
|
||||
return res
|
||||
|
||||
# ---- series ------------------------------------------------------------------------------------------------
|
||||
def run(self):
|
||||
if self.a.plan_only:
|
||||
for p in self.state.get("plan") or build_plan():
|
||||
print(p)
|
||||
return
|
||||
pf = preflight(self.a.base_url, wait=self.a.wait)
|
||||
json.dump(pf, open(os.path.join(OUT, "preflight.json"), "w"), indent=1)
|
||||
print("preflight:", json.dumps(pf)[:400], flush=True)
|
||||
if not pf["ok"]:
|
||||
sys.exit("the model server is not usable: %s" % pf.get("error"))
|
||||
plan = self.state.get("plan") or build_plan()
|
||||
if self.a.only:
|
||||
plan = [p for p in plan if p["task"] in self.a.only]
|
||||
self.state.update(plan=plan, settings=SETTINGS, base_url=self.a.base_url, model_id=pf["model_id"],
|
||||
window_hours=self.a.window_hours, preflight=pf, server_ok=True, results=self.state.get("results", []))
|
||||
self.state.setdefault("window_start", now()) # 12 hours from the first run
|
||||
self.save()
|
||||
threading.Thread(target=self.heartbeat, daemon=True).start()
|
||||
done = {r["task"] for r in self.state["results"]}
|
||||
for idx, item in enumerate(plan):
|
||||
if item["task"] in done:
|
||||
continue
|
||||
if now() >= self.window_end():
|
||||
break
|
||||
res = self.one(item, idx, pf["model_id"])
|
||||
if res.get("window_over"):
|
||||
break
|
||||
if res.get("harness_error"):
|
||||
continue
|
||||
self.state["results"].append(res)
|
||||
self.state["current"] = None
|
||||
self.save()
|
||||
print(json.dumps({k: res[k] for k in ("task", "kind", "accepted", "score", "end_reason", "failure_types")}), flush=True)
|
||||
left = self.window_end() - now()
|
||||
self.state["status"] = "window_over" if left <= 0 else "finished"
|
||||
self.state["current"] = None
|
||||
self.save()
|
||||
self.stop_hb.set()
|
||||
self.summary()
|
||||
|
||||
def summary(self):
|
||||
per = {}
|
||||
for r in self.state["results"]:
|
||||
k = per.setdefault(r["kind"], {"runs": 0, "accepted": 0, "scores": [], "failures": {}})
|
||||
k["runs"] += 1
|
||||
k["accepted"] += r["accepted"]
|
||||
if r["score"] is not None:
|
||||
k["scores"].append(r["score"])
|
||||
for ft in r["failure_types"]:
|
||||
k["failures"][ft] = k["failures"].get(ft, 0) + 1
|
||||
for k in per.values():
|
||||
k["mean_score"] = round(sum(k["scores"]) / len(k["scores"]), 1) if k["scores"] else None
|
||||
k["pass_rate"] = round(k["accepted"] / k["runs"], 2) if k["runs"] else None
|
||||
out = {"per_kind": per, "runs": len(self.state["results"]), "accepted": sum(r["accepted"] for r in self.state["results"]),
|
||||
"window_hours": self.a.window_hours, "paused_seconds": self.state.get("paused_seconds", 0),
|
||||
"events": self.state.get("events", []), "settings": SETTINGS, "model_id": self.state.get("model_id")}
|
||||
json.dump(out, open(os.path.join(OUT, "summary.json"), "w"), indent=1)
|
||||
print("summary:", json.dumps({k: (v["accepted"], v["runs"]) for k, v in per.items()}), flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--base-url", default=os.environ.get("LOCAL_MODEL_URL"), help="http://<MacBook LAN IP>:8080/v1")
|
||||
ap.add_argument("--window-hours", type=float, default=12.0)
|
||||
ap.add_argument("--wait", type=int, default=0, help="seconds to wait for the server at the start")
|
||||
ap.add_argument("--plan-only", action="store_true")
|
||||
ap.add_argument("--only", nargs="*", help="test: only these tasks")
|
||||
ap.add_argument("--out-dir", help="test: another output directory")
|
||||
a = ap.parse_args()
|
||||
if a.out_dir:
|
||||
global OUT, STATE
|
||||
OUT = os.path.abspath(a.out_dir)
|
||||
STATE = os.path.join(OUT, "state.json")
|
||||
if not a.base_url:
|
||||
sys.exit("--base-url (or LOCAL_MODEL_URL) is required, for example http://192.168.1.20:8080/v1")
|
||||
a.base_url = a.base_url.rstrip("/")
|
||||
Series(a).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
harness/mix.py
Normal file
87
harness/mix.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Object type mix of the training data (CLAUDE.md section 1, Opus review 2026-10-05).
|
||||
|
||||
Target share of the accepted tasks (and of the accepted trajectories) per "kind":
|
||||
CLAS/INTF 35 % (CLAS 28, INTF 7), CDS (DDLS) 25 %, FUNC 15 %, PROG 10 %, DDIC 10 % (TABL 8, STRU 2),
|
||||
MSAG + exception 5 % (MSAG 2.5, EXC 2.5).
|
||||
A task has the kind of its main contract object; a CLAS whose reference inherits from CX_ is EXC.
|
||||
DTEL / DOMA tasks are not made yet (the DDIC share is covered by TABL and STRU).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
POOL = os.path.join(ROOT, "tasks_gen", "train")
|
||||
TYPE_SHARE = {"CLAS": 28.0, "INTF": 7.0, "DDLS": 25.0, "FUNC": 15.0, "PROG": 10.0, "TABL": 8.0, "STRU": 2.0,
|
||||
"MSAG": 2.5, "EXC": 2.5}
|
||||
# categories a kind supports (docs/faz1-tasarim.md 8): the generator picks the one with the biggest deficit
|
||||
KIND_CATEGORIES = {"CLAS": "ACDEFGHI", "FUNC": "ACDEFGHI", "PROG": "BCEGH", "DDLS": "BEFHI", "INTF": "A",
|
||||
"TABL": "B", "STRU": "B", "MSAG": "D", "EXC": "D"}
|
||||
CATEGORY_SHARE = {"A": 10, "B": 15, "C": 15, "D": 10, "E": 15, "F": 10, "G": 10, "I": 5, "H": 10}
|
||||
_CACHE = {}
|
||||
|
||||
|
||||
def kind_of_task_dir(task_id):
|
||||
"""Kind of an accepted training task (cached)."""
|
||||
if task_id in _CACHE:
|
||||
return _CACHE[task_id]
|
||||
d = os.path.join(POOL, task_id)
|
||||
try:
|
||||
t = json.load(open(os.path.join(d, "task.json")))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
otype = t.get("object_type") or "CLAS"
|
||||
for c in t.get("contract", []): # K variants and old tasks: the contract object is the truth
|
||||
otype = c.get("type", otype)
|
||||
break
|
||||
kind = otype
|
||||
if otype == "CLAS":
|
||||
for o in t.get("reference", []):
|
||||
if o.get("file") and o.get("name") == (t.get("contract") or [{}])[0].get("name"):
|
||||
try:
|
||||
src = open(os.path.join(d, o["file"])).read()
|
||||
except OSError:
|
||||
src = ""
|
||||
if re.search(r"INHERITING\s+FROM\s+\S*CX_", src, re.I):
|
||||
kind = "EXC"
|
||||
_CACHE[task_id] = kind
|
||||
return kind
|
||||
|
||||
|
||||
def deficit_pick(counts, share=TYPE_SHARE, allowed=None):
|
||||
"""Kind with the largest (target - actual) for the next item. counts: {kind: n}."""
|
||||
total = sum(counts.values()) + 1
|
||||
best, best_def = None, None
|
||||
tot_share = sum(share.values())
|
||||
for k, s in share.items():
|
||||
if allowed and k not in allowed:
|
||||
continue
|
||||
d = total * s / tot_share - counts.get(k, 0)
|
||||
if best_def is None or d > best_def:
|
||||
best, best_def = k, d
|
||||
return best
|
||||
|
||||
|
||||
def accepted_task_counts(logs_dir=None):
|
||||
"""{kind: accepted tasks} from the generation logs of the training pool (K variants count by their kind)."""
|
||||
import glob
|
||||
logs_dir = logs_dir or os.path.join(POOL, "_logs")
|
||||
out = {}
|
||||
for f in glob.glob(os.path.join(logs_dir, "G*.json")):
|
||||
try:
|
||||
l = json.load(open(f))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if l.get("accepted"):
|
||||
k = kind_of_task_dir(l["id"])
|
||||
if k:
|
||||
out[k] = out.get(k, 0) + 1
|
||||
return out
|
||||
|
||||
|
||||
def below_target(counts, share=TYPE_SHARE):
|
||||
"""Kinds whose share of `counts` is below the target for the next item (deficit > 0).
|
||||
Since 2026-10-05 (Kral + Opus): only these kinds are generated and run; CLAS and FUNC wait until they are at or
|
||||
below their target share."""
|
||||
total, ss = sum(counts.values()) + 1, sum(share.values())
|
||||
return {k for k, v in share.items() if total * v / ss - counts.get(k, 0) > 0}
|
||||
@@ -148,6 +148,74 @@ def mutants(src, otype, seed, n=MAX_MUTANTS):
|
||||
return out
|
||||
|
||||
|
||||
def decl_mutants(src, otype, seed, n=MAX_MUTANTS):
|
||||
"""Mutants of declarations that carry the behavior of DDIC objects and interfaces (no executable code):
|
||||
TABL / STRU: field length, decimals, data type, key flag. INTF: constant values, type lengths."""
|
||||
sites = [] # (kind, start, end, new, line, old)
|
||||
def add(kind, m, new):
|
||||
sites.append((kind, m.start(), m.end(), new, src.count("\n", 0, m.start()) + 1, m.group(0)))
|
||||
if otype in ("TABL", "STRU"):
|
||||
for m in re.finditer(r"abap\.(char|numc|dec|curr|quan|lang|cuky|unit)\((\d+)(?:,\s*(\d+))?\)", src, re.I):
|
||||
kind_, ln, dec = m.group(1).lower(), int(m.group(2)), m.group(3)
|
||||
if kind_ in ("char", "numc") and ln > 1:
|
||||
add("length", m, f"abap.{kind_}({ln - 1})")
|
||||
if dec is not None and int(dec) < ln - 1:
|
||||
add("decimals", m, f"abap.{kind_}({ln},{int(dec) + 1})")
|
||||
if kind_ == "char" and ln > 1:
|
||||
add("type", m, f"abap.numc({ln})")
|
||||
for m in re.finditer(r"abap\.(int4|int8|timestamp|dats|tims)\b", src, re.I):
|
||||
add("type", m, {"int4": "abap.int8", "int8": "abap.int4", "timestamp": "abap.dats",
|
||||
"dats": "abap.tims", "tims": "abap.dats"}[m.group(1).lower()])
|
||||
for m in re.finditer(r"^(\s*)key(\s+)(?!client\b)(\w+\s*:)", src, re.I | re.M):
|
||||
add("key", m, f"{m.group(1)}{m.group(3)}")
|
||||
if otype == "INTF":
|
||||
for m in re.finditer(r"(\bmsgno\s*=\s*')(\d+)(')", src, re.I): # message number of an exception text id
|
||||
add("msgno", m, f"{m.group(1)}{str(int(m.group(2)) + 1).zfill(len(m.group(2)))}{m.group(3)}")
|
||||
for m in re.finditer(r"(\bVALUE\s+)(\d+)(?=\s*\.)", src, re.I):
|
||||
add("const", m, f"{m.group(1)}{int(m.group(2)) + 1}")
|
||||
for m in re.finditer(r"(\bVALUE\s+)'([^'\n]{1,20})'", src, re.I):
|
||||
v = m.group(2)
|
||||
add("lit", m, f"{m.group(1)}'" + ("Z" if v[0] != "Z" else "Y") + v[1:] + "'")
|
||||
for m in re.finditer(r"(\bVALUE\s+)(abap_true|abap_false)\b", src, re.I):
|
||||
add("bool", m, m.group(1) + ("abap_false" if m.group(2).lower() == "abap_true" else "abap_true"))
|
||||
for m in re.finditer(r"\bLENGTH\s+(\d+)", src, re.I):
|
||||
if int(m.group(1)) > 1:
|
||||
add("length", m, f"LENGTH {int(m.group(1)) - 1}")
|
||||
rnd = random.Random(seed)
|
||||
rnd.shuffle(sites)
|
||||
chosen, kinds = [], set()
|
||||
for prefer_new in (True, False):
|
||||
for st in sites:
|
||||
if len(chosen) >= n:
|
||||
break
|
||||
if st in chosen or (prefer_new and st[0] in kinds):
|
||||
continue
|
||||
chosen.append(st)
|
||||
kinds.add(st[0])
|
||||
return [(f"line {line}: {old.strip()} -> {new.strip()} ({kind})", src[:a] + new + src[b:])
|
||||
for kind, a, b, new, line, old in sorted(chosen, key=lambda x: x[1])]
|
||||
|
||||
|
||||
def msag_mutants(messages, seed, n=MAX_MUTANTS):
|
||||
"""Mutants of a message class: changed text, changed placeholder, a message moved to another number."""
|
||||
out = []
|
||||
for i, m in enumerate(messages):
|
||||
t = m.get("text", "")
|
||||
mut = [dict(x) for x in messages]
|
||||
mut[i]["text"] = t + " x" if len(t) < 70 else t[:-1]
|
||||
out.append((f"message {m['msgno']}: text + ' x' (text)", mut))
|
||||
if "&1" in t:
|
||||
mut = [dict(x) for x in messages]
|
||||
mut[i]["text"] = t.replace("&1", "&2", 1)
|
||||
out.append((f"message {m['msgno']}: &1 -> &2 (placeholder)", mut))
|
||||
if len(messages) > 1:
|
||||
mut = [dict(x) for x in messages]
|
||||
mut[i]["msgno"] = str(int(m["msgno"]) + 50).zfill(3)
|
||||
out.append((f"message {m['msgno']}: number + 50 (number)", mut))
|
||||
random.Random(seed).shuffle(out)
|
||||
return out[:n]
|
||||
|
||||
|
||||
def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
|
||||
"""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)
|
||||
@@ -156,14 +224,25 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
|
||||
def is_test(o):
|
||||
return o["type"] == "CLAS" and re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING",
|
||||
open(os.path.join(task_dir, o["file"])).read(), re.I | re.M)
|
||||
targets = [o for o in meta["reference"] if o.get("file") and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS")
|
||||
targets = [o for o in meta["reference"] if o.get("file") and o["type"] in ("CLAS", "FUNC", "PROG", "DDLS", "INTF",
|
||||
"TABL", "STRU")
|
||||
and not is_test(o)]
|
||||
targets += [o for o in meta["reference"] if o["type"] == "MSAG" and o.get("messages")]
|
||||
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]
|
||||
def _muts(o):
|
||||
if o["type"] == "MSAG":
|
||||
return msag_mutants(o["messages"], f"{task_id}:{o['name']}", n)
|
||||
src = open(os.path.join(task_dir, o["file"])).read()
|
||||
if o["type"] in ("TABL", "STRU", "INTF"):
|
||||
return decl_mutants(src, o["type"], f"{task_id}:{o['name']}", n)
|
||||
ms = mutants(src, o["type"], f"{task_id}:{o['name']}", n)
|
||||
if o["type"] == "CLAS" and len(ms) < n: # exception classes: constants, message numbers, texts
|
||||
ms += decl_mutants(src, "INTF", f"{task_id}:{o['name']}", n - len(ms))
|
||||
return ms
|
||||
cand = [[(o, d, m) for d, m in _muts(o)] for o in targets]
|
||||
plan = []
|
||||
while len(plan) < n and any(cand):
|
||||
for c in cand:
|
||||
@@ -174,7 +253,14 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
|
||||
mdir = os.path.join(work, "pool", task_id)
|
||||
shutil.rmtree(mdir, ignore_errors=True)
|
||||
shutil.copytree(task_dir, mdir)
|
||||
open(os.path.join(mdir, o["file"]), "w").write(msrc)
|
||||
if o["type"] == "MSAG": # the mutant changes the messages of the reference in task.json
|
||||
tj = json.load(open(os.path.join(mdir, "task.json")))
|
||||
for r in tj["reference"]:
|
||||
if r["name"] == o["name"]:
|
||||
r["messages"] = msrc
|
||||
json.dump(tj, open(os.path.join(mdir, "task.json"), "w"), indent=2)
|
||||
else:
|
||||
open(os.path.join(mdir, o["file"]), "w").write(msrc)
|
||||
rep, _ = runner.run(task_id, OracleAgent(), run_base + k)
|
||||
h = rep.get("hidden_tests") or {}
|
||||
g = rep.get("gates") or {}
|
||||
@@ -187,7 +273,7 @@ def check_task(pool_root, task_id, run_base, n=MAX_MUTANTS, keep=False):
|
||||
results.append({"object": o["name"], "mutant": desc, "status": status,
|
||||
"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
|
||||
if keep and status == "killed" and o["type"] != "MSAG": # 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)
|
||||
|
||||
200
harness/owntests.py
Normal file
200
harness/owntests.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""Own-test mutation score of an accepted trajectory (Opus item D, 2026-10-06): metadata only, the acceptance filter does not change.
|
||||
|
||||
The model's own unit tests (a testclasses include of the contract class, or global test classes) are run against the faulty references
|
||||
of the task (`faulty/`: mutants of the reference that the hidden tests kill). Per trajectory: one run with the correct reference (the tests must
|
||||
pass there, otherwise they encode model specific behavior and a kill proves nothing), then one run per mutant. Status per mutant:
|
||||
killed (an own test fails), survived (all own tests pass), invalid (the mutant or the tests do not activate, or no own test ran).
|
||||
score = killed / (killed + survived) over the valid mutants. PROG tasks are not supported (the tests live inside the program source).
|
||||
|
||||
python3 -m harness.owntests [--tasks G1000 ...] [--limit N] [--workers 1] writes runs/traj/<run>/own_test_mutation.json
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
from .adt_client import load_env
|
||||
from .agents import OracleAgent
|
||||
from . import mix
|
||||
from .runner import Runner
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "train"))
|
||||
import accept as acc # noqa: E402
|
||||
|
||||
POOL = os.path.join(ROOT, "tasks_gen", "train")
|
||||
WORK = os.path.join(ROOT, "runs", "owntests")
|
||||
RUN_BASE = 480000 # + sequence number; below 466560 is NOT needed here: the run numbers stay under 466560 with 4-char base36
|
||||
RUN_BASE = 420000
|
||||
MAX_MUTANTS = 5
|
||||
|
||||
|
||||
def calls_of(messages):
|
||||
res = {m.get("tool_call_id"): m["content"] for m in messages if m["role"] == "tool"}
|
||||
out = []
|
||||
for m in messages:
|
||||
if m["role"] != "assistant":
|
||||
continue
|
||||
for c in m.get("tool_calls") or []:
|
||||
a = c["function"].get("arguments") or "{}"
|
||||
try:
|
||||
a = json.loads(a) if isinstance(a, str) else a
|
||||
except ValueError:
|
||||
a = {}
|
||||
out.append((c["function"]["name"], a, res.get(c.get("id"), "")))
|
||||
return out
|
||||
|
||||
|
||||
def model_tests(rec, task_meta):
|
||||
"""{'include': {OBJECT: source}, 'global': {NAME: source}} of the model's last successful pushes."""
|
||||
contract = {c["name"].upper() for c in task_meta.get("contract", [])}
|
||||
last = {}
|
||||
for tool, a, res in calls_of(rec["messages"]):
|
||||
if tool == "sap_push_source" and a.get("source") and '"success":true' in (res or "").replace(" ", ""):
|
||||
last[(str(a.get("objectName", "")).upper(), str(a.get("includeType") or "").lower(), a.get("objectType"))] = a["source"]
|
||||
pre = rec["prefix"].upper()
|
||||
seed_hidden = {o["name"].replace("{{P}}", pre).upper() for k in ("seed", "hidden_tests") for o in task_meta.get(k, [])}
|
||||
out = {"include": {}, "global": {}}
|
||||
for (name, inc, otype), src in last.items():
|
||||
contract_names = {c.replace("{{P}}", pre).upper() for c in contract}
|
||||
if inc == "testclasses" and name in contract_names:
|
||||
out["include"][name] = src
|
||||
elif otype == "CLAS" and not inc and name not in contract_names and name not in seed_hidden \
|
||||
and re.search(r"FOR\s+TESTING", src, re.I) and re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING", src, re.I | re.M):
|
||||
out["global"][name] = src
|
||||
return out
|
||||
|
||||
|
||||
def placeholder(src, prefix):
|
||||
return re.sub(re.escape(prefix), "{{P}}", re.sub(re.escape(prefix.lower()), "{{p}}", src), flags=re.I) if False else \
|
||||
src.replace(prefix.upper(), "{{P}}").replace(prefix.lower(), "{{p}}")
|
||||
|
||||
|
||||
def mutant_files(task_id):
|
||||
"""[(reference object name, path)] of faulty/ (a K variant has none: the base task's)."""
|
||||
meta = json.load(open(os.path.join(POOL, task_id, "task.json")))
|
||||
d = os.path.join(POOL, meta.get("base_task") or task_id, "faulty")
|
||||
return sorted(glob.glob(os.path.join(d, "m*_*")))
|
||||
|
||||
|
||||
def derive(task_id, run_label, tests, prefix, mutant_path=None):
|
||||
"""Build a temporary task: the reference (optionally with one mutated object) plus the model's own tests only."""
|
||||
src_dir = os.path.join(POOL, task_id)
|
||||
pool = os.path.join(WORK, "pool")
|
||||
dst = os.path.join(pool, task_id)
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
shutil.copytree(src_dir, dst, ignore=shutil.ignore_patterns("faulty", "generation.json", "review.json", "mutation.json", "empirical.json"))
|
||||
meta = json.load(open(os.path.join(dst, "task.json")))
|
||||
pre = prefix.upper()
|
||||
mutated = None
|
||||
if mutant_path:
|
||||
base = re.sub(r"^m\d+_", "", os.path.basename(mutant_path))
|
||||
refs = []
|
||||
for o in meta["reference"]:
|
||||
f = o.get("file")
|
||||
is_test = o["type"] == "CLAS" and f and re.search(r"^\s*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING",
|
||||
open(os.path.join(dst, f)).read(), re.I | re.M)
|
||||
if is_test:
|
||||
continue # the reference's own global test class: not the model's
|
||||
o = dict(o)
|
||||
o.pop("testclasses_file", None) # the reference's local tests are out; the model's go in
|
||||
if mutant_path and f and os.path.basename(f) == base:
|
||||
shutil.copy(mutant_path, os.path.join(dst, f))
|
||||
mutated = o["name"]
|
||||
name_up = o["name"].replace("{{P}}", pre).upper()
|
||||
if name_up in tests["include"]:
|
||||
rel = "reference/_own_include_%s.abap" % re.sub(r"\W", "_", name_up)
|
||||
open(os.path.join(dst, rel), "w").write(placeholder(tests["include"][name_up], prefix))
|
||||
o["testclasses_file"] = rel
|
||||
refs.append(o)
|
||||
for i, (name, src) in enumerate(tests["global"].items()):
|
||||
rel = "reference/_own_global_%d.clas.abap" % i
|
||||
open(os.path.join(dst, rel), "w").write(placeholder(src, prefix))
|
||||
refs.append({"type": "CLAS", "name": placeholder(name, prefix), "file": rel, "description": "own test class"})
|
||||
meta["reference"] = refs
|
||||
meta["budget"] = dict(meta.get("budget", {}), max_tool_calls=200)
|
||||
json.dump(meta, open(os.path.join(dst, "task.json"), "w"), indent=1)
|
||||
return pool, mutated
|
||||
|
||||
|
||||
def run_one(pool, task_id, run_no):
|
||||
runner = Runner(pool, os.path.join(WORK, "runs"))
|
||||
rep, _ = runner.run(task_id, OracleAgent(), run_no, teardown=True)
|
||||
own = rep.get("own_tests") or {}
|
||||
g = rep.get("gates") or {}
|
||||
return {"tests": own.get("tests", 0), "failures": own.get("failures", 0), "active": bool(g.get("G1_active")), "run": run_no}
|
||||
|
||||
|
||||
def score_run(run_dir, seq):
|
||||
rec = json.load(open(os.path.join(run_dir, "record.json")))
|
||||
tid = rec["task"]["id"]
|
||||
meta = json.load(open(os.path.join(POOL, tid, "task.json")))
|
||||
out = {"task": tid, "run": rec["run"], "kind": mix.kind_of_task_dir(tid), "time": time.strftime("%F %T")}
|
||||
if any(c.get("type") == "PROG" for c in meta.get("contract", [])):
|
||||
return dict(out, status="not_supported", reason="PROG: the tests are inside the program source")
|
||||
tests = model_tests(rec, meta)
|
||||
if not tests["include"] and not tests["global"]:
|
||||
return dict(out, status="no_own_tests", score=None, mutants=[])
|
||||
muts = mutant_files(tid)[:MAX_MUTANTS]
|
||||
if not muts:
|
||||
return dict(out, status="no_mutants", score=None, mutants=[])
|
||||
pre = rec["prefix"]
|
||||
pool, _ = derive(tid, "base", tests, pre)
|
||||
base = run_one(pool, tid, RUN_BASE + seq * 10)
|
||||
out["reference_run"] = base
|
||||
out["tests_pass_on_reference"] = base["active"] and base["tests"] > 0 and base["failures"] == 0
|
||||
res = []
|
||||
for k, mp in enumerate(muts):
|
||||
pool, mutated = derive(tid, "m%d" % k, tests, pre, mp)
|
||||
r = run_one(pool, tid, RUN_BASE + seq * 10 + 1 + k)
|
||||
status = "invalid" if (not r["active"] or r["tests"] == 0) else ("killed" if r["failures"] > 0 else "survived")
|
||||
res.append({"mutant": os.path.basename(mp), "object": mutated, "status": status, "tests": r["tests"], "failures": r["failures"]})
|
||||
valid = [x for x in res if x["status"] != "invalid"]
|
||||
killed = [x for x in res if x["status"] == "killed"]
|
||||
out.update(status="scored", mutants=res, valid=len(valid), killed=len(killed),
|
||||
score=round(len(killed) / len(valid), 2) if valid else None)
|
||||
shutil.rmtree(os.path.join(WORK, "pool", tid), ignore_errors=True)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--tasks", nargs="*")
|
||||
ap.add_argument("--limit", type=int)
|
||||
ap.add_argument("--redo", action="store_true")
|
||||
a = ap.parse_args()
|
||||
os.makedirs(WORK, exist_ok=True)
|
||||
rows = [json.loads(l) for l in open(os.path.join(ROOT, "runs", "traj", "summary.jsonl"))]
|
||||
todo = []
|
||||
for r in rows:
|
||||
p = os.path.join(ROOT, "runs", "traj", r.get("run_dir") or "-")
|
||||
if not os.path.exists(os.path.join(p, "record.json")):
|
||||
continue
|
||||
if a.tasks and r["task"] not in a.tasks:
|
||||
continue
|
||||
if os.path.exists(os.path.join(p, "own_test_mutation.json")) and not a.redo:
|
||||
continue
|
||||
rec = json.load(open(os.path.join(p, "record.json")))
|
||||
if acc.judge(rec, r, 80)[0]:
|
||||
todo.append((r, p))
|
||||
if a.limit:
|
||||
todo = todo[:a.limit]
|
||||
print(len(todo), "accepted trajectories to score", flush=True)
|
||||
for i, (r, p) in enumerate(todo):
|
||||
t0 = time.time()
|
||||
try:
|
||||
res = score_run(p, i)
|
||||
except Exception as e: # noqa: BLE001
|
||||
res = {"task": r["task"], "status": "error", "error": repr(e)[:300]}
|
||||
json.dump(res, open(os.path.join(p, "own_test_mutation.json"), "w"), indent=1)
|
||||
print(r["task"], res.get("status"), res.get("score"), "valid", res.get("valid"), "killed", res.get("killed"),
|
||||
"ref_ok", res.get("tests_pass_on_reference"), "%.0fs" % (time.time() - t0), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -25,7 +25,7 @@ import threading
|
||||
import time
|
||||
|
||||
from .adt_client import load_env
|
||||
from . import trainset, trajectories
|
||||
from . import mix, trainset, trajectories
|
||||
from .ledger import spent
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -129,13 +129,27 @@ class Pipeline:
|
||||
done = {(r["task"], r["attempt"]) for r in rows}
|
||||
ev0 = {r["task"]: self.evaluate(r) for r in rows if r["attempt"] == 0}
|
||||
tasks = trajectories.accepted_tasks()
|
||||
for t in tasks: # first attempt for every task first
|
||||
if (t, 0) not in done and (t, 0) not in self.inflight:
|
||||
self.inflight.add((t, 0))
|
||||
return t, 0
|
||||
counts = {} # accepted trajectories (and runs in flight) per kind
|
||||
for r in rows:
|
||||
if self.evaluate(r)["accepted"]:
|
||||
k = mix.kind_of_task_dir(r["task"])
|
||||
counts[k] = counts.get(k, 0) + 1
|
||||
for (t, _a) in self.inflight:
|
||||
k = mix.kind_of_task_dir(t)
|
||||
counts[k] = counts.get(k, 0) + 1
|
||||
below = mix.below_target(counts) # only kinds below their target share run (Kral + Opus 2026-10-05)
|
||||
cands = [t for t in tasks if (t, 0) not in done and (t, 0) not in self.inflight
|
||||
and mix.kind_of_task_dir(t) in below]
|
||||
if cands: # first attempts: the kind with the biggest deficit against the target mix goes first
|
||||
total, tot_share = sum(counts.values()) + 1, sum(mix.TYPE_SHARE.values())
|
||||
best = max(cands, key=lambda t: (total * mix.TYPE_SHARE.get(mix.kind_of_task_dir(t), 0) / tot_share
|
||||
- counts.get(mix.kind_of_task_dir(t), 0), -int(t[1:])))
|
||||
self.inflight.add((best, 0))
|
||||
return best, 0
|
||||
for t in tasks: # second attempt: first failed, or accepted without a repair
|
||||
e = ev0.get(t)
|
||||
if e and (t, 1) not in done and (t, 1) not in self.inflight and (not e["accepted"] or not e["repair"]):
|
||||
if e and (t, 1) not in done and (t, 1) not in self.inflight and (not e["accepted"] or not e["repair"]) \
|
||||
and mix.kind_of_task_dir(t) in below:
|
||||
self.inflight.add((t, 1))
|
||||
return t, 1
|
||||
return None
|
||||
@@ -210,7 +224,7 @@ class Pipeline:
|
||||
|
||||
def old_generators(self):
|
||||
out = subprocess.run(["pgrep", "-fl", r"harness\.trainset run --part"], capture_output=True, text=True).stdout
|
||||
return [l for l in out.splitlines() if "--plan plan2" not in l]
|
||||
return [l for l in out.splitlines() if "--plan balanced" not in l and "--plan plan2" not in l]
|
||||
|
||||
def supervisor(self):
|
||||
launched = False
|
||||
@@ -221,15 +235,14 @@ class Pipeline:
|
||||
if time.time() > self.gen_deadline or os.path.exists(trainset.STOP_FLAG):
|
||||
continue
|
||||
if not launched and not self.old_generators():
|
||||
trainset.ensure_plan("plan2")
|
||||
dl = self.a.gen_deadline
|
||||
for i in range(3):
|
||||
f = open(os.path.join(ROOT, "runs", "gen_train", f"p2_w{i}.log"), "a")
|
||||
self.children.append(subprocess.Popen(
|
||||
[sys.executable, "-m", "harness.trainset", "run", "--plan", "plan2", "--part", str(i),
|
||||
[sys.executable, "-m", "harness.trainset", "run", "--plan", "balanced", "--part", str(i),
|
||||
"--parts", "3", "--deadline", dl], cwd=ROOT, stdout=f, stderr=f))
|
||||
launched = True
|
||||
log("plan 2 generators started (3 workers, deadline", dl + ")")
|
||||
log("balanced generators started (3 workers, deadline", dl + ")")
|
||||
elif launched:
|
||||
for i, c in enumerate(self.children):
|
||||
if c.poll() not in (None, 0) and crashes < 3:
|
||||
@@ -237,7 +250,7 @@ class Pipeline:
|
||||
log("generator", i, "exited with", c.returncode, "- restarted")
|
||||
f = open(os.path.join(ROOT, "runs", "gen_train", f"p2_w{i}.log"), "a")
|
||||
self.children[i] = subprocess.Popen(
|
||||
[sys.executable, "-m", "harness.trainset", "run", "--plan", "plan2", "--part", str(i),
|
||||
[sys.executable, "-m", "harness.trainset", "run", "--plan", "balanced", "--part", str(i),
|
||||
"--parts", "3", "--deadline", self.a.gen_deadline], cwd=ROOT, stdout=f, stderr=f)
|
||||
# K variants: free text, about 10 % of the other accepted training tasks
|
||||
if k_proc is None or k_proc.poll() is not None:
|
||||
@@ -275,6 +288,25 @@ class Pipeline:
|
||||
a[1] += e["accepted"]
|
||||
a[2] += e["accepted"] and e["repair"]
|
||||
return "; ".join(f"{k} {v[1]}/{v[0]} (repair {v[2]})" for k, v in sorted(agg.items()))
|
||||
kinds_t, kinds_r = mix.accepted_task_counts(), {}
|
||||
for r, e in zip(rows, evs):
|
||||
k = mix.kind_of_task_dir(r["task"])
|
||||
a = kinds_r.setdefault(k, [0, 0])
|
||||
a[0] += 1
|
||||
a[1] += e["accepted"]
|
||||
tt, tr_ = max(sum(kinds_t.values()), 1), max(sum(v[1] for v in kinds_r.values()), 1)
|
||||
share_sum = sum(mix.TYPE_SHARE.values())
|
||||
kind_line = "; ".join("%s tasks %d (%.0f%%) traj %d/%d (%.0f%%) target %.0f%%" % (
|
||||
k, kinds_t.get(k, 0), 100.0 * kinds_t.get(k, 0) / tt, kinds_r.get(k, [0, 0])[1], kinds_r.get(k, [0, 0])[0],
|
||||
100.0 * kinds_r.get(k, [0, 0])[1] / tr_, 100.0 * v / share_sum) for k, v in mix.TYPE_SHARE.items())
|
||||
ddls = {}
|
||||
for r, e in zip(rows, evs):
|
||||
if mix.kind_of_task_dir(r["task"]) == "DDLS" and not e["accepted"]:
|
||||
for w in (e["reasons"] or ["?"]):
|
||||
w = w.split("=")[0] if w.startswith(("end_reason", "score")) else w
|
||||
ddls[w] = ddls.get(w, 0) + 1
|
||||
ddls_n = sum(1 for r in rows if mix.kind_of_task_dir(r["task"]) == "DDLS")
|
||||
ddls_acc = sum(1 for r, e in zip(rows, evs) if mix.kind_of_task_dir(r["task"]) == "DDLS" and e["accepted"])
|
||||
s = spent()
|
||||
used = (s - BASE_LEDGER) / LEDGER_TO_USAGE
|
||||
from .ledger import _env_budget
|
||||
@@ -290,6 +322,8 @@ trajectory runs {len(rows)}, accepted trajectories {len(accepted)} (acceptance {
|
||||
- By category, accepted/runs: {table('category')}.
|
||||
- By object type, accepted/runs: {table('object_type')}.
|
||||
- Tokens of accepted samples (20 tool schemas kept): p50 {pct(0.5)}, p90 {pct(0.9)}, p95 {pct(0.95)}, max {toks[-1] if toks else None}, n {len(toks)}.
|
||||
- Object type mix (accepted tasks; accepted/runs trajectories; target): {kind_line}.
|
||||
- DDLS (CDS) runs: {ddls_acc} accepted of {ddls_n}; reject reasons {ddls or 'none'} (activation, hidden tests and ATC rejections are separate gate failures, they show as score reasons).
|
||||
- Syntax hints (proxy syntaxCheck added): {sum(e['hints'] for e in evs)} in {sum(1 for e in evs if e['hints'])} runs.
|
||||
- Harness events: {sum(self.events.values())} ({self.events or 'none'}); trajectory workers now {self.max_workers}.
|
||||
"""
|
||||
@@ -327,6 +361,26 @@ trajectory runs {len(rows)}, accepted trajectories {len(accepted)} (acceptance {
|
||||
log("pipeline ended:", text.replace("\n", " | "))
|
||||
|
||||
|
||||
def acquire_controller_lock():
|
||||
"""One controller only. An exclusive flock on runs/pipeline/controller.lock lives as long as the process (the
|
||||
kernel drops it when the process dies, so a crash never leaves a stale lock); the pid is written for people."""
|
||||
import fcntl
|
||||
os.makedirs(PIPE, exist_ok=True)
|
||||
path = os.path.join(PIPE, "controller.lock")
|
||||
f = open(path, "a+")
|
||||
try:
|
||||
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
f.seek(0)
|
||||
sys.exit("a controller is already running (pid %s, %s); not starting a second one" % (
|
||||
f.read().strip() or "?", path))
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write("%d\n" % os.getpid())
|
||||
f.flush()
|
||||
return f
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
@@ -348,7 +402,9 @@ def main():
|
||||
break
|
||||
time.sleep(30)
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
guard = acquire_controller_lock()
|
||||
Pipeline(a).run()
|
||||
guard.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -51,6 +51,9 @@ def activation_messages(text, is_error=False):
|
||||
|
||||
|
||||
RUN_PREFIX = re.compile(r"^Z\d[0-9A-Z]{5,6}_", re.I)
|
||||
# The model sometimes names its own helper or test class with the prefix inside the name (ZCL_Z4CGT1HJ_JOB_COST_TEST). Such objects of
|
||||
# other runs stay behind in A4H and show up in lists and searches (57 of 90 accepted trajectories saw them, 2026-10-06).
|
||||
MID_PREFIX = re.compile(r"^[A-Z]{1,5}_(Z\d[0-9A-Z]{6}_)", re.I)
|
||||
|
||||
|
||||
class BudgetExceeded(Exception):
|
||||
@@ -111,7 +114,10 @@ class ToolProxy:
|
||||
|
||||
def _foreign(self, name):
|
||||
n = (name or "").upper()
|
||||
return bool(RUN_PREFIX.match(n)) and not n.startswith(self.prefix)
|
||||
if RUN_PREFIX.match(n):
|
||||
return not n.startswith(self.prefix)
|
||||
m = MID_PREFIX.match(n)
|
||||
return bool(m) and m.group(1).upper() != self.prefix
|
||||
|
||||
def _filter(self, tool, text):
|
||||
if tool == "sap_short_dumps": # only dumps of this run: other runs (and mutants) also write dumps
|
||||
@@ -124,7 +130,7 @@ class ToolProxy:
|
||||
data["count"] = len(data["dumps"])
|
||||
return json.dumps(data)
|
||||
return text
|
||||
if tool not in ("sap_search_object", "sap_usage_references"):
|
||||
if tool not in ("sap_search_object", "sap_usage_references", "sap_inactive_objects"):
|
||||
return text
|
||||
try:
|
||||
data = json.loads(text)
|
||||
@@ -148,7 +154,9 @@ class ToolProxy:
|
||||
else:
|
||||
self.calls += 1
|
||||
name = str(args.get("objectName", "")).upper()
|
||||
if tool in WRITE_TOOLS and self._foreign(name):
|
||||
if (tool in WRITE_TOOLS or tool in ("sap_pull_source", "sap_object_structure", "sap_object_members", "sap_element_info",
|
||||
"sap_run_unit_test", "sap_check_object", "sap_syntax_check", "sap_atc_run")) \
|
||||
and self._foreign(name):
|
||||
result = (True, f"{name} is not available.")
|
||||
else:
|
||||
if tool in WRITE_TOOLS and (tool == "sap_activate" or args.get("activate", True)):
|
||||
|
||||
77
harness/restart_plan.py
Normal file
77
harness/restart_plan.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""State and suggested settings for restarting the cloud work after the budget reset (12 October).
|
||||
|
||||
python3 -m harness.restart_plan [--panel 0.0] [--ratio 1.2]
|
||||
|
||||
Prints (no cloud call, no file change): ledger, the suggested BUDGET_* values from the panel value, the object
|
||||
type deficits, the tasks waiting for a first run per kind, the second attempt candidates, and the commands.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
from .adt_client import load_env
|
||||
from . import mix
|
||||
from .ledger import spent
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
RESERVE_USAGE = 3.0
|
||||
LEDGER_RESERVE = 8
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--panel", type=float, default=0.0, help="Ollama panel usage after the reset (USD)")
|
||||
ap.add_argument("--ratio", type=float, default=1.2, help="ledger per usage, pessimistic (trajectory runs 1.2)")
|
||||
a = ap.parse_args()
|
||||
s = spent("2026-10-12")
|
||||
usable = 60 - a.panel - RESERVE_USAGE
|
||||
limit = round(usable * a.ratio + LEDGER_RESERVE)
|
||||
print(f"ledger since 2026-10-12: {s:.2f}")
|
||||
print(f"panel {a.panel} of 60, reserve {RESERVE_USAGE} usage -> usable {usable:.1f} usage x {a.ratio} = {usable * a.ratio:.0f} ledger")
|
||||
print(f"set in .env: BUDGET_CYCLE_START=2026-10-12 BUDGET_LIMIT_USD={limit + int(s)} BUDGET_RESERVE_USD={LEDGER_RESERVE}")
|
||||
rows = [json.loads(l) for l in open(os.path.join(ROOT, "runs", "traj", "summary.jsonl"))]
|
||||
import sys
|
||||
sys.path.insert(0, os.path.join(ROOT, "train"))
|
||||
import accept
|
||||
acc_traj, runs = {}, {}
|
||||
first = {}
|
||||
for r in rows:
|
||||
k = mix.kind_of_task_dir(r["task"])
|
||||
runs[k] = runs.get(k, 0) + 1
|
||||
ok = False
|
||||
p = os.path.join(ROOT, "runs", "traj", r.get("run_dir") or "-", "record.json")
|
||||
if os.path.exists(p):
|
||||
ok = accept.judge(json.load(open(p)), r, 80)[0]
|
||||
acc_traj[k] = acc_traj.get(k, 0) + ok
|
||||
if r["attempt"] == 0:
|
||||
first[r["task"]] = ok
|
||||
tot_t = max(sum(acc_traj.values()), 1)
|
||||
tasks = mix.accepted_task_counts()
|
||||
ss = sum(mix.TYPE_SHARE.values())
|
||||
print("\nkind target tasks traj acc/runs (share) below target (generate + run)")
|
||||
below_t, below_r = mix.below_target(tasks), mix.below_target(acc_traj)
|
||||
for k, v in mix.TYPE_SHARE.items():
|
||||
print(f"{k:5s} {100 * v / ss:5.1f}% {tasks.get(k, 0):6d} {acc_traj.get(k, 0):4d}/{runs.get(k, 0):<4d} ({100 * acc_traj.get(k, 0) / tot_t:4.1f}%) "
|
||||
f"tasks:{'yes' if k in below_t else 'no '} trajectories:{'yes' if k in below_r else 'no'}")
|
||||
done0 = {r["task"] for r in rows if r["attempt"] == 0}
|
||||
done1 = {r["task"] for r in rows if r["attempt"] == 1}
|
||||
waiting, second = {}, {}
|
||||
import glob
|
||||
for f in glob.glob(os.path.join(ROOT, "tasks_gen", "train", "G*", "generation.json")):
|
||||
tid = os.path.basename(os.path.dirname(f))
|
||||
if not json.load(open(f)).get("accepted"):
|
||||
continue
|
||||
k = mix.kind_of_task_dir(tid)
|
||||
if tid not in done0:
|
||||
waiting[k] = waiting.get(k, 0) + 1
|
||||
elif tid not in done1 and not first.get(tid):
|
||||
second[k] = second.get(k, 0) + 1
|
||||
print("\nwaiting for a first run, per kind:", dict(sorted(waiting.items())))
|
||||
print("second attempt candidates (first attempt failed), per kind:", dict(sorted(second.items())))
|
||||
print("\nstart: python3 -m harness.pipeline (one controller only; it starts the generators, the K variants and the "
|
||||
"trajectory workers; only kinds below target run; second attempts for failed tasks)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -21,7 +21,7 @@ CLEAN_RULES = {
|
||||
}
|
||||
DELETE_ORDER = ["CLAS", "INTF", "PROG", "FUNC", "FUGR", "SRVD", "DDLX", "DCLS", "DDLS",
|
||||
"TTYP", "TABL", "STRU", "DTEL", "DOMA", "MSAG"]
|
||||
SOURCE_TYPES = ("CLAS", "INTF", "PROG", "FUNC", "DDLS", "DCLS", "DDLX", "TABL")
|
||||
SOURCE_TYPES = ("CLAS", "INTF", "PROG", "FUNC", "DDLS", "DCLS", "DDLX", "TABL", "STRU")
|
||||
|
||||
|
||||
def _cds_elements(src):
|
||||
@@ -40,6 +40,13 @@ def _cds_elements(src):
|
||||
return out
|
||||
|
||||
|
||||
def _ddl_fields(src):
|
||||
"""Field names of a DDL table or structure source ('key name : type', 'name : type', 'include x')."""
|
||||
body = re.sub(r"//[^\n]*|/\*.*?\*/", "", src or "", flags=re.S)
|
||||
return {m.group(1).upper() for m in re.finditer(r"^\s*(?:key\s+)?(\w+)\s*:", body, re.I | re.M)
|
||||
if not m.group(1).startswith("@") and m.group(1).lower() not in ("define",)}
|
||||
|
||||
|
||||
def _obj_args(otype, name, fg=None):
|
||||
a = {"objectType": otype, "objectName": name}
|
||||
if fg:
|
||||
@@ -136,6 +143,9 @@ class Runner:
|
||||
includeType="testclasses",
|
||||
source=o["testclasses_source"]))
|
||||
ok = ok and not e3 and (_json(t3) or {}).get("success", False)
|
||||
if o.get("messages"): # message class (MSAG): created empty, messages written with sap_push_message
|
||||
e5, t5 = mcp.call("sap_push_message", {"objectName": o["name"], "messages": o["messages"]})
|
||||
ok = ok and not e5 and (_json(t5) or {}).get("success", False)
|
||||
if o.get("run"):
|
||||
e4, t4 = mcp.call("sap_run_class", {"className": o["name"]})
|
||||
ok = ok and not e4 and (_json(t4) or {}).get("success", False)
|
||||
@@ -143,9 +153,16 @@ class Runner:
|
||||
return out
|
||||
|
||||
def _objects_with_prefix(self, mcp, prefix):
|
||||
_, text = mcp.call("sap_search_object", {"query": prefix + "*", "maxResults": 200})
|
||||
return [d for d in (_json(text) or []) if d.get("name", "").upper().startswith(prefix)
|
||||
and d.get("objectType")] # skips STOB entries of CDS entities
|
||||
"""Objects of the run: the name starts with the prefix, or contains it after a short type part (the model named its own
|
||||
helper or test class ZCL_<prefix>_..., 27 such objects were left behind before 2026-10-06)."""
|
||||
out = {}
|
||||
for q in (prefix + "*", "*_" + prefix + "*"):
|
||||
_, text = mcp.call("sap_search_object", {"query": q, "maxResults": 200})
|
||||
for d in (_json(text) or []):
|
||||
n = d.get("name", "").upper()
|
||||
if d.get("objectType") and (n.startswith(prefix) or re.match(r"^[A-Z]{1,5}_" + re.escape(prefix), n)):
|
||||
out[n] = d # skips STOB entries of CDS entities (no objectType)
|
||||
return list(out.values())
|
||||
|
||||
def _source(self, mcp, otype, name, fg=None):
|
||||
err, text = mcp.call("sap_pull_source", _obj_args(otype, name, fg))
|
||||
@@ -199,11 +216,13 @@ class Runner:
|
||||
"line": i.get("start", {}).get("row"), "msg": i.get("description")} for i in issues]
|
||||
|
||||
# ---------- main ----------
|
||||
def run(self, task_id, agent, run_no, teardown=True, rescore_dir=None, cds_calls=None):
|
||||
def run(self, task_id, agent, run_no, teardown=True, rescore_dir=None, cds_calls=None, tool_budget=None):
|
||||
"""cds_calls: raise max_tool_calls to this value for tasks with a CDS (DDLS) contract object
|
||||
(trajectory runs, 2026-10-05: the own CDS test class needs more than 60 calls; eval stays at the task value)."""
|
||||
prefix = prefix_for(run_no, task_id)
|
||||
task = Task(os.path.join(self.tasks_root, task_id), prefix)
|
||||
if tool_budget: # fixed budget for every task (local model series: 60)
|
||||
task.meta.setdefault("budget", {})["max_tool_calls"] = tool_budget
|
||||
if cds_calls and any(c.get("type") == "DDLS" for c in task.meta.get("contract", [])):
|
||||
b = task.meta.setdefault("budget", {})
|
||||
b["max_tool_calls"] = max(b.get("max_tool_calls", 60), cds_calls)
|
||||
@@ -251,6 +270,15 @@ class Runner:
|
||||
if proxy.fallbacks:
|
||||
rep["adt_fallbacks"] = proxy.fallbacks
|
||||
|
||||
if rep.get("end_reason") in ("window_end", "server_down"): # not a result: no scoring, only the teardown
|
||||
rep["score"] = {"total": None, "note": f"not scored: {rep['end_reason']}"}
|
||||
rep["not_a_result"] = True
|
||||
rep["seconds"] = round(time.time() - t0, 1)
|
||||
all_objs = self._objects_with_prefix(mcp, prefix)
|
||||
rep["teardown"] = self._teardown(all_objs, run_dir) if teardown else "skipped"
|
||||
json.dump(rep, open(os.path.join(run_dir, "report.json"), "w"), indent=1)
|
||||
return rep, run_dir
|
||||
|
||||
# 3 collect
|
||||
hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")}
|
||||
seed_names = set(seed_src)
|
||||
@@ -285,9 +313,11 @@ class Runner:
|
||||
g2_detail = []
|
||||
for c in contract:
|
||||
src = sources.get((c["type"], c["name"].upper()), "")
|
||||
if c.get("implements") and not re.search(rf"INTERFACES\s+{re.escape(c['implements'])}\b", src, re.I):
|
||||
g2 = False
|
||||
g2_detail.append(f"{c['name']}: does not implement {c['implements']}")
|
||||
impl = c.get("implements") or []
|
||||
for iname in ([impl] if isinstance(impl, str) else impl): # one interface or a list
|
||||
if not re.search(rf"INTERFACES\s+{re.escape(str(iname))}\b", src, re.I):
|
||||
g2 = False
|
||||
g2_detail.append(f"{c['name']}: does not implement {iname}")
|
||||
if c["type"] == "FUNC": # signature: every parameter with its type in the FUNCTION header
|
||||
# the FUNCTION statement, not the first statement: local classes can come before it (G0107)
|
||||
fm = re.search(rf"^\s*FUNCTION\s+{re.escape(c['name'])}\b[^.]*\.", src, re.I | re.M)
|
||||
@@ -303,15 +333,25 @@ class Runner:
|
||||
if not re.search(rf"(PARAMETERS|SELECT-OPTIONS)\s*:?[^.]*\b{re.escape(prm)}\b", src, re.I):
|
||||
g2 = False
|
||||
g2_detail.append(f"{c['name']}: no selection screen parameter {prm}")
|
||||
if c["type"] == "DDLS" and c.get("fields") and g["G1_active"]:
|
||||
_, q = mcp.call("sap_sql_query", {"query": f"SELECT * FROM {c['name']}", "maxRows": 1})
|
||||
cols = {col.get("name", "").upper() for col in (_json(q) or {}).get("columns", [])}
|
||||
if c["type"] in ("DDLS", "TABL", "STRU") and c.get("fields") and g["G1_active"]:
|
||||
cols = set()
|
||||
if c["type"] != "STRU": # a structure cannot be selected
|
||||
_, q = mcp.call("sap_sql_query", {"query": f"SELECT * FROM {c['name']}", "maxRows": 1})
|
||||
cols = {col.get("name", "").upper() for col in (_json(q) or {}).get("columns", [])}
|
||||
if not cols: # view with parameters: SELECT without parameters fails
|
||||
cols = _cds_elements(src)
|
||||
cols = _cds_elements(src) if c["type"] == "DDLS" else _ddl_fields(src)
|
||||
missing = {f.upper() for f in c["fields"]} - cols
|
||||
if missing:
|
||||
g2 = False
|
||||
g2_detail.append(f"{c['name']}: CDS elements missing: {sorted(missing)}")
|
||||
g2_detail.append(f"{c['name']}: {'CDS elements' if c['type'] == 'DDLS' else 'fields'} missing: {sorted(missing)}")
|
||||
if c["type"] == "MSAG" and c.get("messages") and g["G1_active"]: # the numbers must exist (texts: hidden tests)
|
||||
_, q = mcp.call("sap_sql_query", {"query": "SELECT msgnr FROM t100 WHERE arbgb = '%s' AND sprsl = 'E'"
|
||||
% c["name"].upper(), "maxRows": 999})
|
||||
have = {r.get("MSGNR") for r in (_json(q) or {}).get("rows", [])}
|
||||
missing = {str(m["msgno"]).zfill(3) for m in c["messages"]} - have
|
||||
if missing:
|
||||
g2 = False
|
||||
g2_detail.append(f"{c['name']}: message numbers missing: {sorted(missing)}")
|
||||
# Categories E and I: the contract object is a seed object (refactor or fix in place). It must
|
||||
# change (else the null agent passes on the legacy code), and it is not out of scope.
|
||||
contract_upper = {c["name"].upper() for c in contract}
|
||||
@@ -404,8 +444,15 @@ class Runner:
|
||||
contract_files = {f"{c['name'].lower()}.{c['type'].lower()}.abap" for c in contract}
|
||||
# abaplint does not know standard superclasses (CX_STATIC_CHECK ...); an exception hierarchy
|
||||
# then gives "Super class ... not found or contains errors". A4H activation (G1) covers this.
|
||||
# Cascade: a class that uses such an exception class gets "Definition for X not found in scope" (X is
|
||||
# the exception class abaplint could not build). Messages that name a broken class are not counted.
|
||||
broken = {re.sub(r"\.(clas|intf)\.abap$", "", i["file"]).upper() for i in issues
|
||||
if re.match(r"Super class .* not found or contains errors", i["msg"] or "")}
|
||||
def _cascade(msg):
|
||||
return any(re.search(rf"\b{re.escape(b)}\b", msg or "", re.I) for b in broken)
|
||||
g["G6_release"] = not any(i["rule"] == "check_syntax" and i["file"] in contract_files
|
||||
and not re.match(r"Super class .* not found or contains errors", i["msg"] or "")
|
||||
and not _cascade(i["msg"])
|
||||
for i in issues)
|
||||
|
||||
# 8 score
|
||||
|
||||
89
harness/sweep.py
Normal file
89
harness/sweep.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Find (and delete) objects that harness runs left in A4H: names with a run prefix at the start (Z4CGT1HJ_X) or inside (ZCL_Z4CGT1HJ_X).
|
||||
|
||||
python3 -m harness.sweep dry run: list them by prefix
|
||||
python3 -m harness.sweep --delete delete them (ADT deletion API, dependency order); a locked object is reported, not forced
|
||||
python3 -m harness.sweep --keep-recent 60 do not touch prefixes of runs whose directory changed in the last 60 minutes (default 60)
|
||||
|
||||
Never run it while a controller or a series is writing: it can only judge by directory age.
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from .adt_client import load_env
|
||||
from .mcp_client import McpClient
|
||||
from .runner import DELETE_ORDER, delete_uris
|
||||
from .task import prefix_for
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
START = re.compile(r"^(Z\d[0-9A-Z]{6}_)", re.I)
|
||||
MID = re.compile(r"^[A-Z]{1,5}_(Z\d[0-9A-Z]{6}_)", re.I)
|
||||
|
||||
|
||||
def recent_prefixes(minutes):
|
||||
keep = set()
|
||||
for d in glob.glob(os.path.join(ROOT, "runs", "**", "*_G*_*"), recursive=True) + glob.glob(os.path.join(ROOT, "runs", "**", "*_T*_*"), recursive=True):
|
||||
b = os.path.basename(d)
|
||||
m = re.match(r"^(\d+)_([GT]\d+)_", b)
|
||||
if m and time.time() - os.path.getmtime(d) < minutes * 60:
|
||||
keep.add(prefix_for(int(m.group(1)), m.group(2)).upper())
|
||||
return keep
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(ROOT, ".env"))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--delete", action="store_true")
|
||||
ap.add_argument("--keep-recent", type=int, default=60)
|
||||
a = ap.parse_args()
|
||||
found = {}
|
||||
with McpClient() as m:
|
||||
# run prefixes are Z + a digit + 6 characters: one query per digit (a single "Z*" query is cut at 1000 results), at the start and after the type part
|
||||
for q in [f"Z{d}*" for d in "0123456789"] + [f"*_Z{d}*" for d in "0123456789"] + ["ZPROBE*", "ZTEST*"]:
|
||||
try:
|
||||
for o in json.loads(m.call("sap_search_object", {"query": q, "maxResults": 1000})[1]):
|
||||
if o.get("objectType"): # a STOB entry of a CDS view has the same name and no objectType: skip it
|
||||
found[(o["name"], o["objectType"])] = o
|
||||
except ValueError:
|
||||
pass
|
||||
keep = recent_prefixes(a.keep_recent)
|
||||
by_prefix = {}
|
||||
for (n, _t), o in found.items():
|
||||
mm = START.match(n.upper()) or MID.match(n.upper())
|
||||
if not mm or not o.get("objectType"):
|
||||
continue
|
||||
pre = mm.group(1).upper()
|
||||
if len(pre) != 9 or not pre[1].isdigit():
|
||||
continue
|
||||
if pre in keep:
|
||||
continue
|
||||
by_prefix.setdefault(pre, []).append(o)
|
||||
total = sum(len(v) for v in by_prefix.values())
|
||||
print(f"{total} objects of {len(by_prefix)} run prefixes ({len(keep)} recent prefixes kept)")
|
||||
for pre, objs in sorted(by_prefix.items()):
|
||||
print(" ", pre, [o["name"] for o in objs][:6])
|
||||
json.dump({p: [o["name"] for o in v] for p, v in by_prefix.items()}, open(os.path.join(ROOT, "runs", "sweep.json"), "w"), indent=1)
|
||||
if not a.delete:
|
||||
return
|
||||
objs = sorted([o for v in by_prefix.values() for o in v],
|
||||
key=lambda o: DELETE_ORDER.index(o["objectType"]) if o["objectType"] in DELETE_ORDER else 99)
|
||||
done = failed = 0
|
||||
for i in range(0, len(objs), 10):
|
||||
uris = []
|
||||
for o in objs[i:i + 10]:
|
||||
uris.append(o["uri"])
|
||||
res = delete_uris(uris)
|
||||
for u, v in res.items():
|
||||
if v["deleted"]:
|
||||
done += 1
|
||||
else:
|
||||
failed += 1
|
||||
print("not deleted:", u.split("/")[-1], v["msg"])
|
||||
print(f"deleted {done}, not deleted {failed}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,6 +19,7 @@ from .evalset import SLOTS, RELEASES, accepted_goals
|
||||
from .generator import ROOT, generate, make_k_variant
|
||||
from .ledger import BudgetExceeded, spent
|
||||
from . import overlap
|
||||
from . import mix
|
||||
|
||||
POOL = os.path.join(ROOT, "tasks_gen", "train")
|
||||
PLAN = os.path.join(POOL, "plan.json")
|
||||
@@ -132,6 +133,28 @@ def backlog():
|
||||
return len(acc - done)
|
||||
|
||||
|
||||
def backlog_by_kind():
|
||||
"""{kind: accepted tasks without a first trajectory run}."""
|
||||
done = set()
|
||||
sp = os.path.join(ROOT, "runs", "traj", "summary.jsonl")
|
||||
if os.path.exists(sp):
|
||||
done = {json.loads(l)["task"] for l in open(sp) if json.loads(l)["attempt"] == 0}
|
||||
out = {}
|
||||
for f in glob.glob(os.path.join(POOL, "G*", "generation.json")):
|
||||
tid = os.path.basename(os.path.dirname(f))
|
||||
try:
|
||||
ok = json.load(open(f)).get("accepted")
|
||||
except (OSError, ValueError):
|
||||
ok = False
|
||||
if ok and tid not in done:
|
||||
k = mix.kind_of_task_dir(tid)
|
||||
out[k] = out.get(k, 0) + 1
|
||||
return out
|
||||
|
||||
|
||||
BAL_KIND_BACKLOG = 8 # a kind with more waiting tasks than this is not generated (its trajectories come first)
|
||||
|
||||
|
||||
def run(part, parts, target, stop_ledger, plan_name="plan", deadline=None):
|
||||
plan = ensure_plan(plan_name, stop_ledger, target)
|
||||
stop_at = plan["ledger_at_start"] + plan["stop_ledger"]
|
||||
@@ -186,6 +209,140 @@ def run(part, parts, target, stop_ledger, plan_name="plan", deadline=None):
|
||||
print(json.dumps(log), flush=True)
|
||||
|
||||
|
||||
BAL_FIRST_ID = 1910
|
||||
BAL_RUN_BASE = 370000 # 40 per slot; below 466560 (a digit must lead the 4-char base36 run)
|
||||
BAL_ERROR_KINDS = {"CLAS": ["named-type", "long-names"], "FUNC": ["named-type"], "DDLS": ["reserved-word"],
|
||||
"TABL": ["reserved-word"], "STRU": ["reserved-word"]}
|
||||
BAL_HINTS = dict(((c, t), h) for c, t, _, h in SLOTS if h)
|
||||
|
||||
|
||||
def _claims_dir():
|
||||
d = os.path.join(POOL, "_claims")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _claim_slot():
|
||||
"""Next free balanced slot number, claimed with O_EXCL (several workers). Returns (n, claim path)."""
|
||||
for n in range(BAL_FIRST_ID, BAL_FIRST_ID + 600):
|
||||
sid = "G%04d" % n
|
||||
if os.path.exists(os.path.join(POOL, "_logs", sid + ".json")):
|
||||
continue
|
||||
path = os.path.join(_claims_dir(), sid + ".json")
|
||||
try:
|
||||
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||
except FileExistsError:
|
||||
continue
|
||||
os.close(fd)
|
||||
return n, path
|
||||
return None, None
|
||||
|
||||
|
||||
def _kind_stats():
|
||||
"""({kind: accepted}, {kind: attempted}) from the generation logs; claims of running slots count as attempted."""
|
||||
acc = mix.accepted_task_counts()
|
||||
att = {}
|
||||
for f in glob.glob(os.path.join(POOL, "_logs", "G*.json")):
|
||||
try:
|
||||
l = json.load(open(f))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
k = l.get("kind") or l.get("object_type")
|
||||
if k and l.get("category") != "K":
|
||||
att[k] = att.get(k, 0) + 1
|
||||
return acc, att
|
||||
|
||||
|
||||
def run_balanced(part, parts, deadline):
|
||||
"""Generation without a fixed plan: each slot takes the kind with the biggest deficit against mix.TYPE_SHARE.
|
||||
A kind with 6 or more tries and an acceptance below 20 % is skipped (a harness or prompt problem: do not burn budget)."""
|
||||
base_url = os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1")
|
||||
evals = overlap.load_pool("eval")
|
||||
while True:
|
||||
if os.path.exists(STOP_FLAG):
|
||||
print("STOP flag", flush=True)
|
||||
return
|
||||
if deadline and time.time() > deadline:
|
||||
print("DEADLINE", flush=True)
|
||||
return
|
||||
acc, att = _kind_stats()
|
||||
running = {}
|
||||
for f in glob.glob(os.path.join(_claims_dir(), "G*.json")):
|
||||
try:
|
||||
k = json.load(open(f)).get("kind")
|
||||
except (OSError, ValueError):
|
||||
k = None
|
||||
if k:
|
||||
running[k] = running.get(k, 0) + 1
|
||||
counts = {k: acc.get(k, 0) + running.get(k, 0) for k in set(acc) | set(running) | set(mix.TYPE_SHARE)}
|
||||
blocked = {k for k in mix.TYPE_SHARE if att.get(k, 0) >= 6 and acc.get(k, 0) < 0.2 * att.get(k, 0)}
|
||||
if blocked:
|
||||
print("kinds skipped (low acceptance):", sorted(blocked), flush=True)
|
||||
waiting = {k for k, n in backlog_by_kind().items() if n > BAL_KIND_BACKLOG}
|
||||
allowed = (set(mix.TYPE_SHARE) - blocked - waiting) & mix.below_target(counts) # only kinds below their share
|
||||
if not allowed:
|
||||
time.sleep(120) # every kind has a backlog: the trajectories are the slower side
|
||||
continue
|
||||
kind = mix.deficit_pick(counts, allowed=allowed)
|
||||
n, claim = _claim_slot()
|
||||
if n is None:
|
||||
print("no free slot", flush=True)
|
||||
return
|
||||
sid = "G%04d" % n
|
||||
json.dump({"kind": kind}, open(claim, "w"))
|
||||
otype = "CLAS" if kind == "EXC" else kind
|
||||
cats = mix.KIND_CATEGORIES[kind]
|
||||
logs = [json.load(open(f)) for f in glob.glob(os.path.join(POOL, "_logs", "G*.json"))]
|
||||
ccount = {c: sum(1 for l in logs if l.get("accepted") and l.get("category") == c) for c in cats}
|
||||
tot = sum(ccount.values()) + 1
|
||||
cat = max(cats, key=lambda c: tot * mix.CATEGORY_SHARE[c] / sum(mix.CATEGORY_SHARE[x] for x in cats) - ccount[c])
|
||||
idx = n - BAL_FIRST_ID
|
||||
error_kind = None
|
||||
if idx % 5 == 2 and kind in BAL_ERROR_KINDS: # 20 % error-targeted slots
|
||||
error_kind = BAL_ERROR_KINDS[kind][(idx // 5) % len(BAL_ERROR_KINDS[kind])]
|
||||
cat = ERROR_CATEGORY[error_kind] if kind in ("CLAS", "FUNC") else cat
|
||||
hard = cat != "H" and idx % 10 in (3, 6, 9) # 30 % hard
|
||||
topic = None
|
||||
if error_kind:
|
||||
topic = dict(ERROR_HINTS)[error_kind]
|
||||
elif kind == "EXC":
|
||||
topic = "exception class (CX_...): " + ["a domain exception with context attributes and message texts",
|
||||
"an exception hierarchy with a common super class",
|
||||
"an exception that wraps a previous exception"][idx % 3]
|
||||
elif (cat, otype) in BAL_HINTS:
|
||||
h = BAL_HINTS[(cat, otype)]
|
||||
topic = h[idx % len(h)]
|
||||
if cat == "G":
|
||||
topic = (topic + "; " if topic else "") + f"release target {RELEASES[idx % 2]}"
|
||||
avoid = [g for g in accepted_goals() if g][-170:]
|
||||
full = ((topic + ". ") if topic else "Choose a new, realistic business topic. ") + \
|
||||
"Do not repeat these existing topics: " + "; ".join(avoid)
|
||||
pool_now = evals + overlap.load_pool("train")
|
||||
|
||||
def extra(b, _pool=pool_now):
|
||||
hits = overlap.check(overlap.load_bundle(b), _pool)
|
||||
return [f"Too close to task {i} (similarity spec {sc['spec']:.2f}, rules {sc['core']:.2f}, "
|
||||
f"names {sc['name']:.2f}). Choose a different business topic and different object names."
|
||||
for i, sc in hits[:3]]
|
||||
try:
|
||||
log = generate(sid, "train", otype, cat, 3 if hard else 2, "deepseek-v4.1-flash:cloud", base_url,
|
||||
BAL_RUN_BASE + 40 * idx, full, extra_check=extra)
|
||||
except BudgetExceeded as e:
|
||||
print("BUDGET", e, flush=True)
|
||||
os.remove(claim)
|
||||
return
|
||||
except Exception as e: # noqa: BLE001
|
||||
log = {"id": sid, "error": str(e)[:500]}
|
||||
log.update(kind=kind, error_kind=error_kind, difficulty=3 if hard else 2, spent_total=spent())
|
||||
os.makedirs(os.path.join(POOL, "_logs"), exist_ok=True)
|
||||
json.dump(log, open(os.path.join(POOL, "_logs", sid + ".json"), "w"), indent=1)
|
||||
stray = os.path.join(POOL, "generation.json")
|
||||
if os.path.exists(stray):
|
||||
os.remove(stray)
|
||||
os.remove(claim)
|
||||
print(json.dumps(log), flush=True)
|
||||
|
||||
|
||||
K_FIRST_ID = 1300
|
||||
K_RUN_BASE = 41800 # 20 per variant; above the trajectory run numbers (41000-41700)
|
||||
K_COUNT = 18 # K share of the eval plan: 10 of 110 (9 %); counted inside the 200 accepted tasks
|
||||
@@ -240,7 +397,7 @@ def main():
|
||||
ap.add_argument("--part", type=int, default=0)
|
||||
ap.add_argument("--parts", type=int, default=1)
|
||||
ap.add_argument("--target", type=int, default=200)
|
||||
ap.add_argument("--plan", default="plan", help="plan (first 223 slots) or plan2 (hard and error share raised)")
|
||||
ap.add_argument("--plan", default="plan", help="plan (first 223 slots), plan2 (hard and error share raised) or balanced (kind with the biggest deficit)")
|
||||
ap.add_argument("--deadline", help="YYYY-MM-DDTHH:MM local time: no new slot after it")
|
||||
ap.add_argument("--k-count", type=int, default=K_COUNT)
|
||||
ap.add_argument("--stop-ledger", type=float, default=27.0, help="ledger USD for this phase (10 USD usage = 27)")
|
||||
@@ -255,6 +412,9 @@ def main():
|
||||
run_k(a.k_count)
|
||||
return
|
||||
dl = time.mktime(time.strptime(a.deadline, "%Y-%m-%dT%H:%M")) if a.deadline else None
|
||||
if a.plan == "balanced":
|
||||
run_balanced(a.part, a.parts, dl)
|
||||
return
|
||||
run(a.part, a.parts, a.target, a.stop_ledger, a.plan, dl)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from .adt_client import load_env
|
||||
from .agents import LlmAgent
|
||||
from . import infra
|
||||
from .ledger import BudgetExceeded, check_budget, spent
|
||||
from .task import prefix_for
|
||||
from .record import LEDGER_TO_USAGE
|
||||
from .runner import Runner
|
||||
|
||||
@@ -26,7 +28,20 @@ POOL = os.path.join(ROOT, "tasks_gen", "train")
|
||||
OUT = os.path.join(ROOT, "runs", "traj")
|
||||
RUN_BASE = 200000 # 200000 + (task number - 1000) * 3 + attempt (a digit must lead the 4-char base36 run: < 466560)
|
||||
MODEL = "deepseek-v4.1-flash:cloud"
|
||||
CDS_CALLS = 100 # tool-call budget for tasks with a CDS contract object (eval keeps 60; Kral 2026-10-05)
|
||||
# empty_response fix (2026-10-06, docs/empty-response.md): 13 of 119 runs ended with one turn that used the whole output limit on
|
||||
# reasoning. Cap per turn 24000 (only 3 of 1997 turns were legitimately longer), one retry at another temperature (the same
|
||||
# sample ran away again in 7 of 13 runs), and the stream guard (a streamed turn with only reasoning is cut after that many reasoning
|
||||
# tokens) which stays off until it is verified on the cloud model (env STREAM_GUARD, for example 9000).
|
||||
TEACHER_MAX_TOKENS = 24000
|
||||
EMPTY_RETRIES = 1
|
||||
RETRY_TEMPERATURE = 0.8
|
||||
STREAM_GUARD = int(os.environ["STREAM_GUARD"]) if os.environ.get("STREAM_GUARD") else None
|
||||
CDS_CALLS = 100
|
||||
|
||||
|
||||
def new_agent():
|
||||
return LlmAgent(MODEL, loop_guard=3, max_tokens=TEACHER_MAX_TOKENS, empty_retries=EMPTY_RETRIES,
|
||||
retry_temperature=RETRY_TEMPERATURE, stream_guard=STREAM_GUARD) # tool-call budget for tasks with a CDS contract object (eval keeps 60; Kral 2026-10-05)
|
||||
LOCK = threading.Lock()
|
||||
|
||||
|
||||
@@ -72,12 +87,30 @@ def one(task_id, attempt, stop):
|
||||
print("BUDGET", e, flush=True)
|
||||
return
|
||||
run_no = RUN_BASE + (int("".join(c for c in task_id if c.isdigit())) - 1000) * 3 + attempt
|
||||
agent = LlmAgent(MODEL, loop_guard=3)
|
||||
agent = new_agent()
|
||||
runner = Runner(POOL, OUT)
|
||||
t0 = time.time()
|
||||
row = {"task": task_id, "attempt": attempt, "run": run_no, "model": MODEL}
|
||||
try:
|
||||
rep, run_dir = runner.run(task_id, agent, run_no, teardown=True, cds_calls=CDS_CALLS)
|
||||
for _try in range(4): # an outage of MCP or A4H is waited out and the same run starts again
|
||||
try:
|
||||
rep, run_dir = runner.run(task_id, agent, run_no, teardown=True, cds_calls=CDS_CALLS)
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
if not infra.is_outage(e) or _try == 3:
|
||||
raise
|
||||
print(time.strftime("%F %T"), "infra outage at", task_id, "->", repr(e)[:120], "; waiting", flush=True)
|
||||
append("outages.jsonl", {"t": time.time(), "task": task_id, "attempt": attempt, "error": repr(e)[:200]})
|
||||
if not infra.wait_until_up():
|
||||
raise
|
||||
try:
|
||||
infra.cleanup_prefix(prefix_for(run_no, task_id))
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
for d in glob.glob(os.path.join(OUT, f"{run_no}_{task_id}_*")):
|
||||
os.makedirs(os.path.join(OUT, "_aborted"), exist_ok=True)
|
||||
os.rename(d, os.path.join(OUT, "_aborted", os.path.basename(d) + "_" + str(int(time.time()))))
|
||||
agent = new_agent()
|
||||
score = (rep.get("score") or {}).get("total")
|
||||
rec = os.path.exists(os.path.join(run_dir, "record.json"))
|
||||
row.update(score=score, setup_failed=bool(rep.get("setup_failed")), end_reason=rep.get("end_reason"),
|
||||
|
||||
22
scripts_probe/after_owntests.sh
Executable file
22
scripts_probe/after_owntests.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Waits for harness.owntests to end, then: report, rebuild the stage 2 set with the own-test score as metadata, update the HF dataset, commit.
|
||||
cd "$HOME/projects/abap-llm/harness" || exit 1
|
||||
PID="$1"
|
||||
while kill -0 "$PID" 2>/dev/null; do sleep 60; done
|
||||
python3 train/own_test_report.py > runs/own_test_report.log 2>&1
|
||||
train/.venv/bin/python train/build_stage2.py --out runs/stage2_data --hook hooks_example:own_test_weight > runs/build_after_owntests.log 2>&1
|
||||
python3 train/build_doc.py >> runs/build_after_owntests.log 2>&1
|
||||
set -a; . ./.env; set +a
|
||||
train/.venv/bin/python - >> runs/build_after_owntests.log 2>&1 <<'PY'
|
||||
import os
|
||||
from huggingface_hub import HfApi
|
||||
api = HfApi(token=os.environ["HF_TOKEN"])
|
||||
for f in ("stage2_train.jsonl", "stage2_valid.jsonl", "stage2_reserve.jsonl", "build_report.json", "README.md"):
|
||||
api.upload_file(path_or_fileobj="runs/stage2_data/" + f, path_in_repo=f, repo_id="erhankeseli/abap-stage2-data", repo_type="dataset")
|
||||
print("uploaded")
|
||||
PY
|
||||
export GIT_AUTHOR_NAME=Kral GIT_AUTHOR_EMAIL=kral@local GIT_COMMITTER_NAME=Kral GIT_COMMITTER_EMAIL=kral@local
|
||||
git add docs train && git commit -q -m "D: own-test mutation scores of the accepted trajectories (metadata only), stage 2 set rebuilt with the score
|
||||
|
||||
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>"
|
||||
echo "after_owntests done $(date)" >> runs/own_test_report.log
|
||||
41
scripts_probe/deltest.py
Normal file
41
scripts_probe/deltest.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Delete an object through ADT while a write on it is running (what happened on 2026-10-05 19:25)."""
|
||||
import json, sys, threading, time
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
|
||||
from lockprobe import *
|
||||
from killtest import MAIN, tc
|
||||
|
||||
def attempt(delay, n, extra=40):
|
||||
name = "ZPROBE0DL_%03d" % n
|
||||
with McpClient() as m:
|
||||
m.call("sap_create_object", {"objectType": "CLAS", "objectName": name, "packageName": "$TMP", "description": "delete race"})
|
||||
m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "source": MAIN.format(n=name.lower())})
|
||||
res = {}
|
||||
def writer():
|
||||
with McpClient() as m:
|
||||
res["t_write_start"] = time.time()
|
||||
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "includeType": "testclasses", "source": tc(extra)})
|
||||
res["write"] = t[:260]
|
||||
res["t_write_end"] = time.time()
|
||||
th = threading.Thread(target=writer); th.start()
|
||||
time.sleep(delay)
|
||||
res["delete_during_write"] = delete_uris(["/sap/bc/adt/oo/classes/" + name.lower()])
|
||||
th.join()
|
||||
time.sleep(2)
|
||||
with McpClient() as m:
|
||||
res["enqueue"] = read_locks(m)
|
||||
e, t = m.call("sap_push_element", {"objectType": "CLAS", "objectName": name, "element": "RUN", "source": " METHOD run.\n rv = 2.\n ENDMETHOD.\n"})
|
||||
res["follow_up"] = t[:200]
|
||||
e, t = m.call("sap_search_object", {"query": name})
|
||||
res["still_exists"] = name in t
|
||||
res["delete_after"] = delete_uris(["/sap/bc/adt/oo/classes/" + name.lower()]) if res["still_exists"] else "n/a"
|
||||
res["name"], res["delay"] = name, delay
|
||||
return res
|
||||
|
||||
if __name__ == "__main__":
|
||||
for i, d in enumerate(float(x) for x in sys.argv[1].split(",")):
|
||||
r = attempt(d, int(sys.argv[2]) + i)
|
||||
short = {k: v for k, v in r.items() if k not in ("t_write_start", "t_write_end")}
|
||||
short["enqueue"] = short["enqueue"][:600]
|
||||
print(json.dumps(short, indent=1))
|
||||
if "[LOCK]" in r["follow_up"] or "enqueue entries matching the probe prefixes: 0" not in r["enqueue"]:
|
||||
print("POSSIBLE LEAK at delay", d, r["name"]); break
|
||||
52
scripts_probe/killcreate.py
Normal file
52
scripts_probe/killcreate.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Two clients both CREATE and WRITE the same table at the same time (two controllers installing the seed of run 200273)."""
|
||||
import json, os, signal, subprocess, sys, time
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
|
||||
from lockprobe import *
|
||||
from killtabl import DDL
|
||||
|
||||
VICTIM2 = r'''
|
||||
import sys, json
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness")
|
||||
from harness.adt_client import load_env; load_env("/Users/erhankeseli/projects/abap-llm/harness/.env")
|
||||
from harness.mcp_client import McpClient
|
||||
a = json.loads(sys.argv[1])
|
||||
m = McpClient().open()
|
||||
print("SENT", flush=True)
|
||||
m.call("sap_create_object", {"objectType": "TABL", "objectName": a["objectName"], "packageName": "$TMP", "description": "seed"})
|
||||
print("CREATED", flush=True)
|
||||
m.call("sap_push_source", a)
|
||||
print("DONE", flush=True)
|
||||
'''
|
||||
def attempt(delay, n):
|
||||
name = "ZPROBE0K3_%03d" % n
|
||||
args = {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=20 + n)}
|
||||
ps = [subprocess.Popen([sys.executable, "-c", VICTIM2, json.dumps(args)], stdout=subprocess.PIPE, text=True) for _ in range(2)]
|
||||
for p in ps:
|
||||
assert p.stdout.readline().strip() == "SENT"
|
||||
time.sleep(delay)
|
||||
states = []
|
||||
for p in ps:
|
||||
states.append("finished" if p.poll() is not None else "killed")
|
||||
try: os.kill(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError: pass
|
||||
p.wait()
|
||||
time.sleep(3)
|
||||
out = {"name": name, "delay": delay, "victims": states}
|
||||
with McpClient() as m:
|
||||
out["enqueue"] = read_locks(m)
|
||||
e, t = m.call("sap_push_source", {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=40 + n)})
|
||||
out["follow_up"] = t[:300]
|
||||
e, t = m.call("sap_search_object", {"query": name}); out["exists"] = name in t
|
||||
out["leak"] = "[LOCK]" in out["follow_up"] or "ZPROBE0K3" in out["enqueue"]
|
||||
if out["exists"]:
|
||||
out["delete"] = {k.split("/")[-1]: (v["deleted"], v["msg"]) for k, v in delete_uris(["/sap/bc/adt/ddic/tables/" + name.lower()]).items()}
|
||||
return out
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = int(sys.argv[2])
|
||||
for d in (float(x) for x in sys.argv[1].split(",")):
|
||||
o = attempt(d, n); n += 1
|
||||
o["enqueue"] = o["enqueue"][:1200]
|
||||
print(json.dumps(o, indent=1))
|
||||
if o["leak"]:
|
||||
print("LEAK at delay", d, "object", o["name"]); break
|
||||
51
scripts_probe/killtabl.py
Normal file
51
scripts_probe/killtabl.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Controlled kill during the activation of a DDIC table (database table creation takes seconds)."""
|
||||
import json, os, signal, subprocess, sys, time
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
|
||||
from lockprobe import *
|
||||
from killtest import VICTIM
|
||||
|
||||
DDL = """@EndUserText.label : 'kill test'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {n} {{
|
||||
key client : abap.clnt not null;
|
||||
key item_id : abap.char(10) not null;
|
||||
qty : abap.int4;
|
||||
name : abap.char({w});
|
||||
}}"""
|
||||
|
||||
def attempt(delay, n):
|
||||
name = "ZPROBE0KT_%03d" % n
|
||||
with McpClient() as m:
|
||||
print("create", m.call("sap_create_object", {"objectType": "TABL", "objectName": name, "packageName": "$TMP", "description": "kill table"})[1][:50])
|
||||
# first write alone, to learn how long it takes
|
||||
args = {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=20 + n)}
|
||||
p = subprocess.Popen([sys.executable, "-c", VICTIM, json.dumps(args)], stdout=subprocess.PIPE, text=True)
|
||||
assert p.stdout.readline().strip() == "SENT"
|
||||
t0 = time.time(); time.sleep(delay)
|
||||
finished = p.poll() is not None
|
||||
try:
|
||||
os.kill(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
finished = True
|
||||
p.wait()
|
||||
print("table write: victim %s after %.2f s" % ("had finished" if finished else "killed", time.time() - t0))
|
||||
time.sleep(3)
|
||||
out = {"name": name, "delay": delay, "finished_before_kill": finished}
|
||||
with McpClient() as m:
|
||||
out["enqueue"] = read_locks(m)
|
||||
e, t = m.call("sap_push_source", {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=40 + n)})
|
||||
out["second_write"] = t[:300]
|
||||
out["leak"] = "[LOCK]" in out["second_write"] or ("ZPROBE0KT" in out["enqueue"] and "enqueue entries matching the probe prefixes: 0" not in out["enqueue"])
|
||||
out["delete"] = delete_uris(["/sap/bc/adt/ddic/tables/" + name.lower()])
|
||||
return out
|
||||
|
||||
if __name__ == "__main__":
|
||||
for i, d in enumerate(float(x) for x in sys.argv[1].split(",")):
|
||||
o = attempt(d, int(sys.argv[2]) + i)
|
||||
o["enqueue"] = o["enqueue"][:900]
|
||||
print(json.dumps(o, indent=1))
|
||||
if o["leak"]:
|
||||
print("LEAK at delay", d, "object", o["name"]); break
|
||||
71
scripts_probe/killtest.py
Normal file
71
scripts_probe/killtest.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Controlled kill: a client sends sap_push_source (testclasses include) and is killed with SIGKILL `delay` seconds
|
||||
after the request left. Then the enqueue table is read and a second write is tried."""
|
||||
import json, os, signal, subprocess, sys, time
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
|
||||
from lockprobe import *
|
||||
|
||||
MAIN = """CLASS {n} DEFINITION PUBLIC FINAL CREATE PUBLIC.
|
||||
PUBLIC SECTION.
|
||||
METHODS run RETURNING VALUE(rv) TYPE i.
|
||||
ENDCLASS.
|
||||
CLASS {n} IMPLEMENTATION.
|
||||
METHOD run.
|
||||
rv = 1.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
"""
|
||||
def tc(extra):
|
||||
body = "".join(" cl_abap_unit_assert=>assert_equals( act = %d exp = %d ).\n" % (i, i) for i in range(extra))
|
||||
return ("CLASS ltc DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.\n PRIVATE SECTION.\n METHODS t1 FOR TESTING.\n"
|
||||
"ENDCLASS.\nCLASS ltc IMPLEMENTATION.\n METHOD t1.\n" + body + " ENDMETHOD.\nENDCLASS.\n")
|
||||
|
||||
VICTIM = r'''
|
||||
import sys, json
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness")
|
||||
from harness.adt_client import load_env; load_env("/Users/erhankeseli/projects/abap-llm/harness/.env")
|
||||
from harness.mcp_client import McpClient
|
||||
args = json.loads(sys.argv[1])
|
||||
m = McpClient().open()
|
||||
print("SENT", flush=True)
|
||||
m.call("sap_push_source", args)
|
||||
print("DONE", flush=True)
|
||||
'''
|
||||
|
||||
def attempt(delay, n, extra=40):
|
||||
name = "ZPROBE0KL_%03d" % n
|
||||
with McpClient() as m:
|
||||
print("create", m.call("sap_create_object", {"objectType": "CLAS", "objectName": name, "packageName": "$TMP", "description": "kill test"})[1][:60])
|
||||
print("main ", m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "source": MAIN.format(n=name.lower())})[1][:70])
|
||||
args = {"objectType": "CLAS", "objectName": name, "includeType": "testclasses", "source": tc(extra)}
|
||||
p = subprocess.Popen([sys.executable, "-c", VICTIM, json.dumps(args)], stdout=subprocess.PIPE, text=True)
|
||||
assert p.stdout.readline().strip() == "SENT"
|
||||
t0 = time.time()
|
||||
time.sleep(delay)
|
||||
done_before_kill = p.poll() is not None
|
||||
try:
|
||||
os.kill(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
done_before_kill = True
|
||||
p.wait()
|
||||
print("victim killed %.2f s after the request was sent (finished before kill: %s)" % (time.time() - t0, done_before_kill))
|
||||
time.sleep(3)
|
||||
out = {"name": name, "delay": delay}
|
||||
with McpClient() as m:
|
||||
out["enqueue"] = read_locks(m)
|
||||
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": name, "includeType": "testclasses", "source": tc(extra + 1)})
|
||||
out["second_write"] = t[:300]
|
||||
e, t = m.call("sap_push_element", {"objectType": "CLAS", "objectName": name, "element": "RUN", "source": " METHOD run.\n rv = 2.\n ENDMETHOD.\n"})
|
||||
out["push_element"] = t[:300]
|
||||
out["leak"] = "currently editing" in out["second_write"] or "[LOCK]" in out["second_write"] or "[LOCK]" in out["push_element"]
|
||||
if not out["leak"]:
|
||||
out["delete"] = delete_uris(["/sap/bc/adt/oo/classes/" + name.lower()])
|
||||
return out
|
||||
|
||||
if __name__ == "__main__":
|
||||
delays = [float(x) for x in sys.argv[1].split(",")]
|
||||
n0 = int(sys.argv[2]) if len(sys.argv) > 2 else 1
|
||||
for i, d in enumerate(delays):
|
||||
o = attempt(d, n0 + i, int(sys.argv[3]) if len(sys.argv) > 3 else 40)
|
||||
print(json.dumps({k: (v if k != "enqueue" else v[:700]) for k, v in o.items()}, indent=1))
|
||||
if o["leak"]:
|
||||
print("LEAK at delay", d, "- stopping, object", o["name"], "needs SM12"); break
|
||||
42
scripts_probe/killtwo.py
Normal file
42
scripts_probe/killtwo.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Two clients write the same table at the same time (what two controllers did with run 200273); both are killed."""
|
||||
import json, os, signal, subprocess, sys, time
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness/scripts_probe")
|
||||
from lockprobe import *
|
||||
from killtest import VICTIM
|
||||
from killtabl import DDL
|
||||
|
||||
def attempt(delay, n):
|
||||
name = "ZPROBE0K2_%03d" % n
|
||||
with McpClient() as m:
|
||||
m.call("sap_create_object", {"objectType": "TABL", "objectName": name, "packageName": "$TMP", "description": "two writers"})
|
||||
args = {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=20 + n)}
|
||||
ps = [subprocess.Popen([sys.executable, "-c", VICTIM, json.dumps(args)], stdout=subprocess.PIPE, text=True) for _ in range(2)]
|
||||
for p in ps:
|
||||
assert p.stdout.readline().strip() == "SENT"
|
||||
time.sleep(delay)
|
||||
states = []
|
||||
for p in ps:
|
||||
states.append("finished" if p.poll() is not None else "killed")
|
||||
try:
|
||||
os.kill(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
p.wait()
|
||||
time.sleep(3)
|
||||
out = {"name": name, "delay": delay, "victims": states}
|
||||
with McpClient() as m:
|
||||
out["enqueue"] = read_locks(m)
|
||||
e, t = m.call("sap_push_source", {"objectType": "TABL", "objectName": name, "source": DDL.format(n=name.lower(), w=40 + n)})
|
||||
out["follow_up"] = t[:300]
|
||||
out["leak"] = "[LOCK]" in out["follow_up"] or ("ZPROBE0K2" in out["enqueue"])
|
||||
out["delete"] = {k.split("/")[-1]: v["deleted"] for k, v in delete_uris(["/sap/bc/adt/ddic/tables/" + name.lower()]).items()}
|
||||
return out
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = int(sys.argv[2])
|
||||
for d in (float(x) for x in sys.argv[1].split(",")):
|
||||
o = attempt(d, n); n += 1
|
||||
o["enqueue"] = o["enqueue"][:1200]
|
||||
print(json.dumps(o, indent=1))
|
||||
if o["leak"]:
|
||||
print("LEAK at delay", d, "object", o["name"]); break
|
||||
59
scripts_probe/lockprobe.py
Normal file
59
scripts_probe/lockprobe.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Lock leak probe helpers (no cloud, A4H only): the enqueue reader class and a controlled kill of a writing client."""
|
||||
import json, os, signal, subprocess, sys, time
|
||||
sys.path.insert(0, "/Users/erhankeseli/projects/abap-llm/harness")
|
||||
from harness.adt_client import load_env
|
||||
load_env("/Users/erhankeseli/projects/abap-llm/harness/.env")
|
||||
from harness.mcp_client import McpClient
|
||||
from harness.runner import delete_uris
|
||||
|
||||
READER = "ZPROBE0EQ_LOCKS"
|
||||
READER_SRC = """CLASS zprobe0eq_locks DEFINITION PUBLIC FINAL CREATE PUBLIC.
|
||||
PUBLIC SECTION.
|
||||
INTERFACES if_oo_adt_classrun.
|
||||
ENDCLASS.
|
||||
CLASS zprobe0eq_locks IMPLEMENTATION.
|
||||
METHOD if_oo_adt_classrun~main.
|
||||
DATA lt_enq TYPE STANDARD TABLE OF seqg3 WITH DEFAULT KEY.
|
||||
DATA lv_subrc TYPE sy-subrc.
|
||||
CALL FUNCTION 'ENQUEUE_READ'
|
||||
EXPORTING gclient = sy-mandt gname = '' garg = '' guname = ''
|
||||
IMPORTING subrc = lv_subrc
|
||||
TABLES enq = lt_enq
|
||||
EXCEPTIONS communication_failure = 1 system_failure = 2 OTHERS = 3.
|
||||
DATA(lv_n) = 0.
|
||||
LOOP AT lt_enq ASSIGNING FIELD-SYMBOL(<l>).
|
||||
lv_n = lv_n + 1.
|
||||
DATA(lv_line) = ||.
|
||||
DO.
|
||||
ASSIGN COMPONENT sy-index OF STRUCTURE <l> TO FIELD-SYMBOL(<f>).
|
||||
IF sy-subrc <> 0. EXIT. ENDIF.
|
||||
DATA(lo_d) = CAST cl_abap_structdescr( cl_abap_typedescr=>describe_by_data( <l> ) ).
|
||||
DATA(lv_name) = lo_d->components[ sy-index ]-name.
|
||||
IF <f> IS NOT INITIAL.
|
||||
lv_line = lv_line && lv_name && '=' && condense( |{ <f> }| ) && ' '.
|
||||
ENDIF.
|
||||
ENDDO.
|
||||
out->write( lv_line ).
|
||||
ENDLOOP.
|
||||
out->write( |enqueue entries listed: { lv_n } of { lines( lt_enq ) } (subrc { lv_subrc })| ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
"""
|
||||
|
||||
def ensure_reader(m, force=False):
|
||||
e, t = m.call("sap_search_object", {"query": READER})
|
||||
if READER not in t or force:
|
||||
if READER not in t:
|
||||
m.call("sap_create_object", {"objectType": "CLAS", "objectName": READER, "packageName": "$TMP", "description": "enqueue reader probe"})
|
||||
e, t = m.call("sap_push_source", {"objectType": "CLAS", "objectName": READER, "source": READER_SRC})
|
||||
if '"success":true' not in t.replace(" ", ""):
|
||||
print("reader activation:", t[:600])
|
||||
|
||||
def read_locks(m):
|
||||
e, t = m.call("sap_run_class", {"className": READER})
|
||||
return t
|
||||
|
||||
if __name__ == "__main__":
|
||||
with McpClient() as m:
|
||||
ensure_reader(m, force=True)
|
||||
print(read_locks(m)[:3500])
|
||||
105
tasks_gen/train/G1140/faulty/m0_oil_report.prog.abap
Normal file
105
tasks_gen/train/G1140/faulty/m0_oil_report.prog.abap
Normal file
@@ -0,0 +1,105 @@
|
||||
REPORT {{p}}oil_report.
|
||||
|
||||
DATA gv_region TYPE {{p}}grove-region.
|
||||
|
||||
PARAMETERS p_from TYPE d OBLIGATORY.
|
||||
SELECT-OPTIONS s_reg FOR gv_region.
|
||||
|
||||
CLASS lcl_report DEFINITION FINAL.
|
||||
PUBLIC SECTION.
|
||||
TYPES:
|
||||
BEGIN OF ty_line,
|
||||
region TYPE {{p}}grove-region,
|
||||
litres TYPE {{p}}press-litres,
|
||||
kilos TYPE {{p}}press-kilos,
|
||||
END OF ty_line,
|
||||
tt_line TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY.
|
||||
TYPES tt_region TYPE RANGE OF {{p}}grove-region.
|
||||
METHODS collect
|
||||
IMPORTING iv_from TYPE d
|
||||
it_region TYPE tt_region
|
||||
RETURNING VALUE(rt_lines) TYPE tt_line.
|
||||
METHODS show
|
||||
CHANGING ct_lines TYPE tt_line.
|
||||
ENDCLASS.
|
||||
|
||||
CLASS lcl_report IMPLEMENTATION.
|
||||
METHOD collect.
|
||||
SELECT gr~region,
|
||||
SUM( pr~litres ) AS litres,
|
||||
SUM( pr~kilos ) AS kilos
|
||||
FROM {{p}}press AS pr
|
||||
INNER JOIN {{p}}grove AS gr ON gr~grove_id = pr~grove_id
|
||||
WHERE pr~press_date < @iv_from
|
||||
AND gr~region IN @it_region
|
||||
GROUP BY gr~region
|
||||
ORDER BY litres DESCENDING
|
||||
INTO CORRESPONDING FIELDS OF TABLE @rt_lines.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD show.
|
||||
TRY.
|
||||
cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
|
||||
CHANGING t_table = ct_lines ).
|
||||
lo_alv->display( ).
|
||||
CATCH cx_salv_msg INTO DATA(lx_error).
|
||||
MESSAGE lx_error TYPE 'I' DISPLAY LIKE 'E'.
|
||||
ENDTRY.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
|
||||
START-OF-SELECTION.
|
||||
DATA(go_report) = NEW lcl_report( ).
|
||||
DATA(gt_lines) = go_report->collect( iv_from = p_from it_region = s_reg[] ).
|
||||
go_report->show( CHANGING ct_lines = gt_lines ).
|
||||
|
||||
CLASS ltc_report DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
|
||||
CLASS-METHODS class_setup.
|
||||
CLASS-METHODS class_teardown.
|
||||
METHODS litres_per_region FOR TESTING.
|
||||
METHODS region_filter FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
CLASS ltc_report IMPLEMENTATION.
|
||||
METHOD class_setup.
|
||||
go_env = cl_osql_test_environment=>create(
|
||||
i_dependency_list = VALUE #( ( '{{P}}GROVE' ) ( '{{P}}PRESS' ) ) ).
|
||||
DATA lt_grove TYPE STANDARD TABLE OF {{p}}grove WITH EMPTY KEY.
|
||||
lt_grove = VALUE #( ( grove_id = 1 grove_name = 'A' region = 'NORTH' variety = 'ARBEQ' )
|
||||
( grove_id = 2 grove_name = 'B' region = 'NORTH' variety = 'ARBEQ' )
|
||||
( grove_id = 3 grove_name = 'C' region = 'SOUTH' variety = 'PICUA' ) ).
|
||||
go_env->insert_test_data( lt_grove ).
|
||||
DATA lt_press TYPE STANDARD TABLE OF {{p}}press WITH EMPTY KEY.
|
||||
lt_press = VALUE #( ( press_id = 1 grove_id = 1 press_date = '20260110' kilos = 1000 litres = '200.00' )
|
||||
( press_id = 2 grove_id = 2 press_date = '20260111' kilos = 500 litres = '90.00' )
|
||||
( press_id = 3 grove_id = 3 press_date = '20260112' kilos = 800 litres = '150.00' ) ).
|
||||
go_env->insert_test_data( lt_press ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD class_teardown.
|
||||
go_env->destroy( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD litres_per_region.
|
||||
DATA lv_expected TYPE {{p}}press-litres.
|
||||
lv_expected = '290.00'.
|
||||
DATA(lt_lines) = NEW lcl_report( )->collect(
|
||||
iv_from = '20260101' it_region = VALUE #( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_lines ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'NORTH' act = lt_lines[ 1 ]-region ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1500 act = lt_lines[ 1 ]-kilos ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD region_filter.
|
||||
DATA lv_expected TYPE {{p}}press-litres.
|
||||
lv_expected = '150.00'.
|
||||
DATA(lt_lines) = NEW lcl_report( )->collect(
|
||||
iv_from = '20260101'
|
||||
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
105
tasks_gen/train/G1140/faulty/m1_oil_report.prog.abap
Normal file
105
tasks_gen/train/G1140/faulty/m1_oil_report.prog.abap
Normal file
@@ -0,0 +1,105 @@
|
||||
REPORT {{p}}oil_report.
|
||||
|
||||
DATA gv_region TYPE {{p}}grove-region.
|
||||
|
||||
PARAMETERS p_from TYPE d OBLIGATORY.
|
||||
SELECT-OPTIONS s_reg FOR gv_region.
|
||||
|
||||
CLASS lcl_report DEFINITION FINAL.
|
||||
PUBLIC SECTION.
|
||||
TYPES:
|
||||
BEGIN OF ty_line,
|
||||
region TYPE {{p}}grove-region,
|
||||
litres TYPE {{p}}press-litres,
|
||||
kilos TYPE {{p}}press-kilos,
|
||||
END OF ty_line,
|
||||
tt_line TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY.
|
||||
TYPES tt_region TYPE RANGE OF {{p}}grove-region.
|
||||
METHODS collect
|
||||
IMPORTING iv_from TYPE d
|
||||
it_region TYPE tt_region
|
||||
RETURNING VALUE(rt_lines) TYPE tt_line.
|
||||
METHODS show
|
||||
CHANGING ct_lines TYPE tt_line.
|
||||
ENDCLASS.
|
||||
|
||||
CLASS lcl_report IMPLEMENTATION.
|
||||
METHOD collect.
|
||||
SELECT gr~region,
|
||||
SUM( pr~litres ) AS litres,
|
||||
SUM( pr~kilos ) AS kilos
|
||||
FROM {{p}}press AS pr
|
||||
INNER JOIN {{p}}grove AS gr ON gr~grove_id = pr~grove_id
|
||||
WHERE pr~press_date >= @iv_from
|
||||
OR gr~region IN @it_region
|
||||
GROUP BY gr~region
|
||||
ORDER BY litres DESCENDING
|
||||
INTO CORRESPONDING FIELDS OF TABLE @rt_lines.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD show.
|
||||
TRY.
|
||||
cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
|
||||
CHANGING t_table = ct_lines ).
|
||||
lo_alv->display( ).
|
||||
CATCH cx_salv_msg INTO DATA(lx_error).
|
||||
MESSAGE lx_error TYPE 'I' DISPLAY LIKE 'E'.
|
||||
ENDTRY.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
|
||||
START-OF-SELECTION.
|
||||
DATA(go_report) = NEW lcl_report( ).
|
||||
DATA(gt_lines) = go_report->collect( iv_from = p_from it_region = s_reg[] ).
|
||||
go_report->show( CHANGING ct_lines = gt_lines ).
|
||||
|
||||
CLASS ltc_report DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
|
||||
CLASS-METHODS class_setup.
|
||||
CLASS-METHODS class_teardown.
|
||||
METHODS litres_per_region FOR TESTING.
|
||||
METHODS region_filter FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
CLASS ltc_report IMPLEMENTATION.
|
||||
METHOD class_setup.
|
||||
go_env = cl_osql_test_environment=>create(
|
||||
i_dependency_list = VALUE #( ( '{{P}}GROVE' ) ( '{{P}}PRESS' ) ) ).
|
||||
DATA lt_grove TYPE STANDARD TABLE OF {{p}}grove WITH EMPTY KEY.
|
||||
lt_grove = VALUE #( ( grove_id = 1 grove_name = 'A' region = 'NORTH' variety = 'ARBEQ' )
|
||||
( grove_id = 2 grove_name = 'B' region = 'NORTH' variety = 'ARBEQ' )
|
||||
( grove_id = 3 grove_name = 'C' region = 'SOUTH' variety = 'PICUA' ) ).
|
||||
go_env->insert_test_data( lt_grove ).
|
||||
DATA lt_press TYPE STANDARD TABLE OF {{p}}press WITH EMPTY KEY.
|
||||
lt_press = VALUE #( ( press_id = 1 grove_id = 1 press_date = '20260110' kilos = 1000 litres = '200.00' )
|
||||
( press_id = 2 grove_id = 2 press_date = '20260111' kilos = 500 litres = '90.00' )
|
||||
( press_id = 3 grove_id = 3 press_date = '20260112' kilos = 800 litres = '150.00' ) ).
|
||||
go_env->insert_test_data( lt_press ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD class_teardown.
|
||||
go_env->destroy( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD litres_per_region.
|
||||
DATA lv_expected TYPE {{p}}press-litres.
|
||||
lv_expected = '290.00'.
|
||||
DATA(lt_lines) = NEW lcl_report( )->collect(
|
||||
iv_from = '20260101' it_region = VALUE #( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_lines ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'NORTH' act = lt_lines[ 1 ]-region ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1500 act = lt_lines[ 1 ]-kilos ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD region_filter.
|
||||
DATA lv_expected TYPE {{p}}press-litres.
|
||||
lv_expected = '150.00'.
|
||||
DATA(lt_lines) = NEW lcl_report( )->collect(
|
||||
iv_from = '20260101'
|
||||
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lv_expected act = lt_lines[ 1 ]-litres ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
34
tasks_gen/train/G1140/generation.json
Normal file
34
tasks_gen/train/G1140/generation.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "G1140",
|
||||
"pool": "train",
|
||||
"object_type": "PROG",
|
||||
"category": "B",
|
||||
"attempts": [
|
||||
{
|
||||
"stage": "bundle",
|
||||
"errors": [
|
||||
"Too close to task G0020 (similarity spec 0.67, rules 0.49, names 1.00). Choose a different business topic and different object names.",
|
||||
"Too close to task G0183 (similarity spec 0.60, rules 0.00, names 1.00). Choose a different business topic and different object names."
|
||||
],
|
||||
"by_check": {
|
||||
"extra": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 86.7,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 2,
|
||||
"killed": 2,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepted": true
|
||||
}
|
||||
@@ -15,7 +15,7 @@ CLASS {{p}}mill_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
IMPORTING ir_data TYPE REF TO data
|
||||
iv_row TYPE i
|
||||
iv_col TYPE string
|
||||
RETURNING VALUE(rv_value) TYPE string.
|
||||
RETURNING VALUE(rv_value) TYPE {{p}}press-litres.
|
||||
METHODS lines_of
|
||||
IMPORTING ir_data TYPE REF TO data
|
||||
RETURNING VALUE(rv_lines) TYPE i.
|
||||
@@ -60,9 +60,12 @@ CLASS {{p}}mill_hidden IMPLEMENTATION.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD amount_of.
|
||||
DATA lv_amount TYPE p LENGTH 15 DECIMALS 2.
|
||||
lv_amount = value_of( ir_data = ir_data iv_row = iv_row iv_col = iv_col ).
|
||||
rv_value = CONV string( lv_amount ).
|
||||
FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
|
||||
ASSIGN ir_data->* TO <lt_data>.
|
||||
ASSIGN <lt_data>[ iv_row ] TO FIELD-SYMBOL(<ls_line>).
|
||||
ASSIGN COMPONENT iv_col OF STRUCTURE <ls_line> TO FIELD-SYMBOL(<lv_value>).
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = |Column { iv_col } missing| ).
|
||||
rv_value = <lv_value>.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD all_regions.
|
||||
@@ -70,29 +73,40 @@ CLASS {{p}}mill_hidden IMPLEMENTATION.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD sorted_by_litres.
|
||||
DATA(lr_data) = run( '20260101' ).
|
||||
DATA lr_data TYPE REF TO data.
|
||||
lr_data = run( '20260101' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `NORTH` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `SOUTH` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `REGION` ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `WEST` act = value_of( ir_data = lr_data iv_row = 3 iv_col = `REGION` ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD litres_and_kilos.
|
||||
DATA(lr_data) = run( '20260101' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `290.00` act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `1500` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `KILOS` ) ).
|
||||
DATA lr_data TYPE REF TO data.
|
||||
DATA lv_expected TYPE {{p}}press-litres.
|
||||
lr_data = run( '20260101' ).
|
||||
lv_expected = '290.00'.
|
||||
cl_abap_unit_assert=>assert_equals( exp = lv_expected
|
||||
act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `1500`
|
||||
act = value_of( ir_data = lr_data iv_row = 1 iv_col = `KILOS` ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD region_filter.
|
||||
DATA(lr_data) = run( iv_from = '20260101'
|
||||
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
|
||||
DATA lr_data TYPE REF TO data.
|
||||
DATA lv_expected TYPE {{p}}press-litres.
|
||||
lr_data = run( iv_from = '20260101'
|
||||
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
|
||||
lv_expected = '150.00'.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines_of( lr_data ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `150.00` act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lv_expected
|
||||
act = amount_of( ir_data = lr_data iv_row = 1 iv_col = `LITRES` ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD two_region_filter.
|
||||
DATA(lr_data) = run( iv_from = '20260101'
|
||||
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'WEST' )
|
||||
( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
|
||||
DATA lr_data TYPE REF TO data.
|
||||
lr_data = run( iv_from = '20260101'
|
||||
it_region = VALUE #( ( sign = 'I' option = 'EQ' low = 'WEST' )
|
||||
( sign = 'I' option = 'EQ' low = 'SOUTH' ) ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( lr_data ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `SOUTH` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `REGION` ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `WEST` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `REGION` ) ).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task": "T50",
|
||||
"task": "G1140",
|
||||
"mutants": [
|
||||
{
|
||||
"object": "{{P}}OIL_REPORT",
|
||||
@@ -8,37 +8,29 @@
|
||||
"hidden": "0/6",
|
||||
"failed_tests": [
|
||||
"ALL_REGIONS",
|
||||
"SORTED_BY_LITRES",
|
||||
"DATE_FILTER",
|
||||
"LITRES_AND_KILOS",
|
||||
"REGION_FILTER",
|
||||
"TWO_REGION_FILTER",
|
||||
"DATE_FILTER"
|
||||
"SORTED_BY_LITRES",
|
||||
"TWO_REGION_FILTER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}OIL_REPORT",
|
||||
"mutant": "line 34: AND -> OR (logic)",
|
||||
"status": "killed",
|
||||
"hidden": "3/6",
|
||||
"hidden": "1/6",
|
||||
"failed_tests": [
|
||||
"ALL_REGIONS",
|
||||
"DATE_FILTER",
|
||||
"REGION_FILTER",
|
||||
"TWO_REGION_FILTER",
|
||||
"DATE_FILTER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}OIL_REPORT",
|
||||
"mutant": "line 36: DESCENDING -> ASCENDING (order)",
|
||||
"status": "killed",
|
||||
"hidden": "4/6",
|
||||
"failed_tests": [
|
||||
"SORTED_BY_LITRES",
|
||||
"TWO_REGION_FILTER"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid": 3,
|
||||
"killed": 3,
|
||||
"valid": 2,
|
||||
"killed": 2,
|
||||
"kill_rate": 1.0,
|
||||
"ok": true
|
||||
}
|
||||
51
tasks_gen/train/G1900/faulty/m0_if_parcel_fee.intf.abap
Normal file
51
tasks_gen/train/G1900/faulty/m0_if_parcel_fee.intf.abap
Normal file
@@ -0,0 +1,51 @@
|
||||
INTERFACE {{p}}if_parcel_fee PUBLIC.
|
||||
TYPES ty_weight_g TYPE i.
|
||||
TYPES ty_cents TYPE i.
|
||||
TYPES ty_zone TYPE c LENGTH 1.
|
||||
TYPES ty_reason TYPE c LENGTH 9.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_zone,
|
||||
home TYPE ty_zone VALUE '1',
|
||||
national TYPE ty_zone VALUE '2',
|
||||
europe TYPE ty_zone VALUE '3',
|
||||
END OF c_zone.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_reason,
|
||||
ok TYPE ty_reason VALUE 'OK',
|
||||
weight TYPE ty_reason VALUE 'WEIGHT',
|
||||
zone TYPE ty_reason VALUE 'ZONE',
|
||||
END OF c_reason.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_limit,
|
||||
min_weight TYPE ty_weight_g VALUE 1,
|
||||
max_weight TYPE ty_weight_g VALUE 31500,
|
||||
END OF c_limit.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_surcharge,
|
||||
per_step TYPE ty_cents VALUE 90,
|
||||
express TYPE ty_cents VALUE 750,
|
||||
END OF c_surcharge.
|
||||
|
||||
METHODS check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason.
|
||||
|
||||
METHODS base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
ENDINTERFACE.
|
||||
51
tasks_gen/train/G1900/faulty/m1_if_parcel_fee.intf.abap
Normal file
51
tasks_gen/train/G1900/faulty/m1_if_parcel_fee.intf.abap
Normal file
@@ -0,0 +1,51 @@
|
||||
INTERFACE {{p}}if_parcel_fee PUBLIC.
|
||||
TYPES ty_weight_g TYPE i.
|
||||
TYPES ty_cents TYPE i.
|
||||
TYPES ty_zone TYPE c LENGTH 1.
|
||||
TYPES ty_reason TYPE c LENGTH 10.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_zone,
|
||||
home TYPE ty_zone VALUE 'Z',
|
||||
national TYPE ty_zone VALUE '2',
|
||||
europe TYPE ty_zone VALUE '3',
|
||||
END OF c_zone.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_reason,
|
||||
ok TYPE ty_reason VALUE 'OK',
|
||||
weight TYPE ty_reason VALUE 'WEIGHT',
|
||||
zone TYPE ty_reason VALUE 'ZONE',
|
||||
END OF c_reason.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_limit,
|
||||
min_weight TYPE ty_weight_g VALUE 1,
|
||||
max_weight TYPE ty_weight_g VALUE 31500,
|
||||
END OF c_limit.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_surcharge,
|
||||
per_step TYPE ty_cents VALUE 90,
|
||||
express TYPE ty_cents VALUE 750,
|
||||
END OF c_surcharge.
|
||||
|
||||
METHODS check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason.
|
||||
|
||||
METHODS base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
ENDINTERFACE.
|
||||
51
tasks_gen/train/G1900/faulty/m2_if_parcel_fee.intf.abap
Normal file
51
tasks_gen/train/G1900/faulty/m2_if_parcel_fee.intf.abap
Normal file
@@ -0,0 +1,51 @@
|
||||
INTERFACE {{p}}if_parcel_fee PUBLIC.
|
||||
TYPES ty_weight_g TYPE i.
|
||||
TYPES ty_cents TYPE i.
|
||||
TYPES ty_zone TYPE c LENGTH 1.
|
||||
TYPES ty_reason TYPE c LENGTH 10.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_zone,
|
||||
home TYPE ty_zone VALUE '1',
|
||||
national TYPE ty_zone VALUE '2',
|
||||
europe TYPE ty_zone VALUE 'Z',
|
||||
END OF c_zone.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_reason,
|
||||
ok TYPE ty_reason VALUE 'OK',
|
||||
weight TYPE ty_reason VALUE 'WEIGHT',
|
||||
zone TYPE ty_reason VALUE 'ZONE',
|
||||
END OF c_reason.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_limit,
|
||||
min_weight TYPE ty_weight_g VALUE 1,
|
||||
max_weight TYPE ty_weight_g VALUE 31500,
|
||||
END OF c_limit.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_surcharge,
|
||||
per_step TYPE ty_cents VALUE 90,
|
||||
express TYPE ty_cents VALUE 750,
|
||||
END OF c_surcharge.
|
||||
|
||||
METHODS check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason.
|
||||
|
||||
METHODS base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
ENDINTERFACE.
|
||||
51
tasks_gen/train/G1900/faulty/m3_if_parcel_fee.intf.abap
Normal file
51
tasks_gen/train/G1900/faulty/m3_if_parcel_fee.intf.abap
Normal file
@@ -0,0 +1,51 @@
|
||||
INTERFACE {{p}}if_parcel_fee PUBLIC.
|
||||
TYPES ty_weight_g TYPE i.
|
||||
TYPES ty_cents TYPE i.
|
||||
TYPES ty_zone TYPE c LENGTH 1.
|
||||
TYPES ty_reason TYPE c LENGTH 10.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_zone,
|
||||
home TYPE ty_zone VALUE '1',
|
||||
national TYPE ty_zone VALUE '2',
|
||||
europe TYPE ty_zone VALUE '3',
|
||||
END OF c_zone.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_reason,
|
||||
ok TYPE ty_reason VALUE 'ZK',
|
||||
weight TYPE ty_reason VALUE 'WEIGHT',
|
||||
zone TYPE ty_reason VALUE 'ZONE',
|
||||
END OF c_reason.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_limit,
|
||||
min_weight TYPE ty_weight_g VALUE 1,
|
||||
max_weight TYPE ty_weight_g VALUE 31500,
|
||||
END OF c_limit.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_surcharge,
|
||||
per_step TYPE ty_cents VALUE 90,
|
||||
express TYPE ty_cents VALUE 750,
|
||||
END OF c_surcharge.
|
||||
|
||||
METHODS check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason.
|
||||
|
||||
METHODS base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
ENDINTERFACE.
|
||||
51
tasks_gen/train/G1900/faulty/m4_if_parcel_fee.intf.abap
Normal file
51
tasks_gen/train/G1900/faulty/m4_if_parcel_fee.intf.abap
Normal file
@@ -0,0 +1,51 @@
|
||||
INTERFACE {{p}}if_parcel_fee PUBLIC.
|
||||
TYPES ty_weight_g TYPE i.
|
||||
TYPES ty_cents TYPE i.
|
||||
TYPES ty_zone TYPE c LENGTH 1.
|
||||
TYPES ty_reason TYPE c LENGTH 10.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_zone,
|
||||
home TYPE ty_zone VALUE '1',
|
||||
national TYPE ty_zone VALUE '2',
|
||||
europe TYPE ty_zone VALUE '3',
|
||||
END OF c_zone.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_reason,
|
||||
ok TYPE ty_reason VALUE 'OK',
|
||||
weight TYPE ty_reason VALUE 'WEIGHT',
|
||||
zone TYPE ty_reason VALUE 'YONE',
|
||||
END OF c_reason.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_limit,
|
||||
min_weight TYPE ty_weight_g VALUE 1,
|
||||
max_weight TYPE ty_weight_g VALUE 31500,
|
||||
END OF c_limit.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_surcharge,
|
||||
per_step TYPE ty_cents VALUE 90,
|
||||
express TYPE ty_cents VALUE 750,
|
||||
END OF c_surcharge.
|
||||
|
||||
METHODS check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason.
|
||||
|
||||
METHODS base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
ENDINTERFACE.
|
||||
29
tasks_gen/train/G1900/generation.json
Normal file
29
tasks_gen/train/G1900/generation.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "G1900",
|
||||
"pool": "train",
|
||||
"object_type": "INTF",
|
||||
"category": "A",
|
||||
"attempts": [
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 0,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 80.6,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 5,
|
||||
"killed": 5,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepted": true
|
||||
}
|
||||
159
tasks_gen/train/G1900/hidden/parcel_fee_hidden.clas.abap
Normal file
159
tasks_gen/train/G1900/hidden/parcel_fee_hidden.clas.abap
Normal file
@@ -0,0 +1,159 @@
|
||||
CLASS {{p}}parcel_fee_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PUBLIC SECTION.
|
||||
INTERFACES {{p}}if_parcel_fee.
|
||||
PRIVATE SECTION.
|
||||
METHODS zone_values FOR TESTING.
|
||||
METHODS reason_values FOR TESTING.
|
||||
METHODS limit_values FOR TESTING.
|
||||
METHODS surcharge_values FOR TESTING.
|
||||
METHODS type_lengths FOR TESTING.
|
||||
METHODS check_input_sig FOR TESTING.
|
||||
METHODS base_fee_sig FOR TESTING.
|
||||
METHODS weight_surcharge_sig FOR TESTING.
|
||||
METHODS total_fee_sig FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}parcel_fee_hidden IMPLEMENTATION.
|
||||
METHOD {{p}}if_parcel_fee~check_input.
|
||||
IF iv_weight_g < {{p}}if_parcel_fee=>c_limit-min_weight
|
||||
OR iv_weight_g > {{p}}if_parcel_fee=>c_limit-max_weight.
|
||||
rv_reason = {{p}}if_parcel_fee=>c_reason-weight.
|
||||
ELSEIF iv_zone <> {{p}}if_parcel_fee=>c_zone-home
|
||||
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-national
|
||||
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-europe.
|
||||
rv_reason = {{p}}if_parcel_fee=>c_reason-zone.
|
||||
ELSE.
|
||||
rv_reason = {{p}}if_parcel_fee=>c_reason-ok.
|
||||
ENDIF.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_parcel_fee~base_fee.
|
||||
CASE iv_zone.
|
||||
WHEN {{p}}if_parcel_fee=>c_zone-home.
|
||||
rv_cents = 350.
|
||||
WHEN {{p}}if_parcel_fee=>c_zone-national.
|
||||
rv_cents = 620.
|
||||
WHEN {{p}}if_parcel_fee=>c_zone-europe.
|
||||
rv_cents = 990.
|
||||
WHEN OTHERS.
|
||||
rv_cents = 0.
|
||||
ENDCASE.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_parcel_fee~weight_surcharge.
|
||||
IF iv_weight_g > 1000.
|
||||
rv_cents = ( iv_weight_g - 1000 + 999 ) DIV 1000
|
||||
* {{p}}if_parcel_fee=>c_surcharge-per_step.
|
||||
ENDIF.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_parcel_fee~total_fee.
|
||||
IF me->{{p}}if_parcel_fee~check_input( iv_weight_g = iv_weight_g
|
||||
iv_zone = iv_zone )
|
||||
<> {{p}}if_parcel_fee=>c_reason-ok.
|
||||
RETURN.
|
||||
ENDIF.
|
||||
rv_cents = me->{{p}}if_parcel_fee~base_fee( iv_zone )
|
||||
+ me->{{p}}if_parcel_fee~weight_surcharge( iv_weight_g ).
|
||||
IF iv_express = abap_true.
|
||||
rv_cents = rv_cents + {{p}}if_parcel_fee=>c_surcharge-express.
|
||||
ENDIF.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD zone_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = '1' act = {{p}}if_parcel_fee=>c_zone-home ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = '2' act = {{p}}if_parcel_fee=>c_zone-national ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = '3' act = {{p}}if_parcel_fee=>c_zone-europe ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD reason_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'OK' act = {{p}}if_parcel_fee=>c_reason-ok ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'WEIGHT' act = {{p}}if_parcel_fee=>c_reason-weight ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'ZONE' act = {{p}}if_parcel_fee=>c_reason-zone ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD limit_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = {{p}}if_parcel_fee=>c_limit-min_weight ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 31500 act = {{p}}if_parcel_fee=>c_limit-max_weight ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD surcharge_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 90 act = {{p}}if_parcel_fee=>c_surcharge-per_step ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 750 act = {{p}}if_parcel_fee=>c_surcharge-express ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD type_lengths.
|
||||
DATA lv_zone TYPE {{p}}if_parcel_fee=>ty_zone.
|
||||
DATA lv_reason TYPE {{p}}if_parcel_fee=>ty_reason.
|
||||
DATA lv_weight TYPE {{p}}if_parcel_fee=>ty_weight_g.
|
||||
DATA lv_cents TYPE {{p}}if_parcel_fee=>ty_cents.
|
||||
DATA(lv_char1) = 'A'.
|
||||
DATA(lv_char10) = 'ABCDEFGHIJ'.
|
||||
DATA(lo_zone) = cl_abap_typedescr=>describe_by_data( lv_zone ).
|
||||
DATA(lo_reason) = cl_abap_typedescr=>describe_by_data( lv_reason ).
|
||||
DATA(lo_weight) = cl_abap_typedescr=>describe_by_data( lv_weight ).
|
||||
DATA(lo_cents) = cl_abap_typedescr=>describe_by_data( lv_cents ).
|
||||
DATA(lo_char1) = cl_abap_typedescr=>describe_by_data( lv_char1 ).
|
||||
DATA(lo_char10) = cl_abap_typedescr=>describe_by_data( lv_char10 ).
|
||||
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
|
||||
act = lo_zone->type_kind ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lo_char1->length act = lo_zone->length ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
|
||||
act = lo_reason->type_kind ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lo_char10->length act = lo_reason->length ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
|
||||
act = lo_weight->type_kind ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
|
||||
act = lo_cents->type_kind ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD check_input_sig.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = {{p}}if_parcel_fee=>c_reason-ok
|
||||
act = lo_cut->check_input( iv_weight_g = 500 iv_zone = {{p}}if_parcel_fee=>c_zone-home ) ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = {{p}}if_parcel_fee=>c_reason-weight
|
||||
act = lo_cut->check_input( iv_weight_g = 0 iv_zone = {{p}}if_parcel_fee=>c_zone-home ) ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = {{p}}if_parcel_fee=>c_reason-zone
|
||||
act = lo_cut->check_input( iv_weight_g = 500 iv_zone = '9' ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD base_fee_sig.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 350
|
||||
act = lo_cut->base_fee( iv_zone = {{p}}if_parcel_fee=>c_zone-home ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 620
|
||||
act = lo_cut->base_fee( iv_zone = {{p}}if_parcel_fee=>c_zone-national ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 990
|
||||
act = lo_cut->base_fee( iv_zone = {{p}}if_parcel_fee=>c_zone-europe ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD weight_surcharge_sig.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 1000 ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 90
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 1001 ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 180
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 2001 ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD total_fee_sig.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 530
|
||||
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_false ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1280
|
||||
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_true ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0
|
||||
act = lo_cut->total_fee( iv_weight_g = 0 iv_zone = '1' iv_express = abap_false ) ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
55
tasks_gen/train/G1900/mutation.json
Normal file
55
tasks_gen/train/G1900/mutation.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"task": "G1900",
|
||||
"mutants": [
|
||||
{
|
||||
"object": "{{P}}IF_PARCEL_FEE",
|
||||
"mutant": "line 5: LENGTH 10 -> LENGTH 9 (length)",
|
||||
"status": "killed",
|
||||
"hidden": "8/9",
|
||||
"failed_tests": [
|
||||
"TYPE_LENGTHS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}IF_PARCEL_FEE",
|
||||
"mutant": "line 9: VALUE '1' -> VALUE 'Z' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "7/9",
|
||||
"failed_tests": [
|
||||
"TOTAL_FEE_SIG",
|
||||
"ZONE_VALUES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}IF_PARCEL_FEE",
|
||||
"mutant": "line 11: VALUE '3' -> VALUE 'Z' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "8/9",
|
||||
"failed_tests": [
|
||||
"ZONE_VALUES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}IF_PARCEL_FEE",
|
||||
"mutant": "line 16: VALUE 'OK' -> VALUE 'ZK' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "8/9",
|
||||
"failed_tests": [
|
||||
"REASON_VALUES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}IF_PARCEL_FEE",
|
||||
"mutant": "line 18: VALUE 'ZONE' -> VALUE 'YONE' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "8/9",
|
||||
"failed_tests": [
|
||||
"REASON_VALUES"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid": 5,
|
||||
"killed": 5,
|
||||
"kill_rate": 1.0,
|
||||
"ok": true
|
||||
}
|
||||
51
tasks_gen/train/G1900/reference/if_parcel_fee.intf.abap
Normal file
51
tasks_gen/train/G1900/reference/if_parcel_fee.intf.abap
Normal file
@@ -0,0 +1,51 @@
|
||||
INTERFACE {{p}}if_parcel_fee PUBLIC.
|
||||
TYPES ty_weight_g TYPE i.
|
||||
TYPES ty_cents TYPE i.
|
||||
TYPES ty_zone TYPE c LENGTH 1.
|
||||
TYPES ty_reason TYPE c LENGTH 10.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_zone,
|
||||
home TYPE ty_zone VALUE '1',
|
||||
national TYPE ty_zone VALUE '2',
|
||||
europe TYPE ty_zone VALUE '3',
|
||||
END OF c_zone.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_reason,
|
||||
ok TYPE ty_reason VALUE 'OK',
|
||||
weight TYPE ty_reason VALUE 'WEIGHT',
|
||||
zone TYPE ty_reason VALUE 'ZONE',
|
||||
END OF c_reason.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_limit,
|
||||
min_weight TYPE ty_weight_g VALUE 1,
|
||||
max_weight TYPE ty_weight_g VALUE 31500,
|
||||
END OF c_limit.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_surcharge,
|
||||
per_step TYPE ty_cents VALUE 90,
|
||||
express TYPE ty_cents VALUE 750,
|
||||
END OF c_surcharge.
|
||||
|
||||
METHODS check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason.
|
||||
|
||||
METHODS base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
|
||||
METHODS total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents.
|
||||
ENDINTERFACE.
|
||||
180
tasks_gen/train/G1900/reference/parcel_fee_test.clas.abap
Normal file
180
tasks_gen/train/G1900/reference/parcel_fee_test.clas.abap
Normal file
@@ -0,0 +1,180 @@
|
||||
CLASS {{p}}parcel_fee_test DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PUBLIC SECTION.
|
||||
INTERFACES {{p}}if_parcel_fee.
|
||||
PRIVATE SECTION.
|
||||
METHODS zone_values FOR TESTING.
|
||||
METHODS reason_values FOR TESTING.
|
||||
METHODS limit_values FOR TESTING.
|
||||
METHODS surcharge_values FOR TESTING.
|
||||
METHODS type_lengths FOR TESTING.
|
||||
METHODS valid_input FOR TESTING.
|
||||
METHODS weight_out_of_range FOR TESTING.
|
||||
METHODS unknown_zone FOR TESTING.
|
||||
METHODS base_fee_by_zone FOR TESTING.
|
||||
METHODS surcharge_steps FOR TESTING.
|
||||
METHODS total_fee_with_express FOR TESTING.
|
||||
METHODS total_fee_invalid FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}parcel_fee_test IMPLEMENTATION.
|
||||
METHOD {{p}}if_parcel_fee~check_input.
|
||||
IF iv_weight_g < {{p}}if_parcel_fee=>c_limit-min_weight
|
||||
OR iv_weight_g > {{p}}if_parcel_fee=>c_limit-max_weight.
|
||||
rv_reason = {{p}}if_parcel_fee=>c_reason-weight.
|
||||
ELSEIF iv_zone <> {{p}}if_parcel_fee=>c_zone-home
|
||||
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-national
|
||||
AND iv_zone <> {{p}}if_parcel_fee=>c_zone-europe.
|
||||
rv_reason = {{p}}if_parcel_fee=>c_reason-zone.
|
||||
ELSE.
|
||||
rv_reason = {{p}}if_parcel_fee=>c_reason-ok.
|
||||
ENDIF.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_parcel_fee~base_fee.
|
||||
CASE iv_zone.
|
||||
WHEN {{p}}if_parcel_fee=>c_zone-home.
|
||||
rv_cents = 350.
|
||||
WHEN {{p}}if_parcel_fee=>c_zone-national.
|
||||
rv_cents = 620.
|
||||
WHEN {{p}}if_parcel_fee=>c_zone-europe.
|
||||
rv_cents = 990.
|
||||
WHEN OTHERS.
|
||||
rv_cents = 0.
|
||||
ENDCASE.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_parcel_fee~weight_surcharge.
|
||||
IF iv_weight_g > 1000.
|
||||
rv_cents = ( iv_weight_g - 1000 + 999 ) DIV 1000
|
||||
* {{p}}if_parcel_fee=>c_surcharge-per_step.
|
||||
ENDIF.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_parcel_fee~total_fee.
|
||||
IF me->{{p}}if_parcel_fee~check_input( iv_weight_g = iv_weight_g
|
||||
iv_zone = iv_zone )
|
||||
<> {{p}}if_parcel_fee=>c_reason-ok.
|
||||
RETURN.
|
||||
ENDIF.
|
||||
rv_cents = me->{{p}}if_parcel_fee~base_fee( iv_zone )
|
||||
+ me->{{p}}if_parcel_fee~weight_surcharge( iv_weight_g ).
|
||||
IF iv_express = abap_true.
|
||||
rv_cents = rv_cents + {{p}}if_parcel_fee=>c_surcharge-express.
|
||||
ENDIF.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD zone_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = '1' act = {{p}}if_parcel_fee=>c_zone-home ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = '2' act = {{p}}if_parcel_fee=>c_zone-national ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = '3' act = {{p}}if_parcel_fee=>c_zone-europe ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD reason_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'OK' act = {{p}}if_parcel_fee=>c_reason-ok ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'WEIGHT' act = {{p}}if_parcel_fee=>c_reason-weight ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'ZONE' act = {{p}}if_parcel_fee=>c_reason-zone ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD limit_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = {{p}}if_parcel_fee=>c_limit-min_weight ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 31500 act = {{p}}if_parcel_fee=>c_limit-max_weight ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD surcharge_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 90 act = {{p}}if_parcel_fee=>c_surcharge-per_step ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 750 act = {{p}}if_parcel_fee=>c_surcharge-express ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD type_lengths.
|
||||
DATA lv_zone TYPE {{p}}if_parcel_fee=>ty_zone.
|
||||
DATA lv_reason TYPE {{p}}if_parcel_fee=>ty_reason.
|
||||
DATA lv_weight TYPE {{p}}if_parcel_fee=>ty_weight_g.
|
||||
DATA lv_cents TYPE {{p}}if_parcel_fee=>ty_cents.
|
||||
DATA(lv_char1) = 'A'.
|
||||
DATA(lv_char10) = 'ABCDEFGHIJ'.
|
||||
DATA(lo_zone) = cl_abap_typedescr=>describe_by_data( lv_zone ).
|
||||
DATA(lo_reason) = cl_abap_typedescr=>describe_by_data( lv_reason ).
|
||||
DATA(lo_weight) = cl_abap_typedescr=>describe_by_data( lv_weight ).
|
||||
DATA(lo_cents) = cl_abap_typedescr=>describe_by_data( lv_cents ).
|
||||
DATA(lo_char1) = cl_abap_typedescr=>describe_by_data( lv_char1 ).
|
||||
DATA(lo_char10) = cl_abap_typedescr=>describe_by_data( lv_char10 ).
|
||||
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
|
||||
act = lo_zone->type_kind ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lo_char1->length act = lo_zone->length ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_char
|
||||
act = lo_reason->type_kind ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = lo_char10->length act = lo_reason->length ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
|
||||
act = lo_weight->type_kind ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = cl_abap_typedescr=>typekind_int
|
||||
act = lo_cents->type_kind ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD valid_input.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-ok
|
||||
act = lo_cut->check_input( iv_weight_g = 1 iv_zone = '1' ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-ok
|
||||
act = lo_cut->check_input( iv_weight_g = 31500 iv_zone = '3' ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD weight_out_of_range.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-weight
|
||||
act = lo_cut->check_input( iv_weight_g = 0 iv_zone = '1' ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-weight
|
||||
act = lo_cut->check_input( iv_weight_g = 31501 iv_zone = '1' ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD unknown_zone.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_parcel_fee=>c_reason-zone
|
||||
act = lo_cut->check_input( iv_weight_g = 500 iv_zone = '9' ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD base_fee_by_zone.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 350 act = lo_cut->base_fee( iv_zone = '1' ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 620 act = lo_cut->base_fee( iv_zone = '2' ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 990 act = lo_cut->base_fee( iv_zone = '3' ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0 act = lo_cut->base_fee( iv_zone = '9' ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD surcharge_steps.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 1000 ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 90
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 1001 ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 90
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 2000 ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 180
|
||||
act = lo_cut->weight_surcharge( iv_weight_g = 2001 ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD total_fee_with_express.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 530
|
||||
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_false ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1280
|
||||
act = lo_cut->total_fee( iv_weight_g = 2500 iv_zone = '1' iv_express = abap_true ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD total_fee_invalid.
|
||||
DATA lo_cut TYPE REF TO {{p}}if_parcel_fee.
|
||||
lo_cut = me.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0
|
||||
act = lo_cut->total_fee( iv_weight_g = 0 iv_zone = '1' iv_express = abap_true ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0
|
||||
act = lo_cut->total_fee( iv_weight_g = 500 iv_zone = '9' iv_express = abap_false ) ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
106
tasks_gen/train/G1900/spec.md
Normal file
106
tasks_gen/train/G1900/spec.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 1. Goal
|
||||
A logistics program calculates the shipping fee of a parcel. The program must not
|
||||
contain the fee rules itself. It uses an interface as the contract. Create this
|
||||
interface.
|
||||
|
||||
# 2. Open questions
|
||||
None.
|
||||
|
||||
# 3. Context
|
||||
- Package $TMP, client 001.
|
||||
- The interface {{P}}IF_PARCEL_FEE does not exist yet. Create it.
|
||||
- A parcel has a weight in grams, a shipping zone, and an express flag. A fee is
|
||||
an amount in whole cents.
|
||||
- The interface is only a contract. Other programs implement it and calculate the
|
||||
fee. The interface declares four methods:
|
||||
- check_input: checks the weight and the zone and returns a reason code.
|
||||
- base_fee: returns the base fee of a zone.
|
||||
- weight_surcharge: returns the surcharge that the weight causes.
|
||||
- total_fee: returns the complete fee of a parcel.
|
||||
- The meaning of the values: zone '1' is the home zone, zone '2' is the national
|
||||
zone, zone '3' is the europe zone. The reason 'OK' means that the input is
|
||||
valid, 'WEIGHT' means that the weight is out of range, and 'ZONE' means that the
|
||||
zone is unknown. The base fee is 350 cents for zone '1', 620 cents for zone '2',
|
||||
and 990 cents for zone '3'. The first 1000 grams are included in the base fee.
|
||||
Each started 1000 grams above 1000 grams costs 90 cents. The express option
|
||||
costs 750 cents. A call with invalid input returns the fee 0.
|
||||
|
||||
# 4. Contract
|
||||
Create the interface {{P}}IF_PARCEL_FEE in package $TMP. The interface is public.
|
||||
It contains exactly the types, the constants, and the methods below. Do not add
|
||||
other components.
|
||||
|
||||
Types:
|
||||
- ty_weight_g TYPE i
|
||||
- ty_cents TYPE i
|
||||
- ty_zone TYPE c LENGTH 1
|
||||
- ty_reason TYPE c LENGTH 10
|
||||
|
||||
Constants:
|
||||
- c_zone: components home, national, europe, each TYPE ty_zone
|
||||
- c_reason: components ok, weight, zone, each TYPE ty_reason
|
||||
- c_limit: components min_weight, max_weight, each TYPE ty_weight_g
|
||||
- c_surcharge: components per_step, express, each TYPE ty_cents
|
||||
|
||||
Methods:
|
||||
- check_input
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_reason) TYPE ty_reason
|
||||
- base_fee
|
||||
IMPORTING iv_zone TYPE ty_zone
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents
|
||||
- weight_surcharge
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents
|
||||
- total_fee
|
||||
IMPORTING iv_weight_g TYPE ty_weight_g
|
||||
iv_zone TYPE ty_zone
|
||||
iv_express TYPE abap_bool
|
||||
RETURNING VALUE(rv_cents) TYPE ty_cents
|
||||
|
||||
# 5. Business rules
|
||||
1. The type ty_zone is a character type with length 1. The type ty_reason is a
|
||||
character type with length 10. The types ty_weight_g and ty_cents are integer
|
||||
types.
|
||||
2. The constant structure c_zone has the components home, national, and europe.
|
||||
Each component has the type ty_zone. The values are '1', '2', and '3'.
|
||||
3. The constant structure c_reason has the components ok, weight, and zone. Each
|
||||
component has the type ty_reason. The values are 'OK', 'WEIGHT', and 'ZONE'.
|
||||
4. The constant structure c_limit has the components min_weight and max_weight.
|
||||
Each component has the type ty_weight_g. The values are 1 and 31500.
|
||||
5. The constant structure c_surcharge has the components per_step and express.
|
||||
Each component has the type ty_cents. The values are 90 and 750.
|
||||
6. The method check_input has the importing parameter iv_weight_g of type
|
||||
ty_weight_g, the importing parameter iv_zone of type ty_zone, and the returning
|
||||
parameter rv_reason of type ty_reason. It returns c_reason-weight if the weight
|
||||
is below c_limit-min_weight or above c_limit-max_weight. Else it returns
|
||||
c_reason-zone if the zone is not one of the values of c_zone. Else it returns
|
||||
c_reason-ok.
|
||||
7. The method base_fee has the importing parameter iv_zone of type ty_zone and
|
||||
the returning parameter rv_cents of type ty_cents. It returns 350 for zone '1',
|
||||
620 for zone '2', 990 for zone '3', and 0 for every other zone.
|
||||
8. The method weight_surcharge has the importing parameter iv_weight_g of type
|
||||
ty_weight_g and the returning parameter rv_cents of type ty_cents. It returns 0
|
||||
for a weight up to 1000 grams. For every started 1000 grams above 1000 grams it
|
||||
returns c_surcharge-per_step cents.
|
||||
9. The method total_fee has the importing parameters iv_weight_g of type
|
||||
ty_weight_g, iv_zone of type ty_zone, and iv_express of type abap_bool, and the
|
||||
returning parameter rv_cents of type ty_cents. It returns 0 if check_input does
|
||||
not return c_reason-ok. Else it returns base_fee plus weight_surcharge, and it
|
||||
adds c_surcharge-express if iv_express is abap_true.
|
||||
|
||||
# 6. Constraints
|
||||
- Release target: SAP_BASIS 816 (SAP ABAP Platform 2025).
|
||||
- Coding standards: Clean ABAP. No global variables. No comment that restates the
|
||||
code.
|
||||
- Out of scope: no database table, no CDS view, no function module, no report, and
|
||||
no application class.
|
||||
|
||||
# 7. Acceptance
|
||||
- The interface {{P}}IF_PARCEL_FEE is active and has no syntax error.
|
||||
- The hidden tests pass.
|
||||
- Write your own ABAP Unit tests for the interface. An interface has no test
|
||||
include, so create one global test class in package $TMP. The test class
|
||||
implements the interface and checks the constants, the type lengths, and the
|
||||
method signatures. Keep all test code in this one global class.
|
||||
52
tasks_gen/train/G1900/task.json
Normal file
52
tasks_gen/train/G1900/task.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "G1900",
|
||||
"category": "A",
|
||||
"object_type": "INTF",
|
||||
"difficulty": 2,
|
||||
"release_target": "v816",
|
||||
"expected_outcome": "implement",
|
||||
"budget": {
|
||||
"max_tool_calls": 60,
|
||||
"max_activations": 15
|
||||
},
|
||||
"seed": [],
|
||||
"contract": [
|
||||
{
|
||||
"type": "INTF",
|
||||
"name": "{{P}}IF_PARCEL_FEE"
|
||||
}
|
||||
],
|
||||
"out_of_scope": [
|
||||
"No database table",
|
||||
"No CDS view",
|
||||
"No function module",
|
||||
"No report",
|
||||
"No application class"
|
||||
],
|
||||
"hidden_tests": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}PARCEL_FEE_HIDDEN",
|
||||
"file": "hidden/parcel_fee_hidden.clas.abap",
|
||||
"description": "Contract tests for {{P}}IF_PARCEL_FEE"
|
||||
}
|
||||
],
|
||||
"reference": [
|
||||
{
|
||||
"type": "INTF",
|
||||
"name": "{{P}}IF_PARCEL_FEE",
|
||||
"file": "reference/if_parcel_fee.intf.abap",
|
||||
"description": "Parcel fee contract"
|
||||
},
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}PARCEL_FEE_TEST",
|
||||
"file": "reference/parcel_fee_test.clas.abap",
|
||||
"description": "Own tests for the interface"
|
||||
}
|
||||
],
|
||||
"craft_checks": [
|
||||
"method_length",
|
||||
"no_global_variables"
|
||||
]
|
||||
}
|
||||
16
tasks_gen/train/G1901/faulty/m1_stock.tabl.asabap
Normal file
16
tasks_gen/train/G1901/faulty/m1_stock.tabl.asabap
Normal file
@@ -0,0 +1,16 @@
|
||||
@EndUserText.label : 'Bin stock'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}stock {
|
||||
key client : abap.clnt not null;
|
||||
key bin_id : abap.numc(8) not null;
|
||||
key article_id : abap.char(12) not null;
|
||||
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
|
||||
quantity : abap.dec(13,3);
|
||||
unit_of_measure : abap.unit(3);
|
||||
shelf_no : abap.numc(4);
|
||||
last_count_date : abap.dats;
|
||||
blocked : abap.char(1);
|
||||
}
|
||||
16
tasks_gen/train/G1901/faulty/m2_stock.tabl.asabap
Normal file
16
tasks_gen/train/G1901/faulty/m2_stock.tabl.asabap
Normal file
@@ -0,0 +1,16 @@
|
||||
@EndUserText.label : 'Bin stock'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}stock {
|
||||
key client : abap.clnt not null;
|
||||
key bin_id : abap.char(8) not null;
|
||||
key article_id : abap.char(12) not null;
|
||||
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
|
||||
quantity : abap.dec(13,4);
|
||||
unit_of_measure : abap.unit(3);
|
||||
shelf_no : abap.numc(4);
|
||||
last_count_date : abap.dats;
|
||||
blocked : abap.char(1);
|
||||
}
|
||||
16
tasks_gen/train/G1901/faulty/m3_stock.tabl.asabap
Normal file
16
tasks_gen/train/G1901/faulty/m3_stock.tabl.asabap
Normal file
@@ -0,0 +1,16 @@
|
||||
@EndUserText.label : 'Bin stock'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}stock {
|
||||
key client : abap.clnt not null;
|
||||
key bin_id : abap.char(8) not null;
|
||||
key article_id : abap.char(12) not null;
|
||||
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
|
||||
quantity : abap.dec(13,3);
|
||||
unit_of_measure : abap.unit(3);
|
||||
shelf_no : abap.numc(3);
|
||||
last_count_date : abap.dats;
|
||||
blocked : abap.char(1);
|
||||
}
|
||||
16
tasks_gen/train/G1901/faulty/m4_stock.tabl.asabap
Normal file
16
tasks_gen/train/G1901/faulty/m4_stock.tabl.asabap
Normal file
@@ -0,0 +1,16 @@
|
||||
@EndUserText.label : 'Bin stock'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}stock {
|
||||
key client : abap.clnt not null;
|
||||
key bin_id : abap.char(8) not null;
|
||||
key article_id : abap.char(12) not null;
|
||||
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
|
||||
quantity : abap.dec(13,3);
|
||||
unit_of_measure : abap.unit(3);
|
||||
shelf_no : abap.numc(4);
|
||||
last_count_date : abap.tims;
|
||||
blocked : abap.char(1);
|
||||
}
|
||||
19
tasks_gen/train/G1901/generation.json
Normal file
19
tasks_gen/train/G1901/generation.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "G1901",
|
||||
"pool": "train",
|
||||
"object_type": "TABL",
|
||||
"category": "B",
|
||||
"attempts": [
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 4,
|
||||
"killed": 4,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepted": true
|
||||
}
|
||||
182
tasks_gen/train/G1901/hidden/t20_hidden.clas.abap
Normal file
182
tasks_gen/train/G1901/hidden/t20_hidden.clas.abap
Normal file
@@ -0,0 +1,182 @@
|
||||
CLASS {{p}}t20_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
|
||||
CLASS-METHODS class_setup.
|
||||
CLASS-METHODS class_teardown.
|
||||
METHODS setup.
|
||||
METHODS fields_exist FOR TESTING.
|
||||
METHODS key_fields FOR TESTING.
|
||||
METHODS field_lengths FOR TESTING.
|
||||
METHODS field_types FOR TESTING.
|
||||
METHODS insert_and_read FOR TESTING.
|
||||
METHODS decimal_roundtrip FOR TESTING.
|
||||
METHODS duplicate_key FOR TESTING.
|
||||
METHODS other_key_allowed FOR TESTING.
|
||||
METHODS get_fields RETURNING VALUE(rt_fields) TYPE ddfields.
|
||||
METHODS field_of IMPORTING it_fields TYPE ddfields
|
||||
iv_name TYPE string
|
||||
RETURNING VALUE(rs_field) TYPE dfies.
|
||||
METHODS assert_has_field IMPORTING it_fields TYPE ddfields
|
||||
iv_name TYPE string.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}t20_hidden IMPLEMENTATION.
|
||||
|
||||
METHOD class_setup.
|
||||
go_env = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD class_teardown.
|
||||
go_env->destroy( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD setup.
|
||||
go_env->clear_doubles( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD get_fields.
|
||||
DATA(lo_descr) = CAST cl_abap_structdescr( cl_abap_typedescr=>describe_by_name( '{{P}}STOCK' ) ).
|
||||
rt_fields = lo_descr->get_ddic_field_list( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD field_of.
|
||||
LOOP AT it_fields INTO DATA(ls_field).
|
||||
IF to_upper( ls_field-fieldname ) = to_upper( iv_name ).
|
||||
rs_field = ls_field.
|
||||
RETURN.
|
||||
ENDIF.
|
||||
ENDLOOP.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD assert_has_field.
|
||||
cl_abap_unit_assert=>assert_true(
|
||||
act = boolc( field_of( it_fields = it_fields iv_name = iv_name )-fieldname IS NOT INITIAL )
|
||||
msg = |Field { iv_name } is missing| ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD fields_exist.
|
||||
DATA(lt_fields) = get_fields( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 8 act = lines( lt_fields ) ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'CLIENT' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'BIN_ID' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'ARTICLE_ID' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'QUANTITY' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'SHELF_NO' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' ).
|
||||
assert_has_field( it_fields = lt_fields iv_name = 'BLOCKED' ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD key_fields.
|
||||
DATA(lt_fields) = get_fields( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( it_fields = lt_fields iv_name = 'CLIENT' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( it_fields = lt_fields iv_name = 'BIN_ID' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'X' act = field_of( it_fields = lt_fields iv_name = 'ARTICLE_ID' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'SHELF_NO' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' )-keyflag ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ' ' act = field_of( it_fields = lt_fields iv_name = 'BLOCKED' )-keyflag ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD field_lengths.
|
||||
DATA(lt_fields) = get_fields( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 3 act = field_of( it_fields = lt_fields iv_name = 'CLIENT' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 8 act = field_of( it_fields = lt_fields iv_name = 'BIN_ID' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 12 act = field_of( it_fields = lt_fields iv_name = 'ARTICLE_ID' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 13 act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 3 act = field_of( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 4 act = field_of( it_fields = lt_fields iv_name = 'SHELF_NO' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 8 act = field_of( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' )-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = field_of( it_fields = lt_fields iv_name = 'BLOCKED' )-leng ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD field_types.
|
||||
DATA(lt_fields) = get_fields( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CLNT' act = field_of( it_fields = lt_fields iv_name = 'CLIENT' )-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( it_fields = lt_fields iv_name = 'BIN_ID' )-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( it_fields = lt_fields iv_name = 'ARTICLE_ID' )-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'P' act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-inttype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 3 act = field_of( it_fields = lt_fields iv_name = 'QUANTITY' )-decimals ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'UNIT' act = field_of( it_fields = lt_fields iv_name = 'UNIT_OF_MEASURE' )-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'NUMC' act = field_of( it_fields = lt_fields iv_name = 'SHELF_NO' )-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = field_of( it_fields = lt_fields iv_name = 'LAST_COUNT_DATE' )-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = field_of( it_fields = lt_fields iv_name = 'BLOCKED' )-datatype ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD insert_and_read.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA ls_read TYPE {{p}}stock.
|
||||
DATA lv_subrc TYPE sy-subrc.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0001'.
|
||||
ls_row-article_id = 'ART-000001'.
|
||||
ls_row-quantity = '12.500'.
|
||||
ls_row-unit_of_measure = 'ST'.
|
||||
ls_row-shelf_no = '0042'.
|
||||
ls_row-last_count_date = '20250115'.
|
||||
ls_row-blocked = 'X'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
lv_subrc = sy-subrc.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
|
||||
SELECT SINGLE * FROM {{p}}stock
|
||||
WHERE bin_id = 'BIN-0001' AND article_id = 'ART-000001'
|
||||
INTO @ls_read.
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-quantity act = ls_read-quantity ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-unit_of_measure act = ls_read-unit_of_measure ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-shelf_no act = ls_read-shelf_no ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-last_count_date act = ls_read-last_count_date ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-blocked act = ls_read-blocked ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-client act = ls_read-client ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD decimal_roundtrip.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA lv_quantity TYPE {{p}}stock-quantity.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0002'.
|
||||
ls_row-article_id = 'ART-000002'.
|
||||
ls_row-quantity = '0.125'.
|
||||
ls_row-unit_of_measure = 'KG'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
SELECT SINGLE quantity FROM {{p}}stock
|
||||
WHERE bin_id = 'BIN-0002' AND article_id = 'ART-000002'
|
||||
INTO @lv_quantity.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = CONV decfloat34( '0.125' )
|
||||
act = CONV decfloat34( lv_quantity ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD duplicate_key.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA lv_subrc TYPE sy-subrc.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0003'.
|
||||
ls_row-article_id = 'ART-000003'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
lv_subrc = sy-subrc.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
lv_subrc = sy-subrc.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 4 act = lv_subrc ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD other_key_allowed.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA lv_subrc TYPE sy-subrc.
|
||||
DATA lv_count TYPE i.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0004'.
|
||||
ls_row-article_id = 'ART-000004'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
ls_row-article_id = 'ART-000005'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
lv_subrc = sy-subrc.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
|
||||
SELECT COUNT(*) FROM {{p}}stock INTO @lv_count.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_count ).
|
||||
ENDMETHOD.
|
||||
|
||||
ENDCLASS.
|
||||
55
tasks_gen/train/G1901/mutation.json
Normal file
55
tasks_gen/train/G1901/mutation.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"task": "G1901",
|
||||
"mutants": [
|
||||
{
|
||||
"object": "{{P}}STOCK",
|
||||
"mutant": "line 8: key bin_id : -> bin_id : (key)",
|
||||
"status": "invalid",
|
||||
"hidden": "0/0",
|
||||
"failed_tests": []
|
||||
},
|
||||
{
|
||||
"object": "{{P}}STOCK",
|
||||
"mutant": "line 8: abap.char(8) -> abap.numc(8) (type)",
|
||||
"status": "killed",
|
||||
"hidden": "5/8",
|
||||
"failed_tests": [
|
||||
"DECIMAL_ROUNDTRIP",
|
||||
"FIELD_TYPES",
|
||||
"INSERT_AND_READ"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}STOCK",
|
||||
"mutant": "line 11: abap.dec(13,3) -> abap.dec(13,4) (decimals)",
|
||||
"status": "killed",
|
||||
"hidden": "7/8",
|
||||
"failed_tests": [
|
||||
"FIELD_TYPES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}STOCK",
|
||||
"mutant": "line 13: abap.numc(4) -> abap.numc(3) (length)",
|
||||
"status": "killed",
|
||||
"hidden": "7/8",
|
||||
"failed_tests": [
|
||||
"FIELD_LENGTHS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}STOCK",
|
||||
"mutant": "line 14: abap.dats -> abap.tims (type)",
|
||||
"status": "killed",
|
||||
"hidden": "6/8",
|
||||
"failed_tests": [
|
||||
"FIELD_LENGTHS",
|
||||
"FIELD_TYPES"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid": 4,
|
||||
"killed": 4,
|
||||
"kill_rate": 1.0,
|
||||
"ok": true
|
||||
}
|
||||
16
tasks_gen/train/G1901/reference/stock.tabl.asabap
Normal file
16
tasks_gen/train/G1901/reference/stock.tabl.asabap
Normal file
@@ -0,0 +1,16 @@
|
||||
@EndUserText.label : 'Bin stock'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}stock {
|
||||
key client : abap.clnt not null;
|
||||
key bin_id : abap.char(8) not null;
|
||||
key article_id : abap.char(12) not null;
|
||||
@Semantics.quantity.unitOfMeasure : '{{p}}stock.unit_of_measure'
|
||||
quantity : abap.dec(13,3);
|
||||
unit_of_measure : abap.unit(3);
|
||||
shelf_no : abap.numc(4);
|
||||
last_count_date : abap.dats;
|
||||
blocked : abap.char(1);
|
||||
}
|
||||
80
tasks_gen/train/G1901/reference/t20_test.clas.abap
Normal file
80
tasks_gen/train/G1901/reference/t20_test.clas.abap
Normal file
@@ -0,0 +1,80 @@
|
||||
CLASS {{p}}t20_test DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
|
||||
CLASS-METHODS class_setup.
|
||||
CLASS-METHODS class_teardown.
|
||||
METHODS setup.
|
||||
METHODS stores_row FOR TESTING.
|
||||
METHODS rejects_duplicate FOR TESTING.
|
||||
METHODS keeps_three_decimals FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}t20_test IMPLEMENTATION.
|
||||
|
||||
METHOD class_setup.
|
||||
go_env = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD class_teardown.
|
||||
go_env->destroy( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD setup.
|
||||
go_env->clear_doubles( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD stores_row.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA ls_read TYPE {{p}}stock.
|
||||
DATA lv_subrc TYPE sy-subrc.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0001'.
|
||||
ls_row-article_id = 'ART-000001'.
|
||||
ls_row-quantity = '12.500'.
|
||||
ls_row-unit_of_measure = 'ST'.
|
||||
ls_row-shelf_no = '0042'.
|
||||
ls_row-last_count_date = '20250115'.
|
||||
ls_row-blocked = 'X'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
lv_subrc = sy-subrc.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_subrc ).
|
||||
SELECT SINGLE * FROM {{p}}stock
|
||||
WHERE bin_id = 'BIN-0001' AND article_id = 'ART-000001'
|
||||
INTO @ls_read.
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-quantity act = ls_read-quantity ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-unit_of_measure act = ls_read-unit_of_measure ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_row-blocked act = ls_read-blocked ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD rejects_duplicate.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA lv_subrc TYPE sy-subrc.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0002'.
|
||||
ls_row-article_id = 'ART-000002'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
lv_subrc = sy-subrc.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 4 act = lv_subrc ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD keeps_three_decimals.
|
||||
DATA ls_row TYPE {{p}}stock.
|
||||
DATA lv_quantity TYPE {{p}}stock-quantity.
|
||||
ls_row-client = sy-mandt.
|
||||
ls_row-bin_id = 'BIN-0003'.
|
||||
ls_row-article_id = 'ART-000003'.
|
||||
ls_row-quantity = '0.125'.
|
||||
ls_row-unit_of_measure = 'KG'.
|
||||
INSERT {{p}}stock FROM @ls_row.
|
||||
SELECT SINGLE quantity FROM {{p}}stock
|
||||
WHERE bin_id = 'BIN-0003' AND article_id = 'ART-000003'
|
||||
INTO @lv_quantity.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = CONV decfloat34( '0.125' )
|
||||
act = CONV decfloat34( lv_quantity ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
ENDCLASS.
|
||||
36
tasks_gen/train/G1901/spec.md
Normal file
36
tasks_gen/train/G1901/spec.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 1. Goal
|
||||
The warehouse team counts the stock of each article in each storage bin. The team needs one table that stores the result of these counts. Other programs read the table, so the structure of the table is fixed.
|
||||
|
||||
# 2. Open questions
|
||||
None.
|
||||
|
||||
# 3. Context
|
||||
- System: SAP ABAP Platform 2025 (SAP_BASIS 816), client 001.
|
||||
- Package: $TMP.
|
||||
- No table for this data exists. Do not use tables of SAP application components.
|
||||
|
||||
# 4. Contract
|
||||
- Create the transparent database table {{P}}STOCK in package $TMP.
|
||||
- The table contains these fields: CLIENT, BIN_ID, ARTICLE_ID, QUANTITY, UNIT_OF_MEASURE, SHELF_NO, LAST_COUNT_DATE, BLOCKED.
|
||||
- Key fields: CLIENT, BIN_ID, ARTICLE_ID.
|
||||
|
||||
# 5. Business rules
|
||||
1. One row of the table is the stock of one article in one storage bin of one client. The key fields CLIENT, BIN_ID and ARTICLE_ID identify the row. A second row with the same key values is not allowed.
|
||||
2. CLIENT: data type CLNT, length 3. Key field. Not null.
|
||||
3. BIN_ID: data type CHAR, length 8. Key field. Not null. The number of the storage bin.
|
||||
4. ARTICLE_ID: data type CHAR, length 12. Key field. Not null. The number of the article.
|
||||
5. QUANTITY: data type DEC, length 13, 3 decimals. The stock quantity. It is a quantity in the unit of measure that UNIT_OF_MEASURE contains.
|
||||
6. UNIT_OF_MEASURE: data type UNIT, length 3. The unit of measure of QUANTITY.
|
||||
7. SHELF_NO: data type NUMC, length 4. The shelf number in the storage bin.
|
||||
8. LAST_COUNT_DATE: data type DATS. The date of the last count.
|
||||
9. BLOCKED: data type CHAR, length 1. The value 'X' means that the storage bin is blocked for withdrawals. The value is initial if the storage bin is not blocked.
|
||||
|
||||
# 6. Constraints
|
||||
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
|
||||
- Coding standards: Clean ABAP. Use ABAP SQL and ABAP Unit.
|
||||
- Out of scope: do not create a CDS view, a report or a function module. Do not change other objects.
|
||||
|
||||
# 7. Acceptance
|
||||
- The table {{P}}STOCK is active and has no syntax error.
|
||||
- The hidden tests pass.
|
||||
- Write ABAP Unit tests with CL_OSQL_TEST_ENVIRONMENT in a global test class.
|
||||
53
tasks_gen/train/G1901/task.json
Normal file
53
tasks_gen/train/G1901/task.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "G1901",
|
||||
"category": "B",
|
||||
"object_type": "TABL",
|
||||
"difficulty": 2,
|
||||
"release_target": "v816",
|
||||
"expected_outcome": "implement",
|
||||
"budget": {
|
||||
"max_tool_calls": 60,
|
||||
"max_activations": 15
|
||||
},
|
||||
"seed": [],
|
||||
"contract": [
|
||||
{
|
||||
"type": "TABL",
|
||||
"name": "{{P}}STOCK",
|
||||
"fields": [
|
||||
"client",
|
||||
"bin_id",
|
||||
"article_id",
|
||||
"quantity",
|
||||
"unit_of_measure",
|
||||
"shelf_no",
|
||||
"last_count_date",
|
||||
"blocked"
|
||||
]
|
||||
}
|
||||
],
|
||||
"out_of_scope": [],
|
||||
"hidden_tests": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}T20_HIDDEN",
|
||||
"file": "hidden/t20_hidden.clas.abap",
|
||||
"description": "T20 hidden tests"
|
||||
}
|
||||
],
|
||||
"reference": [
|
||||
{
|
||||
"type": "TABL",
|
||||
"name": "{{P}}STOCK",
|
||||
"file": "reference/stock.tabl.asabap",
|
||||
"description": "Bin stock table"
|
||||
},
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}T20_TEST",
|
||||
"file": "reference/t20_test.clas.abap",
|
||||
"description": "T20 own tests"
|
||||
}
|
||||
],
|
||||
"craft_checks": []
|
||||
}
|
||||
24
tasks_gen/train/G1902/generation.json
Normal file
24
tasks_gen/train/G1902/generation.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "G1902",
|
||||
"pool": "train",
|
||||
"object_type": "MSAG",
|
||||
"category": "D",
|
||||
"attempts": [
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 85.0,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 5,
|
||||
"killed": 5,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepted": true
|
||||
}
|
||||
97
tasks_gen/train/G1902/hidden/t41_hidden.clas.abap
Normal file
97
tasks_gen/train/G1902/hidden/t41_hidden.clas.abap
Normal file
@@ -0,0 +1,97 @@
|
||||
CLASS {{p}}t41_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
METHODS msg_001_text FOR TESTING.
|
||||
METHODS msg_002_text FOR TESTING.
|
||||
METHODS msg_003_text FOR TESTING.
|
||||
METHODS msg_004_text FOR TESTING.
|
||||
METHODS msg_005_text FOR TESTING.
|
||||
METHODS placeholder_substitution FOR TESTING.
|
||||
METHODS exception_text_two_args FOR TESTING.
|
||||
METHODS exception_text_one_arg FOR TESTING.
|
||||
METHODS exception_is_catchable FOR TESTING.
|
||||
METHODS exception_is_static_check FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}t41_hidden IMPLEMENTATION.
|
||||
METHOD msg_001_text.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP101' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP101 is fully booked' act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_002_text.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '002' WITH 'P-100' 'ABAP101' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Participant P-100 is already booked for course ABAP101'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_003_text.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '003' WITH 'ABAP101' '3' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Course ABAP101 has only 3 free seats left'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_004_text.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '004' WITH 'ABAP101' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Booking for course ABAP101 is confirmed'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_005_text.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '005' WITH 'ABAP101' '20250301' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Course ABAP101 was cancelled on 20250301'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD placeholder_substitution.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP202' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP202 is fully booked' act = lv_text ).
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP303' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP303 is fully booked' act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_text_two_args.
|
||||
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '002' iv_arg1 = 'P-100' iv_arg2 = 'ABAP101' ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Participant P-100 is already booked for course ABAP101'
|
||||
act = lo_cx->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_text_one_arg.
|
||||
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '004' iv_arg1 = 'ABAP101' ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Booking for course ABAP101 is confirmed'
|
||||
act = lo_cx->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_is_catchable.
|
||||
DATA lv_caught TYPE abap_bool.
|
||||
TRY.
|
||||
RAISE EXCEPTION TYPE {{p}}cx_booking
|
||||
EXPORTING iv_msgno = '003' iv_arg1 = 'ABAP101' iv_arg2 = '1'.
|
||||
CATCH {{p}}cx_booking INTO DATA(lo_cx).
|
||||
lv_caught = abap_true.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Course ABAP101 has only 1 free seats left'
|
||||
act = lo_cx->get_text( ) ).
|
||||
ENDTRY.
|
||||
cl_abap_unit_assert=>assert_true( lv_caught ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_is_static_check.
|
||||
DATA lo_cx TYPE REF TO cx_static_check.
|
||||
lo_cx = NEW {{p}}cx_booking( iv_msgno = '001' iv_arg1 = 'ABAP101' ).
|
||||
cl_abap_unit_assert=>assert_bound( lo_cx ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
58
tasks_gen/train/G1902/mutation.json
Normal file
58
tasks_gen/train/G1902/mutation.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"task": "G1902",
|
||||
"mutants": [
|
||||
{
|
||||
"object": "{{P}}MSG",
|
||||
"mutant": "message 005: &1 -> &2 (placeholder)",
|
||||
"status": "killed",
|
||||
"hidden": "9/10",
|
||||
"failed_tests": [
|
||||
"MSG_005_TEXT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}MSG",
|
||||
"mutant": "message 003: &1 -> &2 (placeholder)",
|
||||
"status": "killed",
|
||||
"hidden": "8/10",
|
||||
"failed_tests": [
|
||||
"EXCEPTION_IS_CATCHABLE",
|
||||
"MSG_003_TEXT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}MSG",
|
||||
"mutant": "message 004: &1 -> &2 (placeholder)",
|
||||
"status": "killed",
|
||||
"hidden": "8/10",
|
||||
"failed_tests": [
|
||||
"EXCEPTION_TEXT_ONE_ARG",
|
||||
"MSG_004_TEXT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}MSG",
|
||||
"mutant": "message 002: text + ' x' (text)",
|
||||
"status": "killed",
|
||||
"hidden": "8/10",
|
||||
"failed_tests": [
|
||||
"EXCEPTION_TEXT_TWO_ARGS",
|
||||
"MSG_002_TEXT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}MSG",
|
||||
"mutant": "message 001: text + ' x' (text)",
|
||||
"status": "killed",
|
||||
"hidden": "8/10",
|
||||
"failed_tests": [
|
||||
"MSG_001_TEXT",
|
||||
"PLACEHOLDER_SUBSTITUTION"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid": 5,
|
||||
"killed": 5,
|
||||
"kill_rate": 1.0,
|
||||
"ok": true
|
||||
}
|
||||
26
tasks_gen/train/G1902/reference/cx_booking.clas.abap
Normal file
26
tasks_gen/train/G1902/reference/cx_booking.clas.abap
Normal file
@@ -0,0 +1,26 @@
|
||||
CLASS {{p}}cx_booking DEFINITION PUBLIC INHERITING FROM cx_static_check FINAL CREATE PUBLIC.
|
||||
PUBLIC SECTION.
|
||||
METHODS constructor
|
||||
IMPORTING iv_msgno TYPE symsgno
|
||||
iv_arg1 TYPE string DEFAULT ''
|
||||
iv_arg2 TYPE string DEFAULT ''.
|
||||
METHODS if_message~get_text REDEFINITION.
|
||||
PRIVATE SECTION.
|
||||
DATA mv_msgno TYPE symsgno.
|
||||
DATA mv_arg1 TYPE string.
|
||||
DATA mv_arg2 TYPE string.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}cx_booking IMPLEMENTATION.
|
||||
METHOD constructor.
|
||||
super->constructor( ).
|
||||
mv_msgno = iv_msgno.
|
||||
mv_arg1 = iv_arg1.
|
||||
mv_arg2 = iv_arg2.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD if_message~get_text.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER mv_msgno WITH mv_arg1 mv_arg2 INTO result.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
73
tasks_gen/train/G1902/reference/t41_test.clas.abap
Normal file
73
tasks_gen/train/G1902/reference/t41_test.clas.abap
Normal file
@@ -0,0 +1,73 @@
|
||||
CLASS {{p}}t41_test DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
METHODS msg_001 FOR TESTING.
|
||||
METHODS msg_002 FOR TESTING.
|
||||
METHODS msg_003 FOR TESTING.
|
||||
METHODS msg_004 FOR TESTING.
|
||||
METHODS msg_005 FOR TESTING.
|
||||
METHODS exception_two_args FOR TESTING.
|
||||
METHODS exception_one_arg FOR TESTING.
|
||||
METHODS exception_is_static_check FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}t41_test IMPLEMENTATION.
|
||||
METHOD msg_001.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '001' WITH 'ABAP101' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'Course ABAP101 is fully booked' act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_002.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '002' WITH 'P-100' 'ABAP101' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Participant P-100 is already booked for course ABAP101'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_003.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '003' WITH 'ABAP101' '3' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Course ABAP101 has only 3 free seats left'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_004.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '004' WITH 'ABAP101' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Booking for course ABAP101 is confirmed'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD msg_005.
|
||||
DATA lv_text TYPE string.
|
||||
MESSAGE ID '{{P}}MSG' TYPE 'E' NUMBER '005' WITH 'ABAP101' '20250301' INTO lv_text.
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Course ABAP101 was cancelled on 20250301'
|
||||
act = lv_text ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_two_args.
|
||||
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '002' iv_arg1 = 'P-100' iv_arg2 = 'ABAP101' ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Participant P-100 is already booked for course ABAP101'
|
||||
act = lo_cx->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_one_arg.
|
||||
DATA(lo_cx) = NEW {{p}}cx_booking( iv_msgno = '004' iv_arg1 = 'ABAP101' ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = 'Booking for course ABAP101 is confirmed'
|
||||
act = lo_cx->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD exception_is_static_check.
|
||||
DATA lo_cx TYPE REF TO cx_static_check.
|
||||
lo_cx = NEW {{p}}cx_booking( iv_msgno = '001' iv_arg1 = 'ABAP101' ).
|
||||
cl_abap_unit_assert=>assert_bound( lo_cx ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
19
tasks_gen/train/G1902/seed/cx_legacy.clas.abap
Normal file
19
tasks_gen/train/G1902/seed/cx_legacy.clas.abap
Normal file
@@ -0,0 +1,19 @@
|
||||
CLASS {{p}}cx_legacy DEFINITION PUBLIC INHERITING FROM cx_static_check FINAL CREATE PUBLIC.
|
||||
PUBLIC SECTION.
|
||||
METHODS constructor IMPORTING iv_text TYPE string.
|
||||
METHODS if_message~get_text REDEFINITION.
|
||||
PRIVATE SECTION.
|
||||
DATA mv_text TYPE string.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}cx_legacy IMPLEMENTATION.
|
||||
METHOD constructor.
|
||||
super->constructor( ).
|
||||
mv_text = iv_text.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD if_message~get_text.
|
||||
result = mv_text.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
69
tasks_gen/train/G1902/spec.md
Normal file
69
tasks_gen/train/G1902/spec.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 1. Goal
|
||||
|
||||
Create the message class and the class-based exception for the training course
|
||||
booking service. The booking service reports every problem with a message from
|
||||
the message class. The exception class carries the text of such a message.
|
||||
|
||||
# 2. Open questions
|
||||
|
||||
None.
|
||||
|
||||
# 3. Context
|
||||
|
||||
- All objects are in package $TMP.
|
||||
- The booking service books participants into training courses.
|
||||
- The old exception class {{P}}CX_LEGACY exists. It takes a hard-coded text and
|
||||
does not use a message class. The new exception class replaces it.
|
||||
- All texts of the booking service come from one message class.
|
||||
|
||||
# 4. Contract
|
||||
|
||||
- Create the message class {{P}}MSG with the messages of section 5.
|
||||
- Create the exception class {{P}}CX_BOOKING:
|
||||
- It is a class-based exception and inherits from CX_STATIC_CHECK.
|
||||
- The constructor has these parameters:
|
||||
- iv_msgno TYPE symsgno
|
||||
- iv_arg1 TYPE string, default ''
|
||||
- iv_arg2 TYPE string, default ''
|
||||
- The method GET_TEXT of the interface IF_MESSAGE returns the text of the
|
||||
message with the number iv_msgno from {{P}}MSG. The placeholder &1 is
|
||||
replaced by iv_arg1 and the placeholder &2 is replaced by iv_arg2.
|
||||
|
||||
# 5. Business rules
|
||||
|
||||
Message class {{P}}MSG:
|
||||
|
||||
1. Message 001, error message, text "Course &1 is fully booked". The booking
|
||||
service uses it when a course has no free seat.
|
||||
2. Message 002, error message, text "Participant &1 is already booked for
|
||||
course &2". The booking service uses it for a double booking.
|
||||
3. Message 003, warning message, text "Course &1 has only &2 free seats left".
|
||||
The booking service uses it when a course has few free seats left.
|
||||
4. Message 004, information message, text "Booking for course &1 is
|
||||
confirmed". The booking service uses it for a successful booking.
|
||||
5. Message 005, warning message, text "Course &1 was cancelled on &2". The
|
||||
booking service uses it for a cancelled course.
|
||||
|
||||
Exception class {{P}}CX_BOOKING:
|
||||
|
||||
6. The constructor stores the message number and the two placeholders.
|
||||
7. GET_TEXT returns the text of the message with the number iv_msgno from
|
||||
{{P}}MSG. The placeholder &1 is replaced by iv_arg1 and the placeholder &2 is
|
||||
replaced by iv_arg2.
|
||||
|
||||
# 6. Constraints
|
||||
|
||||
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
|
||||
- Coding standards: Clean ABAP. Keep every method below 40 statements. Do not
|
||||
produce ATC findings of priority 1 or 2.
|
||||
- Out of scope: database tables, CDS views, reports, dynpro, and the old
|
||||
exception class {{P}}CX_LEGACY.
|
||||
|
||||
# 7. Acceptance
|
||||
|
||||
- The message class {{P}}MSG is active. It contains the messages 001 to 005 with
|
||||
the exact texts of section 5.
|
||||
- The exception class {{P}}CX_BOOKING is active and has no syntax error.
|
||||
- The hidden tests pass.
|
||||
- Write ABAP Unit tests for the messages and the exception class in a global
|
||||
test class.
|
||||
103
tasks_gen/train/G1902/task.json
Normal file
103
tasks_gen/train/G1902/task.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"id": "G1902",
|
||||
"category": "D",
|
||||
"object_type": "MSAG",
|
||||
"difficulty": 2,
|
||||
"release_target": "v816",
|
||||
"expected_outcome": "implement",
|
||||
"budget": {
|
||||
"max_tool_calls": 60,
|
||||
"max_activations": 15
|
||||
},
|
||||
"seed": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}CX_LEGACY",
|
||||
"file": "seed/cx_legacy.clas.abap",
|
||||
"description": "Old exception class with a hard-coded text"
|
||||
}
|
||||
],
|
||||
"contract": [
|
||||
{
|
||||
"type": "MSAG",
|
||||
"name": "{{P}}MSG",
|
||||
"messages": [
|
||||
{
|
||||
"msgno": "001"
|
||||
},
|
||||
{
|
||||
"msgno": "002"
|
||||
},
|
||||
{
|
||||
"msgno": "003"
|
||||
},
|
||||
{
|
||||
"msgno": "004"
|
||||
},
|
||||
{
|
||||
"msgno": "005"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}CX_BOOKING",
|
||||
"description": "Class-based exception (CX_STATIC_CHECK). Constructor IMPORTING iv_msgno TYPE symsgno, iv_arg1 TYPE string DEFAULT '', iv_arg2 TYPE string DEFAULT ''. GET_TEXT of IF_MESSAGE returns the text of message iv_msgno from {{P}}MSG with &1 = iv_arg1 and &2 = iv_arg2."
|
||||
}
|
||||
],
|
||||
"out_of_scope": [
|
||||
"{{P}}CX_LEGACY"
|
||||
],
|
||||
"hidden_tests": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}T41_HIDDEN",
|
||||
"file": "hidden/t41_hidden.clas.abap",
|
||||
"description": "T41 hidden tests"
|
||||
}
|
||||
],
|
||||
"reference": [
|
||||
{
|
||||
"type": "MSAG",
|
||||
"name": "{{P}}MSG",
|
||||
"description": "Booking messages",
|
||||
"messages": [
|
||||
{
|
||||
"msgno": "001",
|
||||
"text": "Course &1 is fully booked"
|
||||
},
|
||||
{
|
||||
"msgno": "002",
|
||||
"text": "Participant &1 is already booked for course &2"
|
||||
},
|
||||
{
|
||||
"msgno": "003",
|
||||
"text": "Course &1 has only &2 free seats left"
|
||||
},
|
||||
{
|
||||
"msgno": "004",
|
||||
"text": "Booking for course &1 is confirmed"
|
||||
},
|
||||
{
|
||||
"msgno": "005",
|
||||
"text": "Course &1 was cancelled on &2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}CX_BOOKING",
|
||||
"file": "reference/cx_booking.clas.abap",
|
||||
"description": "Booking exception class"
|
||||
},
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}T41_TEST",
|
||||
"file": "reference/t41_test.clas.abap",
|
||||
"description": "T41 own tests"
|
||||
}
|
||||
],
|
||||
"craft_checks": [
|
||||
"method_length"
|
||||
]
|
||||
}
|
||||
6
tasks_gen/train/G1903/faulty/m0_audit_info.ddls.asddls
Normal file
6
tasks_gen/train/G1903/faulty/m0_audit_info.ddls.asddls
Normal file
@@ -0,0 +1,6 @@
|
||||
@EndUserText.label : 'Audit information'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
define structure {{p}}audit_info {
|
||||
created_by : abap.char(11);
|
||||
created_on : abap.dats;
|
||||
}
|
||||
6
tasks_gen/train/G1903/faulty/m2_audit_info.ddls.asddls
Normal file
6
tasks_gen/train/G1903/faulty/m2_audit_info.ddls.asddls
Normal file
@@ -0,0 +1,6 @@
|
||||
@EndUserText.label : 'Audit information'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
define structure {{p}}audit_info {
|
||||
created_by : abap.numc(12);
|
||||
created_on : abap.dats;
|
||||
}
|
||||
10
tasks_gen/train/G1903/faulty/m3_stock_line.ddls.asddls
Normal file
10
tasks_gen/train/G1903/faulty/m3_stock_line.ddls.asddls
Normal file
@@ -0,0 +1,10 @@
|
||||
@EndUserText.label : 'Stock position line'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
define structure {{p}}stock_line {
|
||||
material : abap.char(18);
|
||||
plant : abap.char(4);
|
||||
net_amount : abap.dec(9,3);
|
||||
currency : abap.char(5);
|
||||
created_by : abap.char(12);
|
||||
created_on : abap.dats;
|
||||
}
|
||||
6
tasks_gen/train/G1903/faulty/m4_audit_info.ddls.asddls
Normal file
6
tasks_gen/train/G1903/faulty/m4_audit_info.ddls.asddls
Normal file
@@ -0,0 +1,6 @@
|
||||
@EndUserText.label : 'Audit information'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
define structure {{p}}audit_info {
|
||||
created_by : abap.char(12);
|
||||
created_on : abap.tims;
|
||||
}
|
||||
29
tasks_gen/train/G1903/generation.json
Normal file
29
tasks_gen/train/G1903/generation.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "G1903",
|
||||
"pool": "train",
|
||||
"object_type": "STRU",
|
||||
"category": "B",
|
||||
"attempts": [
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 0,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 0,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 4,
|
||||
"killed": 4,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepted": true
|
||||
}
|
||||
177
tasks_gen/train/G1903/hidden/t16_hidden.clas.abap
Normal file
177
tasks_gen/train/G1903/hidden/t16_hidden.clas.abap
Normal file
@@ -0,0 +1,177 @@
|
||||
CLASS {{p}}t16_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
|
||||
PRIVATE SECTION.
|
||||
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
|
||||
|
||||
CLASS-METHODS class_setup.
|
||||
CLASS-METHODS class_teardown.
|
||||
METHODS setup.
|
||||
|
||||
METHODS six_components FOR TESTING.
|
||||
METHODS component_order FOR TESTING.
|
||||
METHODS char_lengths FOR TESTING.
|
||||
METHODS amount_type FOR TESTING.
|
||||
METHODS date_type FOR TESTING.
|
||||
METHODS flat_structure FOR TESTING.
|
||||
METHODS audit_structure FOR TESTING.
|
||||
METHODS audit_matches FOR TESTING.
|
||||
METHODS read_stock_row FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}t16_hidden IMPLEMENTATION.
|
||||
|
||||
METHOD class_setup.
|
||||
go_env = cl_osql_test_environment=>create(
|
||||
i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD class_teardown.
|
||||
go_env->destroy( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD setup.
|
||||
go_env->clear_doubles( ).
|
||||
DATA lt_stock TYPE STANDARD TABLE OF {{p}}stock WITH EMPTY KEY.
|
||||
lt_stock = VALUE #(
|
||||
( material = 'MAT-1' plant = 'P001' net_amount = '123.45'
|
||||
currency = 'EUR' created_by = 'USER1' created_on = '20240115' )
|
||||
( material = 'MAT-2' plant = 'P001' net_amount = '10.00'
|
||||
currency = 'EUR' created_by = 'USER2' created_on = '20240116' )
|
||||
( material = 'MAT-3' plant = 'P002' net_amount = '5.50'
|
||||
currency = 'USD' created_by = 'USER1' created_on = '20240117' ) ).
|
||||
go_env->insert_test_data( lt_stock ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD six_components.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 6 act = lines( lt_fields ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD component_order.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_comp) = lo_struct->get_components( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 6 act = lines( lt_comp ) ).
|
||||
DATA(lv_index) = 0.
|
||||
LOOP AT lt_comp INTO DATA(ls_comp).
|
||||
lv_index = lv_index + 1.
|
||||
CASE lv_index.
|
||||
WHEN 1.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'MATERIAL' act = ls_comp-name ).
|
||||
WHEN 2.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'PLANT' act = ls_comp-name ).
|
||||
WHEN 3.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'NET_AMOUNT' act = ls_comp-name ).
|
||||
WHEN 4.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CURRENCY' act = ls_comp-name ).
|
||||
WHEN 5.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CREATED_BY' act = ls_comp-name ).
|
||||
WHEN 6.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CREATED_ON' act = ls_comp-name ).
|
||||
ENDCASE.
|
||||
ENDLOOP.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD char_lengths.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
|
||||
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'MATERIAL'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component MATERIAL is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 18 act = CONV i( ls_field-leng ) ).
|
||||
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'PLANT'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component PLANT is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 4 act = CONV i( ls_field-leng ) ).
|
||||
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'CURRENCY'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CURRENCY is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 5 act = CONV i( ls_field-leng ) ).
|
||||
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'CREATED_BY'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_BY is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 12 act = CONV i( ls_field-leng ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD amount_type.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
|
||||
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'NET_AMOUNT'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component NET_AMOUNT is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'DEC' act = ls_field-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 9 act = CONV i( ls_field-leng ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 2 act = CONV i( ls_field-decimals ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD date_type.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
|
||||
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'CREATED_ON'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_ON is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = ls_field-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 8 act = CONV i( ls_field-leng ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD flat_structure.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_comp) = lo_struct->get_components( ).
|
||||
LOOP AT lt_comp INTO DATA(ls_comp).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = cl_abap_typedescr=>kind_elem
|
||||
act = ls_comp-type->kind
|
||||
msg = |Component { ls_comp-name } is not elementary| ).
|
||||
ENDLOOP.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD audit_structure.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}AUDIT_INFO' ) ).
|
||||
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_fields ) ).
|
||||
READ TABLE lt_fields INTO DATA(ls_field) WITH KEY fieldname = 'CREATED_BY'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_BY is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'CHAR' act = ls_field-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 12 act = CONV i( ls_field-leng ) ).
|
||||
READ TABLE lt_fields INTO ls_field WITH KEY fieldname = 'CREATED_ON'.
|
||||
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = 'Component CREATED_ON is missing' ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'DATS' act = ls_field-datatype ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 8 act = CONV i( ls_field-leng ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD audit_matches.
|
||||
DATA(lo_line) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lo_audit) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}AUDIT_INFO' ) ).
|
||||
DATA(lt_line) = lo_line->get_ddic_field_list( ).
|
||||
DATA(lt_audit) = lo_audit->get_ddic_field_list( ).
|
||||
LOOP AT lt_audit INTO DATA(ls_audit).
|
||||
READ TABLE lt_line INTO DATA(ls_line) WITH KEY fieldname = ls_audit-fieldname.
|
||||
cl_abap_unit_assert=>assert_subrc(
|
||||
exp = 0 msg = |Component { ls_audit-fieldname } is missing| ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_audit-leng act = ls_line-leng ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_audit-datatype act = ls_line-datatype ).
|
||||
ENDLOOP.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD read_stock_row.
|
||||
DATA ls_line TYPE {{p}}stock_line.
|
||||
SELECT SINGLE material, plant, net_amount, currency, created_by, created_on
|
||||
FROM {{p}}stock
|
||||
INTO @ls_line
|
||||
WHERE material = 'MAT-1' AND plant = 'P001'.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'MAT-1' act = ls_line-material ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'P001' act = ls_line-plant ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'EUR' act = ls_line-currency ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'USER1' act = ls_line-created_by ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = CONV d( '20240115' ) act = ls_line-created_on ).
|
||||
cl_abap_unit_assert=>assert_equals(
|
||||
exp = CONV decfloat34( '123.45' )
|
||||
act = CONV decfloat34( ls_line-net_amount ) ).
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
55
tasks_gen/train/G1903/mutation.json
Normal file
55
tasks_gen/train/G1903/mutation.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"task": "G1903",
|
||||
"mutants": [
|
||||
{
|
||||
"object": "{{P}}AUDIT_INFO",
|
||||
"mutant": "line 4: abap.char(12) -> abap.char(11) (length)",
|
||||
"status": "killed",
|
||||
"hidden": "7/9",
|
||||
"failed_tests": [
|
||||
"AUDIT_MATCHES",
|
||||
"AUDIT_STRUCTURE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}STOCK_LINE",
|
||||
"mutant": "line 4: abap.char(18) -> abap.numc(18) (type)",
|
||||
"status": "invalid",
|
||||
"hidden": "0/0",
|
||||
"failed_tests": []
|
||||
},
|
||||
{
|
||||
"object": "{{P}}AUDIT_INFO",
|
||||
"mutant": "line 4: abap.char(12) -> abap.numc(12) (type)",
|
||||
"status": "killed",
|
||||
"hidden": "7/9",
|
||||
"failed_tests": [
|
||||
"AUDIT_MATCHES",
|
||||
"AUDIT_STRUCTURE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}STOCK_LINE",
|
||||
"mutant": "line 6: abap.dec(9,2) -> abap.dec(9,3) (decimals)",
|
||||
"status": "killed",
|
||||
"hidden": "8/9",
|
||||
"failed_tests": [
|
||||
"AMOUNT_TYPE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}AUDIT_INFO",
|
||||
"mutant": "line 5: abap.dats -> abap.tims (type)",
|
||||
"status": "killed",
|
||||
"hidden": "7/9",
|
||||
"failed_tests": [
|
||||
"AUDIT_MATCHES",
|
||||
"AUDIT_STRUCTURE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid": 4,
|
||||
"killed": 4,
|
||||
"kill_rate": 1.0,
|
||||
"ok": true
|
||||
}
|
||||
6
tasks_gen/train/G1903/reference/audit_info.ddls.asddls
Normal file
6
tasks_gen/train/G1903/reference/audit_info.ddls.asddls
Normal file
@@ -0,0 +1,6 @@
|
||||
@EndUserText.label : 'Audit information'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
define structure {{p}}audit_info {
|
||||
created_by : abap.char(12);
|
||||
created_on : abap.dats;
|
||||
}
|
||||
10
tasks_gen/train/G1903/reference/stock_line.ddls.asddls
Normal file
10
tasks_gen/train/G1903/reference/stock_line.ddls.asddls
Normal file
@@ -0,0 +1,10 @@
|
||||
@EndUserText.label : 'Stock position line'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
define structure {{p}}stock_line {
|
||||
material : abap.char(18);
|
||||
plant : abap.char(4);
|
||||
net_amount : abap.dec(9,2);
|
||||
currency : abap.char(5);
|
||||
created_by : abap.char(12);
|
||||
created_on : abap.dats;
|
||||
}
|
||||
68
tasks_gen/train/G1903/reference/t16_test.clas.abap
Normal file
68
tasks_gen/train/G1903/reference/t16_test.clas.abap
Normal file
@@ -0,0 +1,68 @@
|
||||
CLASS {{p}}t16_test DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
|
||||
PRIVATE SECTION.
|
||||
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
|
||||
|
||||
CLASS-METHODS class_setup.
|
||||
CLASS-METHODS class_teardown.
|
||||
METHODS setup.
|
||||
|
||||
METHODS six_components FOR TESTING.
|
||||
METHODS reads_stock_line FOR TESTING.
|
||||
METHODS audit_matches FOR TESTING.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}t16_test IMPLEMENTATION.
|
||||
|
||||
METHOD class_setup.
|
||||
go_env = cl_osql_test_environment=>create(
|
||||
i_dependency_list = VALUE #( ( '{{P}}STOCK' ) ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD class_teardown.
|
||||
go_env->destroy( ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD setup.
|
||||
go_env->clear_doubles( ).
|
||||
DATA lt_stock TYPE STANDARD TABLE OF {{p}}stock WITH EMPTY KEY.
|
||||
lt_stock = VALUE #(
|
||||
( material = 'MAT-1' plant = 'P001' net_amount = '123.45'
|
||||
currency = 'EUR' created_by = 'USER1' created_on = '20240115' ) ).
|
||||
go_env->insert_test_data( lt_stock ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD six_components.
|
||||
DATA(lo_struct) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lt_fields) = lo_struct->get_ddic_field_list( ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 6 act = lines( lt_fields ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD reads_stock_line.
|
||||
DATA ls_line TYPE {{p}}stock_line.
|
||||
SELECT SINGLE material, plant, net_amount, currency, created_by, created_on
|
||||
FROM {{p}}stock
|
||||
INTO @ls_line
|
||||
WHERE material = 'MAT-1'.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'MAT-1' act = ls_line-material ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'EUR' act = ls_line-currency ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD audit_matches.
|
||||
DATA(lo_line) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}STOCK_LINE' ) ).
|
||||
DATA(lo_audit) = CAST cl_abap_structdescr(
|
||||
cl_abap_typedescr=>describe_by_name( '{{P}}AUDIT_INFO' ) ).
|
||||
DATA(lt_line) = lo_line->get_ddic_field_list( ).
|
||||
DATA(lt_audit) = lo_audit->get_ddic_field_list( ).
|
||||
LOOP AT lt_audit INTO DATA(ls_audit).
|
||||
READ TABLE lt_line INTO DATA(ls_line) WITH KEY fieldname = ls_audit-fieldname.
|
||||
cl_abap_unit_assert=>assert_subrc(
|
||||
exp = 0 msg = |Component { ls_audit-fieldname } is missing| ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = ls_audit-leng act = ls_line-leng ).
|
||||
ENDLOOP.
|
||||
ENDMETHOD.
|
||||
ENDCLASS.
|
||||
14
tasks_gen/train/G1903/seed/stock.tabl.asabap
Normal file
14
tasks_gen/train/G1903/seed/stock.tabl.asabap
Normal file
@@ -0,0 +1,14 @@
|
||||
@EndUserText.label : 'Stock position'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}stock {
|
||||
key client : abap.clnt not null;
|
||||
key material : abap.char(18) not null;
|
||||
key plant : abap.char(4) not null;
|
||||
net_amount : abap.dec(9,2);
|
||||
currency : abap.char(5);
|
||||
created_by : abap.char(12);
|
||||
created_on : abap.dats;
|
||||
}
|
||||
59
tasks_gen/train/G1903/spec.md
Normal file
59
tasks_gen/train/G1903/spec.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 1. Goal
|
||||
A stock report and an OData service must use the same line type for warehouse
|
||||
stock positions. Create the DDIC structure {{P}}STOCK_LINE and the DDIC
|
||||
structure {{P}}AUDIT_INFO in package $TMP.
|
||||
|
||||
# 2. Open questions
|
||||
None.
|
||||
|
||||
# 3. Context
|
||||
- The table {{P}}STOCK exists in package $TMP. Its columns are, in this order:
|
||||
MATERIAL (CHAR 18), PLANT (CHAR 4), NET_AMOUNT (DEC 9,2), CURRENCY (CHAR 5),
|
||||
CREATED_BY (CHAR 12), CREATED_ON (DATS). The key is CLIENT, MATERIAL and
|
||||
PLANT.
|
||||
- The table {{P}}STOCK is empty. Test data comes from test doubles.
|
||||
- {{P}}AUDIT_INFO is the audit part that other objects use, for example a
|
||||
change log view and a service. {{P}}STOCK_LINE must carry the same audit
|
||||
part.
|
||||
- Both structures are used as line types in ABAP SQL and in an OData service.
|
||||
|
||||
# 4. Contract
|
||||
- Create the DDIC structure {{P}}STOCK_LINE in package $TMP.
|
||||
Components, in this order: material (CHAR 18), plant (CHAR 4),
|
||||
net_amount (DEC 9,2), currency (CHAR 5), created_by (CHAR 12),
|
||||
created_on (DATS).
|
||||
- Create the DDIC structure {{P}}AUDIT_INFO in package $TMP.
|
||||
Components, in this order: created_by (CHAR 12), created_on (DATS).
|
||||
- {{P}}STOCK_LINE contains all six components itself. It does not use a DDIC
|
||||
include.
|
||||
|
||||
# 5. Business rules
|
||||
1. {{P}}STOCK_LINE has exactly six components.
|
||||
2. The components of {{P}}STOCK_LINE are, in this order: material, plant,
|
||||
net_amount, currency, created_by, created_on.
|
||||
3. material is a character field of 18 characters.
|
||||
4. plant is a character field of 4 characters.
|
||||
5. net_amount is a packed number with 9 digits and 2 decimal places.
|
||||
6. currency is a character field of 5 characters.
|
||||
7. created_by is a character field of 12 characters.
|
||||
8. created_on is a date field of type DATS.
|
||||
9. {{P}}AUDIT_INFO has exactly two components, in this order: created_by
|
||||
(CHAR 12) and created_on (DATS).
|
||||
10. Every component of {{P}}STOCK_LINE is elementary. {{P}}STOCK_LINE is a
|
||||
flat structure.
|
||||
11. The components created_by and created_on of {{P}}STOCK_LINE have the same
|
||||
data type and the same length as the components of {{P}}AUDIT_INFO.
|
||||
12. A row of {{P}}STOCK can be read into a work area of type {{P}}STOCK_LINE
|
||||
with ABAP SQL.
|
||||
|
||||
# 6. Constraints
|
||||
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
|
||||
- Package $TMP. Do not use transports.
|
||||
- Coding standards: Clean ABAP. No ATC priority 1 or 2 findings.
|
||||
- Out of scope: do not change the table {{P}}STOCK.
|
||||
|
||||
# 7. Acceptance
|
||||
- Both structures are active and have no syntax error.
|
||||
- The hidden tests pass.
|
||||
- Write ABAP Unit tests with CL_OSQL_TEST_ENVIRONMENT in a global test class
|
||||
(for example {{P}}T16_TEST). The tests must prove rules 1 to 12.
|
||||
74
tasks_gen/train/G1903/task.json
Normal file
74
tasks_gen/train/G1903/task.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "G1903",
|
||||
"category": "B",
|
||||
"object_type": "STRU",
|
||||
"difficulty": 2,
|
||||
"release_target": "v816",
|
||||
"expected_outcome": "implement",
|
||||
"budget": {
|
||||
"max_tool_calls": 60,
|
||||
"max_activations": 15
|
||||
},
|
||||
"seed": [
|
||||
{
|
||||
"type": "TABL",
|
||||
"name": "{{P}}STOCK",
|
||||
"file": "seed/stock.tabl.asabap",
|
||||
"description": "Stock positions"
|
||||
}
|
||||
],
|
||||
"contract": [
|
||||
{
|
||||
"type": "STRU",
|
||||
"name": "{{P}}STOCK_LINE",
|
||||
"fields": [
|
||||
"material",
|
||||
"plant",
|
||||
"net_amount",
|
||||
"currency",
|
||||
"created_by",
|
||||
"created_on"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "STRU",
|
||||
"name": "{{P}}AUDIT_INFO",
|
||||
"fields": [
|
||||
"created_by",
|
||||
"created_on"
|
||||
]
|
||||
}
|
||||
],
|
||||
"out_of_scope": [
|
||||
"{{P}}STOCK"
|
||||
],
|
||||
"hidden_tests": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}T16_HIDDEN",
|
||||
"file": "hidden/t16_hidden.clas.abap",
|
||||
"description": "T16 hidden tests"
|
||||
}
|
||||
],
|
||||
"reference": [
|
||||
{
|
||||
"type": "STRU",
|
||||
"name": "{{P}}AUDIT_INFO",
|
||||
"file": "reference/audit_info.ddls.asddls",
|
||||
"description": "Audit information structure"
|
||||
},
|
||||
{
|
||||
"type": "STRU",
|
||||
"name": "{{P}}STOCK_LINE",
|
||||
"file": "reference/stock_line.ddls.asddls",
|
||||
"description": "Stock line structure"
|
||||
},
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}T16_TEST",
|
||||
"file": "reference/t16_test.clas.abap",
|
||||
"description": "T16 own tests"
|
||||
}
|
||||
],
|
||||
"craft_checks": []
|
||||
}
|
||||
45
tasks_gen/train/G1904/generation.json
Normal file
45
tasks_gen/train/G1904/generation.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": "G1904",
|
||||
"pool": "train",
|
||||
"object_type": "CLAS",
|
||||
"category": "D",
|
||||
"attempts": [
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 0,
|
||||
"killed": 0,
|
||||
"ok": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 0,
|
||||
"killed": 0,
|
||||
"ok": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 0,
|
||||
"null": 0
|
||||
},
|
||||
{
|
||||
"stage": "validate",
|
||||
"oracle": 100.0,
|
||||
"null": 0,
|
||||
"mutation": {
|
||||
"valid": 0,
|
||||
"killed": 0,
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepted": true,
|
||||
"note": "mutation check rerun after exception-class mutants were added (3 of 3 killed)"
|
||||
}
|
||||
259
tasks_gen/train/G1904/hidden/d02_hidden.clas.abap
Normal file
259
tasks_gen/train/G1904/hidden/d02_hidden.clas.abap
Normal file
@@ -0,0 +1,259 @@
|
||||
CLASS {{p}}d02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
|
||||
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
METHODS unknown_material_text FOR TESTING.
|
||||
METHODS zero_quantity_text FOR TESTING.
|
||||
METHODS over_limit_text FOR TESTING.
|
||||
METHODS default_text_cases FOR TESTING.
|
||||
METHODS context_of_each_case FOR TESTING.
|
||||
METHODS unknown_case_keeps_data FOR TESTING.
|
||||
METHODS text_via_cx_root FOR TESTING.
|
||||
METHODS case_constant_values FOR TESTING.
|
||||
METHODS is_static_check_exception FOR TESTING.
|
||||
METHODS instances_keep_own_data FOR TESTING.
|
||||
METHODS same_data_different_case FOR TESTING.
|
||||
METHODS text_exact_length FOR TESTING.
|
||||
METHODS caught_error
|
||||
IMPORTING iv_case TYPE {{p}}if_issue_error_info=>ty_case
|
||||
iv_material TYPE string
|
||||
iv_quantity TYPE i
|
||||
iv_limit TYPE i
|
||||
RETURNING VALUE(ro_error) TYPE REF TO {{p}}cx_issue_error.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}d02_hidden IMPLEMENTATION.
|
||||
|
||||
METHOD caught_error.
|
||||
TRY.
|
||||
RAISE EXCEPTION TYPE {{p}}cx_issue_error
|
||||
EXPORTING
|
||||
iv_case = iv_case
|
||||
iv_material = iv_material
|
||||
iv_quantity = iv_quantity
|
||||
iv_limit = iv_limit.
|
||||
CATCH {{p}}cx_issue_error INTO ro_error.
|
||||
ENDTRY.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD unknown_material_text.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = 'MAT-1'
|
||||
iv_quantity = 5
|
||||
iv_limit = 100 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Material MAT-1 is not known`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD zero_quantity_text.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
iv_material = 'MAT-2'
|
||||
iv_quantity = 0
|
||||
iv_limit = 50 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-2`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD over_limit_text.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'MAT-3'
|
||||
iv_quantity = 120
|
||||
iv_limit = 100 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity 120 exceeds limit 100 for material MAT-3`
|
||||
act = lo_error->get_text( ) ).
|
||||
|
||||
DATA(lo_big) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'M-BIG'
|
||||
iv_quantity = 12345
|
||||
iv_limit = 67890 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity 12345 exceeds limit 67890 for material M-BIG`
|
||||
act = lo_big->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD default_text_cases.
|
||||
DATA(lo_other) = caught_error( iv_case = 'SOMETHING_ELSE'
|
||||
iv_material = 'MAT-4'
|
||||
iv_quantity = 7
|
||||
iv_limit = 70 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material MAT-4`
|
||||
act = lo_other->get_text( ) ).
|
||||
|
||||
DATA(lo_empty) = caught_error( iv_case = ''
|
||||
iv_material = 'MAT-5'
|
||||
iv_quantity = 1
|
||||
iv_limit = 10 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material MAT-5`
|
||||
act = lo_empty->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD context_of_each_case.
|
||||
DATA(lo_unknown) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = 'M-1'
|
||||
iv_quantity = 11
|
||||
iv_limit = 111 ).
|
||||
DATA(lo_zero) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
iv_material = 'M-2'
|
||||
iv_quantity = 0
|
||||
iv_limit = 222 ).
|
||||
DATA(lo_over) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'M-3'
|
||||
iv_quantity = 33
|
||||
iv_limit = 333 ).
|
||||
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
|
||||
|
||||
lo_info = lo_unknown.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
act = lo_info->get_case( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `M-1` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 11 act = lo_info->get_quantity( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 111 act = lo_info->get_limit( ) ).
|
||||
|
||||
lo_info = lo_zero.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
act = lo_info->get_case( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `M-2` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 0 act = lo_info->get_quantity( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 222 act = lo_info->get_limit( ) ).
|
||||
|
||||
lo_info = lo_over.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-over_limit
|
||||
act = lo_info->get_case( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `M-3` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 33 act = lo_info->get_quantity( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 333 act = lo_info->get_limit( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD unknown_case_keeps_data.
|
||||
DATA(lo_error) = caught_error( iv_case = 'SOMETHING_ELSE'
|
||||
iv_material = 'M-X'
|
||||
iv_quantity = 55
|
||||
iv_limit = 66 ).
|
||||
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
|
||||
lo_info = lo_error.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'SOMETHING_ELSE' act = lo_info->get_case( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `M-X` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 55 act = lo_info->get_quantity( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 66 act = lo_info->get_limit( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material M-X`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD text_via_cx_root.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
iv_material = 'MAT-6'
|
||||
iv_quantity = 0
|
||||
iv_limit = 60 ).
|
||||
DATA lo_root TYPE REF TO cx_root.
|
||||
lo_root = lo_error.
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-6`
|
||||
act = lo_root->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD case_constant_values.
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'UNKNOWN_MATERIAL'
|
||||
act = {{p}}cx_issue_error=>c_case-unknown_material ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'ZERO_QUANTITY'
|
||||
act = {{p}}cx_issue_error=>c_case-zero_quantity ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'OVER_LIMIT'
|
||||
act = {{p}}cx_issue_error=>c_case-over_limit ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD is_static_check_exception.
|
||||
DATA lo_caught TYPE REF TO cx_static_check.
|
||||
TRY.
|
||||
RAISE EXCEPTION TYPE {{p}}cx_issue_error
|
||||
EXPORTING
|
||||
iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = 'MAT-7'
|
||||
iv_quantity = 3
|
||||
iv_limit = 30.
|
||||
CATCH cx_static_check INTO lo_caught.
|
||||
ENDTRY.
|
||||
cl_abap_unit_assert=>assert_bound( act = lo_caught ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD instances_keep_own_data.
|
||||
DATA(lo_first) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = 'FIRST'
|
||||
iv_quantity = 1
|
||||
iv_limit = 10 ).
|
||||
DATA(lo_second) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'SECOND'
|
||||
iv_quantity = 2
|
||||
iv_limit = 20 ).
|
||||
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
|
||||
|
||||
lo_info = lo_first.
|
||||
cl_abap_unit_assert=>assert_equals( exp = `FIRST` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Material FIRST is not known`
|
||||
act = lo_first->get_text( ) ).
|
||||
|
||||
lo_info = lo_second.
|
||||
cl_abap_unit_assert=>assert_equals( exp = `SECOND` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity 2 exceeds limit 20 for material SECOND`
|
||||
act = lo_second->get_text( ) ).
|
||||
|
||||
lo_info = lo_first.
|
||||
cl_abap_unit_assert=>assert_equals( exp = `FIRST` act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 1 act = lo_info->get_quantity( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD same_data_different_case.
|
||||
DATA lv_material TYPE string.
|
||||
DATA lv_quantity TYPE i.
|
||||
DATA lv_limit TYPE i.
|
||||
lv_material = 'MAT-S'.
|
||||
lv_quantity = 5.
|
||||
lv_limit = 10.
|
||||
|
||||
DATA(lo_unknown) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = lv_material
|
||||
iv_quantity = lv_quantity
|
||||
iv_limit = lv_limit ).
|
||||
DATA(lo_zero) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
iv_material = lv_material
|
||||
iv_quantity = lv_quantity
|
||||
iv_limit = lv_limit ).
|
||||
DATA(lo_over) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = lv_material
|
||||
iv_quantity = lv_quantity
|
||||
iv_limit = lv_limit ).
|
||||
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Material MAT-S is not known`
|
||||
act = lo_unknown->get_text( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-S`
|
||||
act = lo_zero->get_text( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity 5 exceeds limit 10 for material MAT-S`
|
||||
act = lo_over->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD text_exact_length.
|
||||
DATA(lo_unknown) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = 'MAT-1'
|
||||
iv_quantity = 5
|
||||
iv_limit = 100 ).
|
||||
DATA(lo_zero) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
iv_material = 'MAT-2'
|
||||
iv_quantity = 0
|
||||
iv_limit = 50 ).
|
||||
DATA(lo_over) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'MAT-3'
|
||||
iv_quantity = 120
|
||||
iv_limit = 100 ).
|
||||
|
||||
DATA lv_unknown TYPE string.
|
||||
DATA lv_zero TYPE string.
|
||||
DATA lv_over TYPE string.
|
||||
lv_unknown = `Material MAT-1 is not known`.
|
||||
lv_zero = `Quantity must not be zero for material MAT-2`.
|
||||
lv_over = `Quantity 120 exceeds limit 100 for material MAT-3`.
|
||||
|
||||
cl_abap_unit_assert=>assert_equals( exp = strlen( lv_unknown )
|
||||
act = strlen( lo_unknown->get_text( ) ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = strlen( lv_zero )
|
||||
act = strlen( lo_zero->get_text( ) ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = strlen( lv_over )
|
||||
act = strlen( lo_over->get_text( ) ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
ENDCLASS.
|
||||
36
tasks_gen/train/G1904/mutation.json
Normal file
36
tasks_gen/train/G1904/mutation.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"task": "G1904",
|
||||
"mutants": [
|
||||
{
|
||||
"object": "{{P}}CX_ISSUE_ERROR",
|
||||
"mutant": "line 12: VALUE 'UNKNOWN_MATERIAL' -> VALUE 'ZNKNOWN_MATERIAL' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "11/12",
|
||||
"failed_tests": [
|
||||
"CASE_CONSTANT_VALUES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}CX_ISSUE_ERROR",
|
||||
"mutant": "line 13: VALUE 'ZERO_QUANTITY' -> VALUE 'YERO_QUANTITY' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "11/12",
|
||||
"failed_tests": [
|
||||
"CASE_CONSTANT_VALUES"
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "{{P}}CX_ISSUE_ERROR",
|
||||
"mutant": "line 14: VALUE 'OVER_LIMIT' -> VALUE 'ZVER_LIMIT' (lit)",
|
||||
"status": "killed",
|
||||
"hidden": "11/12",
|
||||
"failed_tests": [
|
||||
"CASE_CONSTANT_VALUES"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid": 3,
|
||||
"killed": 3,
|
||||
"kill_rate": 1.0,
|
||||
"ok": true
|
||||
}
|
||||
73
tasks_gen/train/G1904/reference/cx_issue_error.clas.abap
Normal file
73
tasks_gen/train/G1904/reference/cx_issue_error.clas.abap
Normal file
@@ -0,0 +1,73 @@
|
||||
CLASS {{p}}cx_issue_error DEFINITION
|
||||
PUBLIC
|
||||
INHERITING FROM cx_static_check
|
||||
FINAL
|
||||
CREATE PUBLIC.
|
||||
|
||||
PUBLIC SECTION.
|
||||
INTERFACES {{p}}if_issue_error_info.
|
||||
|
||||
CONSTANTS:
|
||||
BEGIN OF c_case,
|
||||
unknown_material TYPE {{p}}if_issue_error_info=>ty_case VALUE 'UNKNOWN_MATERIAL',
|
||||
zero_quantity TYPE {{p}}if_issue_error_info=>ty_case VALUE 'ZERO_QUANTITY',
|
||||
over_limit TYPE {{p}}if_issue_error_info=>ty_case VALUE 'OVER_LIMIT',
|
||||
END OF c_case.
|
||||
|
||||
METHODS constructor
|
||||
IMPORTING
|
||||
iv_case TYPE {{p}}if_issue_error_info=>ty_case
|
||||
iv_material TYPE string
|
||||
iv_quantity TYPE i
|
||||
iv_limit TYPE i.
|
||||
|
||||
METHODS get_text REDEFINITION.
|
||||
|
||||
PRIVATE SECTION.
|
||||
DATA mv_case TYPE {{p}}if_issue_error_info=>ty_case.
|
||||
DATA mv_material TYPE string.
|
||||
DATA mv_quantity TYPE i.
|
||||
DATA mv_limit TYPE i.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS {{p}}cx_issue_error IMPLEMENTATION.
|
||||
|
||||
METHOD constructor.
|
||||
super->constructor( ).
|
||||
mv_case = iv_case.
|
||||
mv_material = iv_material.
|
||||
mv_quantity = iv_quantity.
|
||||
mv_limit = iv_limit.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_issue_error_info~get_case.
|
||||
rv_case = mv_case.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_issue_error_info~get_material.
|
||||
rv_material = mv_material.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_issue_error_info~get_quantity.
|
||||
rv_quantity = mv_quantity.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD {{p}}if_issue_error_info~get_limit.
|
||||
rv_limit = mv_limit.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD get_text.
|
||||
CASE mv_case.
|
||||
WHEN c_case-unknown_material.
|
||||
result = |Material { mv_material } is not known|.
|
||||
WHEN c_case-zero_quantity.
|
||||
result = |Quantity must not be zero for material { mv_material }|.
|
||||
WHEN c_case-over_limit.
|
||||
result = |Quantity { mv_quantity } exceeds limit { mv_limit } for material { mv_material }|.
|
||||
WHEN OTHERS.
|
||||
result = |Stock issue failed for material { mv_material }|.
|
||||
ENDCASE.
|
||||
ENDMETHOD.
|
||||
|
||||
ENDCLASS.
|
||||
@@ -0,0 +1,81 @@
|
||||
CLASS ltc_issue_error DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
|
||||
PRIVATE SECTION.
|
||||
METHODS unknown_material_text FOR TESTING.
|
||||
METHODS zero_quantity_text FOR TESTING.
|
||||
METHODS over_limit_text FOR TESTING.
|
||||
METHODS default_text FOR TESTING.
|
||||
METHODS keeps_context_data FOR TESTING.
|
||||
METHODS caught_error
|
||||
IMPORTING iv_case TYPE {{p}}if_issue_error_info=>ty_case
|
||||
iv_material TYPE string
|
||||
iv_quantity TYPE i
|
||||
iv_limit TYPE i
|
||||
RETURNING VALUE(ro_error) TYPE REF TO {{p}}cx_issue_error.
|
||||
ENDCLASS.
|
||||
|
||||
|
||||
CLASS ltc_issue_error IMPLEMENTATION.
|
||||
|
||||
METHOD caught_error.
|
||||
TRY.
|
||||
RAISE EXCEPTION TYPE {{p}}cx_issue_error
|
||||
EXPORTING
|
||||
iv_case = iv_case
|
||||
iv_material = iv_material
|
||||
iv_quantity = iv_quantity
|
||||
iv_limit = iv_limit.
|
||||
CATCH {{p}}cx_issue_error INTO ro_error.
|
||||
ENDTRY.
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD unknown_material_text.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-unknown_material
|
||||
iv_material = 'MAT-A'
|
||||
iv_quantity = 1
|
||||
iv_limit = 10 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Material MAT-A is not known`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD zero_quantity_text.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-zero_quantity
|
||||
iv_material = 'MAT-B'
|
||||
iv_quantity = 0
|
||||
iv_limit = 20 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity must not be zero for material MAT-B`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD over_limit_text.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'MAT-C'
|
||||
iv_quantity = 30
|
||||
iv_limit = 20 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Quantity 30 exceeds limit 20 for material MAT-C`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD default_text.
|
||||
DATA(lo_error) = caught_error( iv_case = 'OTHER'
|
||||
iv_material = 'MAT-D'
|
||||
iv_quantity = 5
|
||||
iv_limit = 50 ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = `Stock issue failed for material MAT-D`
|
||||
act = lo_error->get_text( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
METHOD keeps_context_data.
|
||||
DATA(lo_error) = caught_error( iv_case = {{p}}cx_issue_error=>c_case-over_limit
|
||||
iv_material = 'MAT-E'
|
||||
iv_quantity = 99
|
||||
iv_limit = 90 ).
|
||||
DATA lo_info TYPE REF TO {{p}}if_issue_error_info.
|
||||
lo_info = lo_error.
|
||||
cl_abap_unit_assert=>assert_equals( exp = {{p}}cx_issue_error=>c_case-over_limit
|
||||
act = lo_info->get_case( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 'MAT-E' act = lo_info->get_material( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 99 act = lo_info->get_quantity( ) ).
|
||||
cl_abap_unit_assert=>assert_equals( exp = 90 act = lo_info->get_limit( ) ).
|
||||
ENDMETHOD.
|
||||
|
||||
ENDCLASS.
|
||||
12
tasks_gen/train/G1904/seed/if_issue_error_info.intf.abap
Normal file
12
tasks_gen/train/G1904/seed/if_issue_error_info.intf.abap
Normal file
@@ -0,0 +1,12 @@
|
||||
INTERFACE {{p}}if_issue_error_info PUBLIC.
|
||||
TYPES ty_case TYPE c LENGTH 20.
|
||||
|
||||
METHODS get_case
|
||||
RETURNING VALUE(rv_case) TYPE ty_case.
|
||||
METHODS get_material
|
||||
RETURNING VALUE(rv_material) TYPE string.
|
||||
METHODS get_quantity
|
||||
RETURNING VALUE(rv_quantity) TYPE i.
|
||||
METHODS get_limit
|
||||
RETURNING VALUE(rv_limit) TYPE i.
|
||||
ENDINTERFACE.
|
||||
58
tasks_gen/train/G1904/spec.md
Normal file
58
tasks_gen/train/G1904/spec.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 1. Goal
|
||||
A stock issue program must stop with a clear reason when it cannot issue a
|
||||
quantity of a material. The caller needs one exception class that carries the
|
||||
reason and the data of the failed issue.
|
||||
|
||||
# 2. Open questions
|
||||
None.
|
||||
|
||||
# 3. Context
|
||||
- The interface {{P}}IF_ISSUE_ERROR_INFO exists in package $TMP. It is active.
|
||||
- The interface has the type TY_CASE (CHAR 20) and the methods GET_CASE,
|
||||
GET_MATERIAL, GET_QUANTITY and GET_LIMIT.
|
||||
- The exception class {{P}}CX_ISSUE_ERROR is raised by the stock issue program
|
||||
and caught by the caller.
|
||||
|
||||
# 4. Contract
|
||||
- Create the class {{P}}CX_ISSUE_ERROR in package $TMP.
|
||||
- The class inherits from CX_STATIC_CHECK.
|
||||
- The class is public, final, and has a public constructor.
|
||||
- The class implements {{P}}IF_ISSUE_ERROR_INFO.
|
||||
- The class has the constant structure C_CASE. Its components are
|
||||
UNKNOWN_MATERIAL, ZERO_QUANTITY and OVER_LIMIT. The type of each component is
|
||||
{{P}}IF_ISSUE_ERROR_INFO=>TY_CASE. The value of each component is its name.
|
||||
- The constructor has these importing parameters, in this order:
|
||||
IV_CASE (type {{P}}IF_ISSUE_ERROR_INFO=>TY_CASE), IV_MATERIAL (type STRING),
|
||||
IV_QUANTITY (type I), IV_LIMIT (type I).
|
||||
- The class redefines the inherited method GET_TEXT. GET_TEXT has no importing
|
||||
parameter and returns the text as a string.
|
||||
- Do not add other public methods.
|
||||
|
||||
# 5. Business rules
|
||||
1. The constructor stores IV_CASE, IV_MATERIAL, IV_QUANTITY and IV_LIMIT.
|
||||
2. {{P}}IF_ISSUE_ERROR_INFO~GET_CASE returns the stored case, ~GET_MATERIAL the
|
||||
stored material, ~GET_QUANTITY the stored quantity and ~GET_LIMIT the stored
|
||||
limit.
|
||||
3. GET_TEXT returns one text that depends on the stored case:
|
||||
- C_CASE-UNKNOWN_MATERIAL: 'Material <material> is not known'
|
||||
- C_CASE-ZERO_QUANTITY: 'Quantity must not be zero for material <material>'
|
||||
- C_CASE-OVER_LIMIT: 'Quantity <quantity> exceeds limit <limit> for material
|
||||
<material>'
|
||||
- any other case, including an empty case: 'Stock issue failed for material
|
||||
<material>'
|
||||
<material>, <quantity> and <limit> are the stored values. Write the quantity
|
||||
and the limit as plain digits, for example 120 and 100. The text has no
|
||||
leading and no trailing blanks.
|
||||
4. The class is a static check exception. A caller must handle it or declare it
|
||||
in the RAISING clause.
|
||||
|
||||
# 6. Constraints
|
||||
- Release target: SAP_BASIS 816.
|
||||
- Coding standards: Clean ABAP. Method length below 40 statements. No global
|
||||
variables. No comment that restates the code.
|
||||
- Out of scope: do not change {{P}}IF_ISSUE_ERROR_INFO.
|
||||
|
||||
# 7. Acceptance
|
||||
- The class is active and has no syntax error.
|
||||
- The hidden tests pass.
|
||||
- Write your own ABAP Unit tests for the class.
|
||||
50
tasks_gen/train/G1904/task.json
Normal file
50
tasks_gen/train/G1904/task.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "G1904",
|
||||
"category": "D",
|
||||
"object_type": "CLAS",
|
||||
"difficulty": 2,
|
||||
"release_target": "v816",
|
||||
"expected_outcome": "implement",
|
||||
"budget": {
|
||||
"max_tool_calls": 60,
|
||||
"max_activations": 15
|
||||
},
|
||||
"seed": [
|
||||
{
|
||||
"type": "INTF",
|
||||
"name": "{{P}}IF_ISSUE_ERROR_INFO",
|
||||
"file": "seed/if_issue_error_info.intf.abap",
|
||||
"description": "Contract interface with the case type and the getters of the issue error"
|
||||
}
|
||||
],
|
||||
"contract": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}CX_ISSUE_ERROR",
|
||||
"implements": "{{P}}IF_ISSUE_ERROR_INFO"
|
||||
}
|
||||
],
|
||||
"out_of_scope": [
|
||||
"{{P}}IF_ISSUE_ERROR_INFO"
|
||||
],
|
||||
"hidden_tests": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}D02_HIDDEN",
|
||||
"file": "hidden/d02_hidden.clas.abap",
|
||||
"description": "D02 hidden tests for the issue error exception class"
|
||||
}
|
||||
],
|
||||
"reference": [
|
||||
{
|
||||
"type": "CLAS",
|
||||
"name": "{{P}}CX_ISSUE_ERROR",
|
||||
"file": "reference/cx_issue_error.clas.abap",
|
||||
"description": "Exception class with case constants, context data and a redefined GET_TEXT",
|
||||
"testclasses_file": "reference/cx_issue_error.testclasses.abap"
|
||||
}
|
||||
],
|
||||
"craft_checks": [
|
||||
"method_length"
|
||||
]
|
||||
}
|
||||
19
tasks_gen/train/G1910/faulty/m0_cider.tabl.asabap
Normal file
19
tasks_gen/train/G1910/faulty/m0_cider.tabl.asabap
Normal file
@@ -0,0 +1,19 @@
|
||||
@EndUserText.label : 'Cider pressing batch'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}cider {
|
||||
key client : abap.clnt not null;
|
||||
batch_id : abap.char(10) not null;
|
||||
orchard_id : abap.char(6) not null;
|
||||
apple_variety : abap.char(20);
|
||||
pressed_on : abap.dats;
|
||||
bin_count : abap.int4;
|
||||
apple_kg : abap.dec(13,3);
|
||||
juice_l : abap.dec(13,3);
|
||||
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
|
||||
press_fee : abap.curr(15,2);
|
||||
currency_code : abap.cuky;
|
||||
grade : abap.char(2);
|
||||
}
|
||||
19
tasks_gen/train/G1910/faulty/m1_cider.tabl.asabap
Normal file
19
tasks_gen/train/G1910/faulty/m1_cider.tabl.asabap
Normal file
@@ -0,0 +1,19 @@
|
||||
@EndUserText.label : 'Cider pressing batch'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}cider {
|
||||
key client : abap.clnt not null;
|
||||
key batch_id : abap.char(10) not null;
|
||||
orchard_id : abap.numc(6) not null;
|
||||
apple_variety : abap.char(20);
|
||||
pressed_on : abap.dats;
|
||||
bin_count : abap.int4;
|
||||
apple_kg : abap.dec(13,3);
|
||||
juice_l : abap.dec(13,3);
|
||||
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
|
||||
press_fee : abap.curr(15,2);
|
||||
currency_code : abap.cuky;
|
||||
grade : abap.char(2);
|
||||
}
|
||||
19
tasks_gen/train/G1910/faulty/m2_cider.tabl.asabap
Normal file
19
tasks_gen/train/G1910/faulty/m2_cider.tabl.asabap
Normal file
@@ -0,0 +1,19 @@
|
||||
@EndUserText.label : 'Cider pressing batch'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}cider {
|
||||
key client : abap.clnt not null;
|
||||
key batch_id : abap.char(10) not null;
|
||||
orchard_id : abap.char(5) not null;
|
||||
apple_variety : abap.char(20);
|
||||
pressed_on : abap.dats;
|
||||
bin_count : abap.int4;
|
||||
apple_kg : abap.dec(13,3);
|
||||
juice_l : abap.dec(13,3);
|
||||
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
|
||||
press_fee : abap.curr(15,2);
|
||||
currency_code : abap.cuky;
|
||||
grade : abap.char(2);
|
||||
}
|
||||
19
tasks_gen/train/G1910/faulty/m3_cider.tabl.asabap
Normal file
19
tasks_gen/train/G1910/faulty/m3_cider.tabl.asabap
Normal file
@@ -0,0 +1,19 @@
|
||||
@EndUserText.label : 'Cider pressing batch'
|
||||
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
|
||||
@AbapCatalog.tableCategory : #TRANSPARENT
|
||||
@AbapCatalog.deliveryClass : #A
|
||||
@AbapCatalog.dataMaintenance : #RESTRICTED
|
||||
define table {{p}}cider {
|
||||
key client : abap.clnt not null;
|
||||
key batch_id : abap.char(10) not null;
|
||||
orchard_id : abap.char(6) not null;
|
||||
apple_variety : abap.char(19);
|
||||
pressed_on : abap.dats;
|
||||
bin_count : abap.int4;
|
||||
apple_kg : abap.dec(13,3);
|
||||
juice_l : abap.dec(13,3);
|
||||
@Semantics.amount.currencyCode : '{{p}}cider.currency_code'
|
||||
press_fee : abap.curr(15,2);
|
||||
currency_code : abap.cuky;
|
||||
grade : abap.char(2);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user