ApiLogicServer 15.2.0__py3-none-any.whl → 15.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api_logic_server_cli/api_logic_server.py +2 -2
- api_logic_server_cli/prototypes/base/.github/.copilot-instructions.md +22 -0
- api_logic_server_cli/prototypes/base/docs/training/testing.md +21 -9
- api_logic_server_cli/prototypes/base/test/api_logic_server_behave/behave_logic_report.py +55 -29
- api_logic_server_cli/prototypes/base/test/api_logic_server_behave/behave_logic_report.py.bak +285 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/test/api_logic_server_behave/reports/Behave Logic Report Intro micro.md +35 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/test/api_logic_server_behave/reports/Behave Logic Report Intro.md +35 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/docs/training/testing.md +210 -12
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/behave_logic_report.py +13 -84
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/test/api_logic_server_behave/behave_logic_report.py.bak +282 -0
- api_logic_server_cli/prototypes/manager/system/ApiLogicServer-Internal-Dev/copilot-dev-context.md +39 -4
- api_logic_server_cli/prototypes/manager/system/app_model_editor/test/api_logic_server_behave/behave_logic_report.py +13 -75
- api_logic_server_cli/prototypes/manager/system/app_model_editor/test/api_logic_server_behave/behave_logic_report.py.bak +256 -0
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_docs_logic/test/api_logic_server_behave/behave_logic_report.py +13 -75
- api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_docs_logic/test/api_logic_server_behave/behave_logic_report.py.bak +256 -0
- api_logic_server_cli/prototypes/nw/test/api_logic_server_behave/reports/Behave Logic Report Intro micro.md +17 -0
- api_logic_server_cli/prototypes/nw/test/api_logic_server_behave/reports/Behave Logic Report Intro.md +17 -0
- {apilogicserver-15.2.0.dist-info → apilogicserver-15.2.3.dist-info}/METADATA +79 -8
- {apilogicserver-15.2.0.dist-info → apilogicserver-15.2.3.dist-info}/RECORD +23 -16
- api_logic_server_cli/prototypes/base/.github/.copilot-instructionsZ.mdx +0 -661
- {apilogicserver-15.2.0.dist-info → apilogicserver-15.2.3.dist-info}/WHEEL +0 -0
- {apilogicserver-15.2.0.dist-info → apilogicserver-15.2.3.dist-info}/entry_points.txt +0 -0
- {apilogicserver-15.2.0.dist-info → apilogicserver-15.2.3.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-15.2.0.dist-info → apilogicserver-15.2.3.dist-info}/top_level.txt +0 -0
|
@@ -19,11 +19,38 @@ Step 1c. Scan api/api_discovery/*.py → Discover custom APIs (PHASE 2!)
|
|
|
19
19
|
Step 2. Decide Phase 1 vs Phase 2 → Based on custom API existence
|
|
20
20
|
Step 3. Generate .feature files → Business language scenarios
|
|
21
21
|
Step 4. Implement steps/*.py → Using discovered APIs or CRUD
|
|
22
|
-
Step
|
|
22
|
+
Step 4b. VERIFY STEP ORDERING → Multi-item BEFORE single-item (Rule #0.5!)
|
|
23
|
+
Step 5. SUGGEST how to run tests (DO NOT run automatically)
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
**CRITICAL PRE-TEST CHECKLIST:**
|
|
27
|
+
- [ ] Step 1c completed? (Custom APIs discovered)
|
|
28
|
+
- [ ] **Database values verified?** (Rule #10: Run SQL to check actual prices/flags)
|
|
29
|
+
- [ ] `sqlite3 db.sqlite "SELECT name, unit_price, carbon_neutral FROM product;"`
|
|
30
|
+
- [ ] Don't assume product attributes - verify BEFORE writing expectations!
|
|
31
|
+
- [ ] **Step ordering verified?** (Most specific → Most general)
|
|
32
|
+
- [ ] @when patterns: carbon neutral > multi-item > single-item
|
|
33
|
+
- [ ] @given patterns: multi-item > single-item
|
|
34
|
+
- [ ] Use `grep -n "@when('.*with" steps/*.py` to verify order
|
|
35
|
+
- [ ] Test data uses timestamps? (Rule #0: Repeatability)
|
|
36
|
+
- [ ] Security config read? (SECURITY_ENABLED value)
|
|
37
|
+
|
|
25
38
|
**DO NOT skip Step 1c!** Custom APIs change the entire testing approach.
|
|
26
39
|
|
|
40
|
+
**DO NOT skip Step 4b!** Wrong step ordering causes silent failures with balance=0.
|
|
41
|
+
|
|
42
|
+
**DO NOT run tests automatically!** Instead, suggest this workflow:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. Start the server (in separate terminal or background)
|
|
46
|
+
python api_logic_server_run.py
|
|
47
|
+
|
|
48
|
+
# 2. Run the tests (in another terminal)
|
|
49
|
+
python test/api_logic_server_behave/behave_run.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Why manual execution?** Tests require a running server. The AI cannot manage multiple terminals or background processes reliably.
|
|
53
|
+
|
|
27
54
|
## Phase 1 vs Phase 2: The Core Decision
|
|
28
55
|
|
|
29
56
|
### Phase 1: CRUD-Level Testing
|
|
@@ -213,7 +240,7 @@ def step_impl_general(context, customer_name, quantity, product_name):
|
|
|
213
240
|
...
|
|
214
241
|
```
|
|
215
242
|
|
|
216
|
-
**Example 2: Multi-Item Orders**
|
|
243
|
+
**Example 2: Multi-Item Orders (GIVEN pattern)**
|
|
217
244
|
|
|
218
245
|
```python
|
|
219
246
|
# ❌ WRONG ORDER - Single-item pattern matches "3 Widget and 2 Gadget"
|
|
@@ -242,6 +269,51 @@ def step_impl_single(context, customer_name, quantity, product_name):
|
|
|
242
269
|
...
|
|
243
270
|
```
|
|
244
271
|
|
|
272
|
+
**Example 3: Multi-Item Orders (WHEN pattern) - Real Bug Found!**
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
# ❌ WRONG ORDER - Causes silent failure with balance=0 instead of expected value
|
|
276
|
+
@when('B2B order placed for "{customer_name}" with {quantity:d} {product_name}')
|
|
277
|
+
def step_impl_single(context, customer_name, quantity, product_name):
|
|
278
|
+
# This matches "3 Widget and 2 Gadget" FIRST!
|
|
279
|
+
# product_name becomes "Widget and 2 Gadget" (entire string)
|
|
280
|
+
# OrderB2B API tries to find product "Widget and 2 Gadget" → fails
|
|
281
|
+
# Returns 200 but no order created → customer balance stays 0
|
|
282
|
+
# Test fails: "expected 570, got 0.0"
|
|
283
|
+
...
|
|
284
|
+
|
|
285
|
+
@when('B2B order placed for "{customer_name}" with {qty1:d} {product1} and {qty2:d} {product2}')
|
|
286
|
+
def step_impl_multi(context, customer_name, qty1, product1, qty2, product2):
|
|
287
|
+
# NEVER REACHED! Single-item pattern matched first
|
|
288
|
+
...
|
|
289
|
+
|
|
290
|
+
# ✅ CORRECT ORDER - Multi-item pattern MUST come before single-item
|
|
291
|
+
@when('B2B order placed for "{customer_name}" with {qty1:d} {product1} and {qty2:d} {product2}')
|
|
292
|
+
def step_impl_multi(context, customer_name, qty1, product1, qty2, product2):
|
|
293
|
+
# Now matches "3 Widget and 2 Gadget" correctly
|
|
294
|
+
# Creates order with 2 items, balance = 570
|
|
295
|
+
...
|
|
296
|
+
|
|
297
|
+
@when('B2B order placed for "{customer_name}" with {quantity:d} {product_name}')
|
|
298
|
+
def step_impl_single(context, customer_name, quantity, product_name):
|
|
299
|
+
# Now only matches single product patterns
|
|
300
|
+
...
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**CRITICAL FILE ORGANIZATION:**
|
|
304
|
+
Within each decorator type (@given, @when, @then), organize patterns like this:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
# For @when patterns:
|
|
308
|
+
@when('... with {qty:d} carbon neutral {product}') # Most specific (3+ keywords)
|
|
309
|
+
@when('... with {qty1:d} {p1} and {qty2:d} {p2}') # More specific (multi-param + "and")
|
|
310
|
+
@when('... with {quantity:d} {product_name}') # General (fewest keywords)
|
|
311
|
+
|
|
312
|
+
# For @given patterns:
|
|
313
|
+
@given('... with {qty1:d} {p1} and {qty2:d} {p2}') # More specific (multi-param + "and")
|
|
314
|
+
@given('... with {quantity:d} {product_name}') # General (fewer params)
|
|
315
|
+
```
|
|
316
|
+
|
|
245
317
|
**Why This Matters:**
|
|
246
318
|
- Wrong order → context.item_id not set → "Then Item amount" step fails with "item_id not set in context"
|
|
247
319
|
- Behave doesn't warn about unreachable patterns
|
|
@@ -401,19 +473,28 @@ Rule.sum(derive=Customer.balance, as_sum_of=Order.amount_total,
|
|
|
401
473
|
```python
|
|
402
474
|
# ALWAYS check actual product prices/flags before writing test expectations!
|
|
403
475
|
|
|
404
|
-
# Check prices:
|
|
405
|
-
# sqlite3 db.sqlite "SELECT name, unit_price, carbon_neutral FROM
|
|
476
|
+
# Check prices AND flags:
|
|
477
|
+
# sqlite3 db.sqlite "SELECT name, unit_price, carbon_neutral FROM product;"
|
|
406
478
|
|
|
407
|
-
#
|
|
479
|
+
# Results:
|
|
480
|
+
# 1|Gadget|150|1 ← carbon_neutral = 1 (TRUE)
|
|
481
|
+
# 2|Widget|90| ← carbon_neutral = NULL (not carbon neutral!)
|
|
482
|
+
# 3|Thingamajig|5075|
|
|
483
|
+
# 4|Doodad|110|
|
|
484
|
+
# 5|Green|109|1 ← carbon_neutral = 1 (TRUE)
|
|
408
485
|
|
|
409
|
-
# ❌ WRONG - Assumed Widget
|
|
410
|
-
#
|
|
486
|
+
# ❌ WRONG - Assumed Widget is carbon neutral
|
|
487
|
+
# Scenario: Carbon Neutral Discount
|
|
488
|
+
# When B2B order placed with 10 carbon neutral Widget
|
|
489
|
+
# Then balance should be 810 # Expected 10 * 90 * 0.9 = 810
|
|
490
|
+
# FAILS: Widget is NOT carbon neutral → no discount → balance = 900
|
|
411
491
|
|
|
412
|
-
# ✅ CORRECT - Verified
|
|
413
|
-
#
|
|
492
|
+
# ✅ CORRECT - Verified Gadget IS carbon neutral (flag = 1)
|
|
493
|
+
# Scenario: Carbon Neutral Discount
|
|
494
|
+
# When B2B order placed with 10 carbon neutral Gadget
|
|
495
|
+
# Then balance should be 1350 # Correct: 10 * 150 * 0.9 = 1350
|
|
414
496
|
|
|
415
|
-
#
|
|
416
|
-
# Expected: 10 * 90 * 0.9 = 810
|
|
497
|
+
# CRITICAL: Don't assume product attributes - VERIFY with SQL first!
|
|
417
498
|
```
|
|
418
499
|
|
|
419
500
|
### Rule #11: Step Definitions Must Match Feature Files ⚠️ NEW
|
|
@@ -433,7 +514,7 @@ def step_impl(context, customer_name): # Missing balance and limit parameters!
|
|
|
433
514
|
limit = 1000
|
|
434
515
|
```
|
|
435
516
|
|
|
436
|
-
### Rule #
|
|
517
|
+
### Rule #12: Always Initialize Context Variables ⚠️ NEW
|
|
437
518
|
```python
|
|
438
519
|
# ✅ CORRECT - prevents KeyError in subsequent steps
|
|
439
520
|
@when('B2B order placed')
|
|
@@ -547,6 +628,43 @@ def step_impl(context, expected):
|
|
|
547
628
|
| "row altered by another user" | Use direct FK: `"customer_id": int(id)` |
|
|
548
629
|
| "circular import" | Remove imports from logic/, database/ |
|
|
549
630
|
| "empty logic log" | Add `test_utils.prt(msg, scenario_name)` |
|
|
631
|
+
| **"balance: expected 570, got 0.0"** | **Step ordering issue (Rule #0.5)! Multi-item pattern after single-item** |
|
|
632
|
+
| **"context has no attribute 'item_id'"** | **Step ordering issue! Specific pattern defined after general pattern** |
|
|
633
|
+
| "Order created but no items" | Check if wrong step matched (print debug in step) |
|
|
634
|
+
|
|
635
|
+
### Debugging Step Ordering Issues
|
|
636
|
+
|
|
637
|
+
**Symptom**: Test passes WHEN step but THEN assertions fail with unexpected values (often 0 or None).
|
|
638
|
+
|
|
639
|
+
**Diagnosis**:
|
|
640
|
+
1. **Check which step executed**: Look at test output for line numbers
|
|
641
|
+
```
|
|
642
|
+
When B2B order placed for "Kevin" with 3 Widget and 2 Gadget # line=261
|
|
643
|
+
```
|
|
644
|
+
If line 261 is single-item but you expected line 320 (multi-item), wrong pattern matched!
|
|
645
|
+
|
|
646
|
+
2. **Verify database**: Check if records were actually created
|
|
647
|
+
```bash
|
|
648
|
+
sqlite3 db.sqlite "SELECT * FROM 'order' WHERE customer_id = X;"
|
|
649
|
+
sqlite3 db.sqlite "SELECT * FROM item WHERE order_id = Y;"
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
3. **Check step file organization**: Count literal keywords in each pattern
|
|
653
|
+
```python
|
|
654
|
+
# line 202: "carbon neutral" = 2 keywords (most specific)
|
|
655
|
+
# line 261: {quantity:d} {product_name} = 0 keywords (general)
|
|
656
|
+
# line 320: {qty1} {p1} "and" {qty2} {p2} = 1 keyword (more specific)
|
|
657
|
+
```
|
|
658
|
+
Line 320 MUST come BEFORE line 261!
|
|
659
|
+
|
|
660
|
+
**Quick Fix**:
|
|
661
|
+
```bash
|
|
662
|
+
# Find all @when patterns in your steps file
|
|
663
|
+
grep -n "@when('.*with.*{" features/steps/*.py
|
|
664
|
+
|
|
665
|
+
# Reorder so patterns with MORE keywords/parameters come FIRST
|
|
666
|
+
# Rule: Most specific → Most general (top to bottom)
|
|
667
|
+
```
|
|
550
668
|
|
|
551
669
|
## Test Generation Workflow
|
|
552
670
|
|
|
@@ -591,3 +709,83 @@ def step_impl(context, expected):
|
|
|
591
709
|
|
|
592
710
|
**The Magic:** Users built OrderB2B for partners → Same API provides natural test scenarios!
|
|
593
711
|
|
|
712
|
+
## Automated Step Ordering Verification
|
|
713
|
+
|
|
714
|
+
**Command to verify step ordering in your test files:**
|
|
715
|
+
|
|
716
|
+
```bash
|
|
717
|
+
# List all @when patterns with line numbers (should be ordered specific→general)
|
|
718
|
+
cd test/api_logic_server_behave
|
|
719
|
+
grep -n "@when('.*with.*{" features/steps/*.py
|
|
720
|
+
|
|
721
|
+
# Expected output (line numbers ascending = correct order):
|
|
722
|
+
# 202: @when('... with {qty:d} carbon neutral {product}') # Most specific
|
|
723
|
+
# 265: @when('... with {qty1:d} {p1} and {qty2:d} {p2}') # More specific
|
|
724
|
+
# 318: @when('... with {quantity:d} {product_name}') # General
|
|
725
|
+
|
|
726
|
+
# If multi-item (line 265) comes AFTER single-item (line 318) = WRONG!
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Python script to auto-check ordering** (add to test directory):
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
#!/usr/bin/env python3
|
|
733
|
+
"""Verify Behave step ordering for multi-param patterns."""
|
|
734
|
+
import re, sys
|
|
735
|
+
from pathlib import Path
|
|
736
|
+
|
|
737
|
+
def check_step_order(steps_file):
|
|
738
|
+
"""Check if multi-item patterns come before single-item patterns."""
|
|
739
|
+
with open(steps_file) as f:
|
|
740
|
+
lines = f.readlines()
|
|
741
|
+
|
|
742
|
+
issues = []
|
|
743
|
+
when_patterns = []
|
|
744
|
+
|
|
745
|
+
for i, line in enumerate(lines, 1):
|
|
746
|
+
if match := re.match(r"@when\('.*with.*\{", line):
|
|
747
|
+
# Count parameters and keywords
|
|
748
|
+
param_count = line.count('{')
|
|
749
|
+
has_and = ' and ' in line
|
|
750
|
+
has_special_keyword = any(k in line for k in ['carbon neutral', 'shipped'])
|
|
751
|
+
|
|
752
|
+
specificity = param_count + (2 if has_and else 0) + (3 if has_special_keyword else 0)
|
|
753
|
+
when_patterns.append((i, specificity, line.strip()))
|
|
754
|
+
|
|
755
|
+
# Check if patterns are in descending specificity order
|
|
756
|
+
for i in range(len(when_patterns) - 1):
|
|
757
|
+
curr_line, curr_spec, curr_text = when_patterns[i]
|
|
758
|
+
next_line, next_spec, next_text = when_patterns[i + 1]
|
|
759
|
+
|
|
760
|
+
if next_spec > curr_spec:
|
|
761
|
+
issues.append(f"❌ Line {next_line} (specificity={next_spec}) should come BEFORE line {curr_line} (specificity={curr_spec})")
|
|
762
|
+
issues.append(f" More specific: {next_text}")
|
|
763
|
+
issues.append(f" Less specific: {curr_text}")
|
|
764
|
+
|
|
765
|
+
return issues
|
|
766
|
+
|
|
767
|
+
if __name__ == '__main__':
|
|
768
|
+
steps_dir = Path('features/steps')
|
|
769
|
+
all_issues = []
|
|
770
|
+
|
|
771
|
+
for steps_file in steps_dir.glob('*_steps.py'):
|
|
772
|
+
issues = check_step_order(steps_file)
|
|
773
|
+
if issues:
|
|
774
|
+
all_issues.extend([f"\n{steps_file}:"] + issues)
|
|
775
|
+
|
|
776
|
+
if all_issues:
|
|
777
|
+
print("Step Ordering Issues Found:")
|
|
778
|
+
print('\n'.join(all_issues))
|
|
779
|
+
sys.exit(1)
|
|
780
|
+
else:
|
|
781
|
+
print("✅ All step patterns correctly ordered (specific → general)")
|
|
782
|
+
sys.exit(0)
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
**Usage:**
|
|
786
|
+
```bash
|
|
787
|
+
cd test/api_logic_server_behave
|
|
788
|
+
python check_step_order.py # Run before committing tests
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
This automation prevents Rule #0.5 violations across ALL databases and projects!
|
|
@@ -193,90 +193,19 @@ def main(behave_log: str, scenario_logs: str, wiki: str, prepend_wiki: str):
|
|
|
193
193
|
wiki_data.append(" ")
|
|
194
194
|
each_line = "## " + each_line
|
|
195
195
|
if each_line.startswith(" Scenario"):
|
|
196
|
-
#
|
|
197
|
-
if just_saw_then and previous_scenario:
|
|
198
|
-
show_logic(scenario=previous_scenario, logic_logs_dir=scenario_logs)
|
|
199
|
-
just_saw_then = False
|
|
200
|
-
each_line = tab + each_line
|
|
201
|
-
if each_line.startswith(" Given") or \
|
|
202
|
-
each_line.startswith(" When") or \
|
|
203
|
-
each_line.startswith(" Then"):
|
|
204
|
-
if each_line.startswith(" Then"):
|
|
205
|
-
just_saw_then = True
|
|
206
|
-
each_line = tab + tab + each_line
|
|
207
|
-
|
|
208
|
-
each_line = each_line[:-1]
|
|
209
|
-
debug_loc = each_line.find(behave_debug_info)
|
|
210
|
-
if debug_loc > 0:
|
|
211
|
-
each_line = each_line[0 : debug_loc]
|
|
212
|
-
each_line = each_line.rstrip()
|
|
213
|
-
if "Scenario" in each_line:
|
|
196
|
+
# Extract scenario name for logic lookup
|
|
214
197
|
current_scenario = each_line[18:]
|
|
215
|
-
previous_scenario = current_scenario
|
|
216
198
|
wiki_data.append(" ")
|
|
217
199
|
wiki_data.append(" ")
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
wiki_full_path = Path(wiki).absolute()
|
|
231
|
-
print(f'Wiki Output: {wiki_full_path}\n\n')
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def print_args(args, msg):
|
|
236
|
-
print(msg)
|
|
237
|
-
for each_arg in args:
|
|
238
|
-
print(f' {each_arg}')
|
|
239
|
-
print(" ")
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
@click.group()
|
|
243
|
-
@click.pass_context
|
|
244
|
-
def cli(ctx):
|
|
245
|
-
"""
|
|
246
|
-
Combine behave.log and scenario_logic_logs to create Behave Logic Report
|
|
247
|
-
|
|
248
|
-
"""
|
|
249
|
-
pass
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
@cli.command("run")
|
|
253
|
-
@click.pass_context
|
|
254
|
-
@click.option('--behave_log',
|
|
255
|
-
default=f'logs/behave.log', # cwd set to test/api_logic_server_behave
|
|
256
|
-
# prompt="Log from behave test suite run [behave.log]",
|
|
257
|
-
help="Help")
|
|
258
|
-
@click.option('--scenario_logs',
|
|
259
|
-
default=f'logs/scenario_logic_logs',
|
|
260
|
-
# prompt="Logic Log directory from ",
|
|
261
|
-
help="Help")
|
|
262
|
-
@click.option('--wiki',
|
|
263
|
-
default=f'reports/Behave Logic Report.md',
|
|
264
|
-
# prompt="Log from behave test suite run [api_logic_server_behave]",
|
|
265
|
-
help="Help")
|
|
266
|
-
@click.option('--prepend_wiki',
|
|
267
|
-
default=f'reports/Behave Logic Report Intro micro.md',
|
|
268
|
-
# prompt="Log from behave test suite run [Behave Logic Report Intro]",
|
|
269
|
-
help="Help")
|
|
270
|
-
def run(ctx, behave_log: str, scenario_logs: str, wiki: str, prepend_wiki: str):
|
|
271
|
-
main(behave_log = behave_log, scenario_logs = scenario_logs, wiki = wiki, prepend_wiki = prepend_wiki)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if __name__ == '__main__': # debugger & python command line start here
|
|
275
|
-
# eg: python api_logic_server_cli/cli.py create --project_name=~/Desktop/test_project
|
|
276
|
-
# unix: python api_logic_server_cli/cli.py create --project_name=/home/ApiLogicProject
|
|
277
|
-
|
|
278
|
-
print(f'\nBehave Logic Report 1.1, started at {os.getcwd()}')
|
|
279
|
-
commands = sys.argv
|
|
280
|
-
if len(sys.argv) > 1:
|
|
281
|
-
print_args(commands, f'\n\nCommand Line Arguments:')
|
|
282
|
-
cli()
|
|
200
|
+
# Remove the debug info (# features/...) from the scenario name
|
|
201
|
+
debug_loc = current_scenario.find(behave_debug_info)
|
|
202
|
+
if debug_loc > 0:
|
|
203
|
+
current_scenario = current_scenario[0:debug_loc].strip()
|
|
204
|
+
wiki_data.append(" ")
|
|
205
|
+
wiki_data.append(" ")
|
|
206
|
+
# Remove debug info from header line too
|
|
207
|
+
header_line = each_line[2:]
|
|
208
|
+
debug_loc = header_line.find(behave_debug_info)
|
|
209
|
+
if debug_loc > 0:
|
|
210
|
+
header_line = header_line[0:debug_loc].rstrip()
|
|
211
|
+
wiki_data.append("### " + header_line) # Add scenario header
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
import ast
|
|
5
|
+
import sys
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Creates wiki file from test/behave/behave.log, with rule use.
|
|
10
|
+
|
|
11
|
+
Tips
|
|
12
|
+
* use 2 spaces (at end) for newline
|
|
13
|
+
* for tab: & emsp;
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
tab = " "
|
|
18
|
+
behave_debug_info = " # "
|
|
19
|
+
wiki_data = []
|
|
20
|
+
debug_scenario = "XXGood Order Custom Service"
|
|
21
|
+
|
|
22
|
+
scenario_doc_strings = {}
|
|
23
|
+
""" dict of scenario_name, array of strings """
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remove_trailer(line: str) -> str:
|
|
27
|
+
""" remove everything after the ## """
|
|
28
|
+
end_here = line.find("\t\t##")
|
|
29
|
+
result = line[0:end_here]
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
def line_spacer():
|
|
33
|
+
wiki_data.append("\n")
|
|
34
|
+
wiki_data.append(" ")
|
|
35
|
+
wiki_data.append(" ")
|
|
36
|
+
wiki_data.append("\n")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_current_readme(prepend_wiki: str):
|
|
40
|
+
""" initialize wiki_data with readme up to {report_name} """
|
|
41
|
+
report_name = "Behave Logic Report"
|
|
42
|
+
with open(prepend_wiki) as readme:
|
|
43
|
+
readme_lines = readme.readlines()
|
|
44
|
+
need_spacer = True
|
|
45
|
+
for each_readme_line in readme_lines:
|
|
46
|
+
if '# ' + report_name in each_readme_line:
|
|
47
|
+
need_spacer = False
|
|
48
|
+
break
|
|
49
|
+
wiki_data.append(each_readme_line[0:-1])
|
|
50
|
+
if need_spacer:
|
|
51
|
+
line_spacer()
|
|
52
|
+
wiki_data.append(f'# {report_name}')
|
|
53
|
+
|
|
54
|
+
def get_truncated_scenario_name(scenario_name: str) -> str:
|
|
55
|
+
""" address max file length (chop at 26), illegal characters """
|
|
56
|
+
scenario_trunc = scenario_name
|
|
57
|
+
if scenario_trunc is not None and len(scenario_trunc) >= 26:
|
|
58
|
+
scenario_trunc = scenario_name[0:25]
|
|
59
|
+
scenario_trunc = f'{str(scenario_trunc).replace(" ", "_")}'
|
|
60
|
+
return scenario_trunc
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def show_logic(scenario: str, logic_logs_dir: str):
|
|
64
|
+
""" insert s{logic_logs_dir}/scenario.log into wiki_data as disclosure area """
|
|
65
|
+
scenario_trunc = get_truncated_scenario_name(scenario)
|
|
66
|
+
logic_file_name = f'{logic_logs_dir}/{scenario_trunc}.log'
|
|
67
|
+
logic_file_name_path = Path(logic_file_name)
|
|
68
|
+
if not logic_file_name_path.is_file(): # debug code
|
|
69
|
+
# wiki_data.append(f'unable to find Logic Log file: {logic_file_name}')
|
|
70
|
+
if scenario == debug_scenario:
|
|
71
|
+
print(f'RELATIVE: {logic_file_name} in {os.getcwd()}')
|
|
72
|
+
full_name = f'{os.getcwd()}/{logic_file_name}'
|
|
73
|
+
print(f'..FULL: {os.getcwd()}/{logic_file_name}')
|
|
74
|
+
logic_file_name = '{logic_logs_dir}/test.log'
|
|
75
|
+
with open(logic_file_name) as logic:
|
|
76
|
+
logic_lines = logic.readlines()
|
|
77
|
+
else:
|
|
78
|
+
logic_log = []
|
|
79
|
+
rules_used = []
|
|
80
|
+
wiki_data.append("<details markdown>")
|
|
81
|
+
wiki_data.append("<summary>Tests - and their logic - are transparent.. click to see Logic</summary>")
|
|
82
|
+
line_spacer()
|
|
83
|
+
scenario_trunc = get_truncated_scenario_name(scenario)
|
|
84
|
+
if scenario_trunc in scenario_doc_strings:
|
|
85
|
+
wiki_data.append(f'**Logic Doc** for scenario: {scenario}')
|
|
86
|
+
wiki_data.append(" ")
|
|
87
|
+
for each_doc_string_line in scenario_doc_strings[scenario_trunc]:
|
|
88
|
+
wiki_data.append(each_doc_string_line[0: -1])
|
|
89
|
+
line_spacer()
|
|
90
|
+
wiki_data.append(f'**Rules Used** in Scenario: {scenario}')
|
|
91
|
+
wiki_data.append("```")
|
|
92
|
+
with open(logic_file_name) as logic:
|
|
93
|
+
logic_lines = logic.readlines()
|
|
94
|
+
is_logic_log = True
|
|
95
|
+
last_rules_start = -1
|
|
96
|
+
last_rules_end = -1
|
|
97
|
+
|
|
98
|
+
# First, find the LAST "These Rules Fired" section
|
|
99
|
+
for i, each_logic_line in enumerate(logic_lines):
|
|
100
|
+
if "These Rules Fired" in each_logic_line:
|
|
101
|
+
last_rules_start = i + 1 # Start collecting from next line
|
|
102
|
+
last_rules_end = -1 # Reset end marker to find the next COMPLETE
|
|
103
|
+
elif last_rules_start > 0 and last_rules_end == -1:
|
|
104
|
+
if 'Logic Phase:' in each_logic_line and 'COMPLETE' in each_logic_line:
|
|
105
|
+
last_rules_end = i
|
|
106
|
+
|
|
107
|
+
# Now process the file, collecting logic log and extracting the last rules section
|
|
108
|
+
for i, each_logic_line in enumerate(logic_lines):
|
|
109
|
+
each_logic_line = remove_trailer(each_logic_line)
|
|
110
|
+
|
|
111
|
+
if is_logic_log:
|
|
112
|
+
if "These Rules Fired" in each_logic_line:
|
|
113
|
+
is_logic_log = False
|
|
114
|
+
else:
|
|
115
|
+
logic_log.append(each_logic_line)
|
|
116
|
+
|
|
117
|
+
# Extract rules from the last "These Rules Fired" section
|
|
118
|
+
if last_rules_start <= i < last_rules_end:
|
|
119
|
+
# Skip empty lines
|
|
120
|
+
if each_logic_line.strip():
|
|
121
|
+
wiki_data.append(each_logic_line + " ")
|
|
122
|
+
|
|
123
|
+
wiki_data.append("```")
|
|
124
|
+
wiki_data.append(f'**Logic Log** in Scenario: {scenario}')
|
|
125
|
+
wiki_data.append("```")
|
|
126
|
+
for each_logic_log in logic_log:
|
|
127
|
+
each_line = remove_trailer(each_logic_log)
|
|
128
|
+
wiki_data.append(each_line)
|
|
129
|
+
wiki_data.append("```")
|
|
130
|
+
wiki_data.append("</details>")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_docStrings(steps_dir: str):
|
|
134
|
+
steps_dir_files = os.listdir(steps_dir)
|
|
135
|
+
indent = 4 # skip leading blanks
|
|
136
|
+
for each_steps_dir_file in steps_dir_files:
|
|
137
|
+
each_steps_dir_file_path = Path(steps_dir).joinpath(each_steps_dir_file)
|
|
138
|
+
if each_steps_dir_file_path.is_file():
|
|
139
|
+
with open(each_steps_dir_file_path) as f:
|
|
140
|
+
step_code = f.readlines()
|
|
141
|
+
# print(f'Found File: {str(each_steps_dir_file_path)}')
|
|
142
|
+
for index, each_step_code_line in enumerate(step_code):
|
|
143
|
+
if each_step_code_line.startswith('@when'):
|
|
144
|
+
comment_start = index + 2
|
|
145
|
+
if '"""' in step_code[comment_start]:
|
|
146
|
+
# print(".. found doc string")
|
|
147
|
+
doc_string_line = comment_start+1
|
|
148
|
+
doc_string = []
|
|
149
|
+
while (True):
|
|
150
|
+
if '"""' in step_code[doc_string_line]:
|
|
151
|
+
break
|
|
152
|
+
doc_string.append(step_code[doc_string_line][indent:])
|
|
153
|
+
doc_string_line += 1
|
|
154
|
+
scenario_line = doc_string_line+1
|
|
155
|
+
if 'scenario_name' not in step_code[scenario_line]:
|
|
156
|
+
print(f'\n** Warning - scenario_name not found '\
|
|
157
|
+
f'in file {str(each_steps_dir_file_path)}, '\
|
|
158
|
+
f'after line {scenario_line} -- skipped')
|
|
159
|
+
else:
|
|
160
|
+
scenario_code_line = step_code[scenario_line]
|
|
161
|
+
scenario_name_start = scenario_code_line.find("'") + 1
|
|
162
|
+
scenario_name_end = scenario_code_line[scenario_name_start+1:].find("'")
|
|
163
|
+
scenario_name = scenario_code_line[scenario_name_start:
|
|
164
|
+
scenario_name_end + scenario_name_start+1]
|
|
165
|
+
if scenario_name == debug_scenario:
|
|
166
|
+
print(f'got {debug_scenario}')
|
|
167
|
+
scenario_trunc = get_truncated_scenario_name(scenario_name)
|
|
168
|
+
# print(f'.... truncated scenario_name: {scenario_trunc} in {scenario_code_line}')
|
|
169
|
+
scenario_doc_strings[scenario_trunc] = doc_string
|
|
170
|
+
# print("that's all, folks")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main(behave_log: str, scenario_logs: str, wiki: str, prepend_wiki: str):
|
|
174
|
+
""" main driver """
|
|
175
|
+
get_docStrings(steps_dir="features/steps")
|
|
176
|
+
|
|
177
|
+
get_current_readme(prepend_wiki=prepend_wiki)
|
|
178
|
+
|
|
179
|
+
contents = None
|
|
180
|
+
with open(behave_log) as f:
|
|
181
|
+
contents = f.readlines()
|
|
182
|
+
|
|
183
|
+
just_saw_then = False
|
|
184
|
+
current_scenario = ""
|
|
185
|
+
previous_scenario = ""
|
|
186
|
+
for each_line in contents:
|
|
187
|
+
if just_saw_then and each_line == "\n":
|
|
188
|
+
show_logic(scenario=current_scenario, logic_logs_dir=scenario_logs)
|
|
189
|
+
just_saw_then = False
|
|
190
|
+
previous_scenario = ""
|
|
191
|
+
if each_line.startswith("Feature"):
|
|
192
|
+
wiki_data.append(" ")
|
|
193
|
+
wiki_data.append(" ")
|
|
194
|
+
each_line = "## " + each_line
|
|
195
|
+
if each_line.startswith(" Scenario"):
|
|
196
|
+
# Before starting new scenario, show logic for previous one if we saw Then
|
|
197
|
+
if just_saw_then and previous_scenario:
|
|
198
|
+
show_logic(scenario=previous_scenario, logic_logs_dir=scenario_logs)
|
|
199
|
+
just_saw_then = False
|
|
200
|
+
each_line = tab + each_line
|
|
201
|
+
if each_line.startswith(" Given") or \
|
|
202
|
+
each_line.startswith(" When") or \
|
|
203
|
+
each_line.startswith(" Then"):
|
|
204
|
+
if each_line.startswith(" Then"):
|
|
205
|
+
just_saw_then = True
|
|
206
|
+
each_line = tab + tab + each_line
|
|
207
|
+
|
|
208
|
+
each_line = each_line[:-1]
|
|
209
|
+
debug_loc = each_line.find(behave_debug_info)
|
|
210
|
+
if debug_loc > 0:
|
|
211
|
+
each_line = each_line[0 : debug_loc]
|
|
212
|
+
each_line = each_line.rstrip()
|
|
213
|
+
if "Scenario" in each_line:
|
|
214
|
+
current_scenario = each_line[18:]
|
|
215
|
+
previous_scenario = current_scenario
|
|
216
|
+
wiki_data.append(" ")
|
|
217
|
+
wiki_data.append(" ")
|
|
218
|
+
wiki_data.append("### " + each_line[8:])
|
|
219
|
+
|
|
220
|
+
each_line = each_line + " " # wiki for "new line"
|
|
221
|
+
|
|
222
|
+
wiki_data.append(each_line)
|
|
223
|
+
|
|
224
|
+
# Show logic for the last scenario if we saw Then
|
|
225
|
+
if just_saw_then and current_scenario:
|
|
226
|
+
show_logic(scenario=current_scenario, logic_logs_dir=scenario_logs)
|
|
227
|
+
|
|
228
|
+
with open(wiki, 'w') as rpt:
|
|
229
|
+
rpt.write('\n'.join(wiki_data))
|
|
230
|
+
wiki_full_path = Path(wiki).absolute()
|
|
231
|
+
print(f'Wiki Output: {wiki_full_path}\n\n')
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def print_args(args, msg):
|
|
236
|
+
print(msg)
|
|
237
|
+
for each_arg in args:
|
|
238
|
+
print(f' {each_arg}')
|
|
239
|
+
print(" ")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@click.group()
|
|
243
|
+
@click.pass_context
|
|
244
|
+
def cli(ctx):
|
|
245
|
+
"""
|
|
246
|
+
Combine behave.log and scenario_logic_logs to create Behave Logic Report
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@cli.command("run")
|
|
253
|
+
@click.pass_context
|
|
254
|
+
@click.option('--behave_log',
|
|
255
|
+
default=f'logs/behave.log', # cwd set to test/api_logic_server_behave
|
|
256
|
+
# prompt="Log from behave test suite run [behave.log]",
|
|
257
|
+
help="Help")
|
|
258
|
+
@click.option('--scenario_logs',
|
|
259
|
+
default=f'logs/scenario_logic_logs',
|
|
260
|
+
# prompt="Logic Log directory from ",
|
|
261
|
+
help="Help")
|
|
262
|
+
@click.option('--wiki',
|
|
263
|
+
default=f'reports/Behave Logic Report.md',
|
|
264
|
+
# prompt="Log from behave test suite run [api_logic_server_behave]",
|
|
265
|
+
help="Help")
|
|
266
|
+
@click.option('--prepend_wiki',
|
|
267
|
+
default=f'reports/Behave Logic Report Intro micro.md',
|
|
268
|
+
# prompt="Log from behave test suite run [Behave Logic Report Intro]",
|
|
269
|
+
help="Help")
|
|
270
|
+
def run(ctx, behave_log: str, scenario_logs: str, wiki: str, prepend_wiki: str):
|
|
271
|
+
main(behave_log = behave_log, scenario_logs = scenario_logs, wiki = wiki, prepend_wiki = prepend_wiki)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == '__main__': # debugger & python command line start here
|
|
275
|
+
# eg: python api_logic_server_cli/cli.py create --project_name=~/Desktop/test_project
|
|
276
|
+
# unix: python api_logic_server_cli/cli.py create --project_name=/home/ApiLogicProject
|
|
277
|
+
|
|
278
|
+
print(f'\nBehave Logic Report 1.1, started at {os.getcwd()}')
|
|
279
|
+
commands = sys.argv
|
|
280
|
+
if len(sys.argv) > 1:
|
|
281
|
+
print_args(commands, f'\n\nCommand Line Arguments:')
|
|
282
|
+
cli()
|