drun 7.2.6__tar.gz → 7.2.7__tar.gz

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.
Files changed (91) hide show
  1. {drun-7.2.6 → drun-7.2.7}/PKG-INFO +226 -28
  2. {drun-7.2.6 → drun-7.2.7}/README.md +225 -27
  3. {drun-7.2.6 → drun-7.2.7}/drun/__init__.py +1 -1
  4. {drun-7.2.6 → drun-7.2.7}/drun/cli.py +1 -2
  5. {drun-7.2.6 → drun-7.2.7}/drun/runner/runner.py +138 -6
  6. {drun-7.2.6 → drun-7.2.7}/drun.egg-info/PKG-INFO +226 -28
  7. {drun-7.2.6 → drun-7.2.7}/pyproject.toml +1 -1
  8. {drun-7.2.6 → drun-7.2.7}/tests/test_cli_help_width.py +16 -2
  9. {drun-7.2.6 → drun-7.2.7}/tests/test_repeat_steps.py +107 -0
  10. {drun-7.2.6 → drun-7.2.7}/LICENSE +0 -0
  11. {drun-7.2.6 → drun-7.2.7}/drun/commands/__init__.py +0 -0
  12. {drun-7.2.6 → drun-7.2.7}/drun/commands/check.py +0 -0
  13. {drun-7.2.6 → drun-7.2.7}/drun/commands/fix.py +0 -0
  14. {drun-7.2.6 → drun-7.2.7}/drun/commands/run.py +0 -0
  15. {drun-7.2.6 → drun-7.2.7}/drun/commands/tags.py +0 -0
  16. {drun-7.2.6 → drun-7.2.7}/drun/db/__init__.py +0 -0
  17. {drun-7.2.6 → drun-7.2.7}/drun/db/database_proxy.py +0 -0
  18. {drun-7.2.6 → drun-7.2.7}/drun/db/generate_mysql_config.py +0 -0
  19. {drun-7.2.6 → drun-7.2.7}/drun/engine/__init__.py +0 -0
  20. {drun-7.2.6 → drun-7.2.7}/drun/engine/http.py +0 -0
  21. {drun-7.2.6 → drun-7.2.7}/drun/engine/request_files.py +0 -0
  22. {drun-7.2.6 → drun-7.2.7}/drun/exporters/curl.py +0 -0
  23. {drun-7.2.6 → drun-7.2.7}/drun/exporters/snippet.py +0 -0
  24. {drun-7.2.6 → drun-7.2.7}/drun/extensions.py +0 -0
  25. {drun-7.2.6 → drun-7.2.7}/drun/importers/base.py +0 -0
  26. {drun-7.2.6 → drun-7.2.7}/drun/importers/curl.py +0 -0
  27. {drun-7.2.6 → drun-7.2.7}/drun/importers/har.py +0 -0
  28. {drun-7.2.6 → drun-7.2.7}/drun/importers/openapi.py +0 -0
  29. {drun-7.2.6 → drun-7.2.7}/drun/importers/postman.py +0 -0
  30. {drun-7.2.6 → drun-7.2.7}/drun/loader/__init__.py +0 -0
  31. {drun-7.2.6 → drun-7.2.7}/drun/loader/collector.py +0 -0
  32. {drun-7.2.6 → drun-7.2.7}/drun/loader/env.py +0 -0
  33. {drun-7.2.6 → drun-7.2.7}/drun/loader/hooks.py +0 -0
  34. {drun-7.2.6 → drun-7.2.7}/drun/loader/yaml_loader.py +0 -0
  35. {drun-7.2.6 → drun-7.2.7}/drun/models/case.py +0 -0
  36. {drun-7.2.6 → drun-7.2.7}/drun/models/config.py +0 -0
  37. {drun-7.2.6 → drun-7.2.7}/drun/models/report.py +0 -0
  38. {drun-7.2.6 → drun-7.2.7}/drun/models/request.py +0 -0
  39. {drun-7.2.6 → drun-7.2.7}/drun/models/step.py +0 -0
  40. {drun-7.2.6 → drun-7.2.7}/drun/models/validators.py +0 -0
  41. {drun-7.2.6 → drun-7.2.7}/drun/notifier/__init__.py +0 -0
  42. {drun-7.2.6 → drun-7.2.7}/drun/notifier/base.py +0 -0
  43. {drun-7.2.6 → drun-7.2.7}/drun/notifier/dingtalk.py +0 -0
  44. {drun-7.2.6 → drun-7.2.7}/drun/notifier/emailer.py +0 -0
  45. {drun-7.2.6 → drun-7.2.7}/drun/notifier/feishu.py +0 -0
  46. {drun-7.2.6 → drun-7.2.7}/drun/notifier/format.py +0 -0
  47. {drun-7.2.6 → drun-7.2.7}/drun/reporter/__init__.py +0 -0
  48. {drun-7.2.6 → drun-7.2.7}/drun/reporter/allure_reporter.py +0 -0
  49. {drun-7.2.6 → drun-7.2.7}/drun/reporter/html_reporter.py +0 -0
  50. {drun-7.2.6 → drun-7.2.7}/drun/reporter/json_reporter.py +0 -0
  51. {drun-7.2.6 → drun-7.2.7}/drun/runner/__init__.py +0 -0
  52. {drun-7.2.6 → drun-7.2.7}/drun/runner/asserting.py +0 -0
  53. {drun-7.2.6 → drun-7.2.7}/drun/runner/assertions.py +0 -0
  54. {drun-7.2.6 → drun-7.2.7}/drun/runner/extractors.py +0 -0
  55. {drun-7.2.6 → drun-7.2.7}/drun/runner/hooks.py +0 -0
  56. {drun-7.2.6 → drun-7.2.7}/drun/runner/invoke.py +0 -0
  57. {drun-7.2.6 → drun-7.2.7}/drun/scaffolds/__init__.py +0 -0
  58. {drun-7.2.6 → drun-7.2.7}/drun/scaffolds/templates.py +0 -0
  59. {drun-7.2.6 → drun-7.2.7}/drun/server/__init__.py +0 -0
  60. {drun-7.2.6 → drun-7.2.7}/drun/server/app.py +0 -0
  61. {drun-7.2.6 → drun-7.2.7}/drun/server/database.py +0 -0
  62. {drun-7.2.6 → drun-7.2.7}/drun/server/scanner.py +0 -0
  63. {drun-7.2.6 → drun-7.2.7}/drun/server/services.py +0 -0
  64. {drun-7.2.6 → drun-7.2.7}/drun/server/templates/detail.html +0 -0
  65. {drun-7.2.6 → drun-7.2.7}/drun/server/templates/index.html +0 -0
  66. {drun-7.2.6 → drun-7.2.7}/drun/templating/__init__.py +0 -0
  67. {drun-7.2.6 → drun-7.2.7}/drun/templating/builtins.py +0 -0
  68. {drun-7.2.6 → drun-7.2.7}/drun/templating/compat.py +0 -0
  69. {drun-7.2.6 → drun-7.2.7}/drun/templating/context.py +0 -0
  70. {drun-7.2.6 → drun-7.2.7}/drun/templating/engine.py +0 -0
  71. {drun-7.2.6 → drun-7.2.7}/drun/utils/__init__.py +0 -0
  72. {drun-7.2.6 → drun-7.2.7}/drun/utils/config.py +0 -0
  73. {drun-7.2.6 → drun-7.2.7}/drun/utils/curl.py +0 -0
  74. {drun-7.2.6 → drun-7.2.7}/drun/utils/data_exporter.py +0 -0
  75. {drun-7.2.6 → drun-7.2.7}/drun/utils/env_writer.py +0 -0
  76. {drun-7.2.6 → drun-7.2.7}/drun/utils/errors.py +0 -0
  77. {drun-7.2.6 → drun-7.2.7}/drun/utils/logging.py +0 -0
  78. {drun-7.2.6 → drun-7.2.7}/drun/utils/mask.py +0 -0
  79. {drun-7.2.6 → drun-7.2.7}/drun/utils/timeit.py +0 -0
  80. {drun-7.2.6 → drun-7.2.7}/drun.egg-info/SOURCES.txt +0 -0
  81. {drun-7.2.6 → drun-7.2.7}/drun.egg-info/dependency_links.txt +0 -0
  82. {drun-7.2.6 → drun-7.2.7}/drun.egg-info/entry_points.txt +0 -0
  83. {drun-7.2.6 → drun-7.2.7}/drun.egg-info/requires.txt +0 -0
  84. {drun-7.2.6 → drun-7.2.7}/drun.egg-info/top_level.txt +0 -0
  85. {drun-7.2.6 → drun-7.2.7}/setup.cfg +0 -0
  86. {drun-7.2.6 → drun-7.2.7}/tests/test_binary_response.py +0 -0
  87. {drun-7.2.6 → drun-7.2.7}/tests/test_invoke_case_selection.py +0 -0
  88. {drun-7.2.6 → drun-7.2.7}/tests/test_request_files.py +0 -0
  89. {drun-7.2.6 → drun-7.2.7}/tests/test_run_env.py +0 -0
  90. {drun-7.2.6 → drun-7.2.7}/tests/test_run_outputs.py +0 -0
  91. {drun-7.2.6 → drun-7.2.7}/tests/test_template_engine.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drun
3
- Version: 7.2.6
3
+ Version: 7.2.7
4
4
  Summary: Easy-to-use API testing with DevOps automation support
5
5
  Author: Drun Team
6
6
  Requires-Python: >=3.10
@@ -22,7 +22,7 @@ Dynamic: license-file
22
22
 
23
23
  # Drun — Modern HTTP API Testing Framework
24
24
 
25
- [![Version](https://img.shields.io/badge/version-7.1.3-blue.svg)](https://github.com/Devliang24/drun)
25
+ [![Version](https://img.shields.io/badge/version-7.2.7-blue.svg)](https://github.com/Devliang24/drun)
26
26
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
27
27
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
28
28
 
@@ -48,7 +48,7 @@ Dynamic: license-file
48
48
  - **Streaming Support**: SSE (Server-Sent Events) with per-event assertions
49
49
  - **File Uploads**: Multipart/form-data support
50
50
  - **Smart File Discovery**: Run tests without `.yaml` extension
51
- - **Test Case Invoke**: Nested test case calls with variable passing (NEW in v6.2)
51
+ - **Test Case Invoke**: Nested test case calls with variable passing
52
52
 
53
53
  ### Variable Management
54
54
  - **Auto-Persist**: Extracted variables automatically saved to `.env`
@@ -61,7 +61,7 @@ Dynamic: license-file
61
61
  - **Test Suites**: Ordered execution with variable chaining and caseflow
62
62
  - **Authentication**: Basic/Bearer auth with auto-injection
63
63
  - **Tag Filtering**: Boolean expressions like `smoke and not slow`
64
- - **Database Assertions**: MySQL integration for data validation
64
+ - **Database Validation via Hooks**: Integrate DB checks through custom hook functions
65
65
 
66
66
  ### Reports & Integrations
67
67
  - **HTML Reports**: Single-file, shareable test reports
@@ -181,6 +181,10 @@ steps:
181
181
  drun run testcases/test_user_api.yaml -env dev
182
182
  drun run test_user_api -env dev
183
183
 
184
+ # Run specific case(s) from a file
185
+ drun run "test_channel_token_flow:获取渠道 token" -env dev
186
+ drun run "test_channel_token_flow:获取渠道 token,刷新渠道 token" -env dev
187
+
184
188
  # Run with HTML report
185
189
  drun run test_user_api -env dev -html reports/report.html
186
190
 
@@ -191,8 +195,55 @@ drun run testcases -env dev -k "smoke and not slow"
191
195
  drun run testsuite_e2e -env dev
192
196
  ```
193
197
 
198
+ By default:
199
+ - Temporary single-file run (outside scaffold) writes only one log file in current directory
200
+ - Scaffold project run keeps default outputs in `logs/`, `reports/`, `snippets/`
201
+
194
202
  ## Core Concepts
195
203
 
204
+ ### YAML Case Authoring Quick Reference
205
+
206
+ Use one of these two file shapes:
207
+
208
+ **1) Single case file (`config + steps`)**
209
+
210
+ ```yaml
211
+ config:
212
+ name: Login API
213
+ base_url: ${ENV(BASE_URL)}
214
+
215
+ steps:
216
+ - name: Login
217
+ request:
218
+ method: POST
219
+ path: /login
220
+ body:
221
+ username: admin
222
+ password: pass123
223
+ extract:
224
+ token: $.data.token
225
+ validate:
226
+ - eq: [status_code, 200]
227
+ ```
228
+
229
+ **2) Suite file (`config + caseflow`)**
230
+
231
+ ```yaml
232
+ config:
233
+ name: Smoke Suite
234
+
235
+ caseflow:
236
+ - name: Login
237
+ invoke: test_login
238
+ - name: User Profile
239
+ invoke: test_profile
240
+ ```
241
+
242
+ Use this with CLI:
243
+ - `drun run testcases/test_login.yaml -env dev`
244
+ - `drun run testsuites/testsuite_smoke.yaml -env dev`
245
+ - `drun run "test_channel_token_flow:获取渠道 token" -env dev`
246
+
196
247
  ### Test Case Structure
197
248
 
198
249
  ```yaml
@@ -233,6 +284,24 @@ steps:
233
284
  - ${cleanup_function()}
234
285
  ```
235
286
 
287
+ ### Step Syntax Matrix (Writing + Constraints)
288
+
289
+ | Field | Example | Notes |
290
+ | --- | --- | --- |
291
+ | `request.method` / `request.path` | `method: POST` / `path: /users` | Required for request steps |
292
+ | `request.headers` / `request.params` | `headers: {Authorization: Bearer $token}` | Key-value maps |
293
+ | `request.body` | `body: {name: alice}` | JSON-style request body |
294
+ | `request.data` | `data: {model: sensevoice}` | Form fields, commonly with multipart |
295
+ | `request.files` | `files: {file: "data/a.wav"}` or `files: {file: ["data/a.wav", "audio/x-wav"]}` | Supported upload forms; prefer `data + files` for multipart |
296
+ | `request.stream` / `request.stream_timeout` | `stream: true` / `stream_timeout: 30` | For SSE/streaming APIs |
297
+ | `extract` | `extract: {token: $.data.token}` | Extract variables for later steps/cases |
298
+ | `validate` | `- eq: [status_code, 200]` | Assertions on status/body/variables |
299
+ | `setup_hooks` / `teardown_hooks` | `setup_hooks: [${sign($request)}]` | Run Python hooks before/after step |
300
+ | `response.save_body_to` | `response: {save_body_to: artifacts/out.mp3}` | Save binary/non-JSON response to file |
301
+ | `skip` | `skip: true` / `skip: "maintenance"` / `skip: ${remain_quota <= 0}` | Supports bool, reason string, or expression (evaluated per iteration with `repeat`) |
302
+ | `repeat` | `repeat: 3` or `repeat: ${retry_count}` | Must resolve to integer `>= 0`; `0` means skipped |
303
+ | `invoke_case_name` / `invoke_case_names` | `invoke_case_name: 获取渠道 token` | Caseflow-only filter by exact `config.name`; two fields are mutually exclusive |
304
+
236
305
  ### Variable Extraction & Auto-Persist
237
306
 
238
307
  **Extraction automatically persists to environment:**
@@ -267,7 +336,7 @@ steps:
267
336
  user_id: ${ENV(USER_ID)} # Uses extracted userId
268
337
  ```
269
338
 
270
- ### Test Suites & Caseflow (NEW in v6.2)
339
+ ### Test Suites & Caseflow
271
340
 
272
341
  **Modern caseflow syntax with invoke:**
273
342
 
@@ -296,30 +365,79 @@ caseflow:
296
365
  variables:
297
366
  order_id: $orderId
298
367
  invoke: test_verify
299
- ```
300
368
 
301
- > **Note**: In caseflow, `variables` comes before `invoke`. Extracted variables are automatically exported to subsequent steps - no explicit `export` needed.
302
-
303
- **Legacy testcases syntax (still supported):**
304
-
305
- ```yaml
306
- config:
307
- name: E2E Test Flow
308
- base_url: ${ENV(BASE_URL)}
369
+ - name: Invoke specific cases in file
370
+ invoke: test_channel_token_flow
371
+ invoke_case_names:
372
+ - 获取渠道 token
373
+ - 刷新渠道 token
309
374
 
310
- testcases:
311
- - testcases/test_login.yaml
312
- - testcases/test_create_order.yaml
313
- - testcases/test_payment.yaml
314
- - testcases/test_verify.yaml
375
+ - name: Repeat invoke step
376
+ invoke: test_channel_token_flow
377
+ invoke_case_name: 获取渠道 token
378
+ repeat: 2
315
379
  ```
316
380
 
381
+ > **Note**: In caseflow, `variables` comes before `invoke`. Extracted variables are automatically exported to subsequent steps - no explicit `export` needed.
382
+ > `invoke_case_name` and `invoke_case_names` are mutually exclusive. Matching is exact by `config.name`.
383
+ > `invoke_case_name` selects one case; `invoke_case_names` selects multiple cases. Matched cases run in invoked-file order.
384
+
317
385
  **Execution characteristics:**
318
386
  - Strict sequential order (top to bottom)
319
387
  - Variables shared via memory (no file I/O between tests)
320
388
  - `.env` file read once at startup
321
389
  - Variables extracted during run are persisted to `.env`
322
390
 
391
+ ### Step Repeat (repeat)
392
+
393
+ Run the same step multiple times:
394
+
395
+ ```yaml
396
+ steps:
397
+ - name: Upload Audio
398
+ repeat: 3
399
+ request:
400
+ method: POST
401
+ path: /asr/upload
402
+ files:
403
+ file: data/audio.wav
404
+ ```
405
+
406
+ `repeat` also supports expressions:
407
+
408
+ ```yaml
409
+ steps:
410
+ - name: Health Check
411
+ repeat: ${retry_count}
412
+ request:
413
+ method: GET
414
+ path: /health
415
+ ```
416
+
417
+ Behavior:
418
+ - `repeat` must resolve to an integer `>= 0`
419
+ - `repeat: 0` marks the step as skipped (no request sent)
420
+ - Logs and reports show iteration suffix like `[repeat=2/5]`
421
+
422
+ ### Conditional Repeat (with skip)
423
+
424
+ Use `skip` with `repeat` when each iteration should decide dynamically whether to run:
425
+
426
+ ```yaml
427
+ steps:
428
+ - name: Retry until quota exhausted
429
+ repeat: 10
430
+ skip: ${remain_quota <= 0}
431
+ request:
432
+ method: POST
433
+ path: /quota/check
434
+ ```
435
+
436
+ Rules:
437
+ - `skip` supports `true/false`, reason strings, and `${...}` expressions
438
+ - With `repeat`, `skip` is evaluated per iteration (after `$repeat_index/$repeat_no` are injected)
439
+ - If a `skip` expression fails to evaluate, the iteration is marked as failed with `skip error`
440
+
323
441
  ### Template System
324
442
 
325
443
  **Dollar-style syntax:**
@@ -531,6 +649,16 @@ steps:
531
649
 
532
650
  ## CLI Reference
533
651
 
652
+ CLI commands are entry points. YAML authoring syntax is documented in:
653
+ - [YAML Case Authoring Quick Reference](#yaml-case-authoring-quick-reference)
654
+ - [Step Syntax Matrix (Writing + Constraints)](#step-syntax-matrix-writing--constraints)
655
+ - [Test Suites & Caseflow](#test-suites--caseflow)
656
+ - [Step Repeat (repeat)](#step-repeat-repeat)
657
+ - [Conditional Repeat (with skip)](#conditional-repeat-with-skip)
658
+ - [File Upload Testing](#file-upload-testing)
659
+ - [Binary Response Save](#binary-response-save)
660
+ - [Quick Request Debug (`drun q`)](#quick-request-debug-drun-q)
661
+
534
662
  ### Web Report Server
535
663
 
536
664
  ```bash
@@ -540,6 +668,9 @@ drun server
540
668
  # Custom port and options
541
669
  drun server -port 8080 -headless
542
670
 
671
+ # Bind host and custom reports directory
672
+ drun server -host 127.0.0.1 -reports-dir reports
673
+
543
674
  # Development mode with auto-reload
544
675
  drun server -reload
545
676
 
@@ -559,6 +690,9 @@ drun server -reload
559
690
  # Basic execution
560
691
  drun run PATH -env <env_name>
561
692
 
693
+ # PATH supports case selector: <path>:<case[,case]>
694
+ drun run "test_channel_token_flow:获取渠道 token" -env dev
695
+
562
696
  # Smart file discovery - extension optional
563
697
  drun run test_api_health -env dev # Finds test_api_health.yaml or .yml
564
698
  drun run testcases/test_user -env dev # Supports paths without extension
@@ -568,6 +702,10 @@ drun run test_api_health.yaml -env dev # Traditional format still works
568
702
  # Default output: only one log file in current directory
569
703
  drun run ./test_api_health.yaml -env-file ./demo.env
570
704
 
705
+ # Scaffold project run keeps default outputs:
706
+ # logs/run-<ts>.log, reports/report-<ts>.html, snippets/<ts>/
707
+ drun run testcases/test_api_health.yaml -env dev
708
+
571
709
  # With more options
572
710
  drun run testcases/ \
573
711
  -env staging \
@@ -580,14 +718,18 @@ drun run testcases/ \
580
718
  -failfast
581
719
  ```
582
720
 
721
+ YAML writing details for run targets are in the sections above, especially `config + steps`, `config + caseflow`, `repeat`, and invoke case selection.
722
+
583
723
  **Options:**
584
724
  - `-env NAME`: Optional named environment; prefers `.env.<name>` and merges named env config if present
585
725
  - `-env-file FILE`: Explicit environment file path; higher priority than `-env` and default `.env`
726
+ - `-persist-env FILE`: Persist extracted variables to specific env file
586
727
  - `-k TAG_EXPR`: Filter by tags (e.g., `smoke and not slow`)
587
728
  - `-vars k=v`: Override variables from CLI
588
729
  - `-html FILE`: Generate HTML report (temporary single-file runs do not generate one by default)
589
730
  - `-report FILE`: Generate JSON report
590
731
  - `-allure-results DIR`: Generate Allure results
732
+ - `-httpx-logs`: Enable internal httpx request logging
591
733
  - `-secrets mask`: Mask sensitive data in logs/reports
592
734
  - `-secrets plain`: Show sensitive data (default for local runs)
593
735
  - `-response-headers`: Log response headers
@@ -595,10 +737,67 @@ drun run testcases/ \
595
737
  - `-log-level LEVEL`: Set log level (DEBUG, INFO, WARNING, ERROR)
596
738
  - `-log-file FILE`: Write logs to file (temporary single-file runs otherwise default to `./<yaml>-<ts>.log`)
597
739
  - `-notify CHANNELS`: Enable notifications (feishu, dingtalk, email)
598
- - `-notify-only POLICY`: Notification policy (always, failed, passed)
740
+ - `-notify-only POLICY`: Notification policy (`failed` or `always`)
741
+ - `-notify-attach-html`: Attach HTML report in email notifications (when email is enabled)
599
742
  - `-snippet off`: Disable code snippet generation (temporary single-file runs are already disabled by default)
600
743
  - `-snippet-output DIR`: Custom output directory for snippets
601
744
  - `-snippet MODE`: Snippet mode: off|all|curl|python
745
+ - `--help`: Keep double-hyphen for help (`drun run --help`)
746
+
747
+ ### Quick Request Debug (`drun q`)
748
+
749
+ `drun q` is the direct-request mode (curl/httpie-like) for quick API debugging without YAML.
750
+
751
+ Migration note:
752
+ - `drun quick` has been removed.
753
+ - Use `drun q ...` instead.
754
+
755
+ ```bash
756
+ # Basic GET
757
+ drun q https://httpbin.org/get
758
+
759
+ # POST JSON
760
+ drun q https://httpbin.org/post \
761
+ -X POST \
762
+ -H "Content-Type: application/json" \
763
+ -data '{"name":"alice","role":"admin"}'
764
+
765
+ # With query params + headers
766
+ drun q https://httpbin.org/anything \
767
+ -p page=1 \
768
+ -p size=20 \
769
+ -H "X-Trace-Id: demo-001"
770
+
771
+ # Validate response
772
+ drun q https://httpbin.org/status/200 \
773
+ -validate status_code=200
774
+
775
+ # Extract values
776
+ drun q https://httpbin.org/get \
777
+ -extract origin=$.body.origin
778
+
779
+ # Save full response body
780
+ drun q https://httpbin.org/json \
781
+ -output artifacts/httpbin.json
782
+
783
+ # Save as YAML testcase template
784
+ drun q https://httpbin.org/post \
785
+ -X POST \
786
+ -data '{"hello":"world"}' \
787
+ -save-yaml testcases/from_q.yaml \
788
+ -secrets mask
789
+ ```
790
+
791
+ Key options:
792
+ - `-X/-method`: HTTP method
793
+ - `-H/-header`: request header, repeatable
794
+ - `-p/-param`: query param, repeatable
795
+ - `-d/-data` / `-data-file`: request body input
796
+ - `-validate`: assertion expression (repeatable)
797
+ - `-extract`: variable extraction (`name=$expr`)
798
+ - `-o/-output`: write full response body
799
+ - `-save-yaml`: convert current request to YAML testcase
800
+ - `-secrets plain|mask`: output secret mode
602
801
 
603
802
  ### Format Conversion
604
803
 
@@ -997,7 +1196,7 @@ jobs:
997
1196
 
998
1197
  ## Advanced Topics
999
1198
 
1000
- ### Test Case Invoke (NEW in v6.2)
1199
+ ### Test Case Invoke
1001
1200
 
1002
1201
  Call other test cases from within a test case, enabling modular test design:
1003
1202
 
@@ -1113,7 +1312,10 @@ steps:
1113
1312
  - gt: [$event_count, 0]
1114
1313
  ```
1115
1314
 
1116
- ### Database Assertions
1315
+ ### Database Validation via Hooks
1316
+
1317
+ Drun does not provide built-in SQL validators in step fields.
1318
+ Use hook functions to query databases and assert against extracted values.
1117
1319
 
1118
1320
  **Dhook.py:**
1119
1321
  ```python
@@ -1133,10 +1335,6 @@ def setup_hook_assert_sql(hook_ctx, user_id):
1133
1335
  conn.close()
1134
1336
 
1135
1337
  return {'db_status': result[0] if result else None}
1136
-
1137
- def expected_sql_value(user_id):
1138
- """Get expected value from previous query"""
1139
- return hook_ctx.get('db_status')
1140
1338
  ```
1141
1339
 
1142
1340
  **Usage:**
@@ -1150,7 +1348,7 @@ steps:
1150
1348
  path: /users/$user_id
1151
1349
  validate:
1152
1350
  - eq: [status_code, 200]
1153
- - eq: [$.data.status, ${expected_sql_value($user_id)}]
1351
+ - eq: [$.data.status, $db_status]
1154
1352
  ```
1155
1353
 
1156
1354
  ### Performance Testing
@@ -1197,7 +1395,7 @@ python -m drun.cli --version
1197
1395
  - **Language**: Python 3.10+
1198
1396
  - **Lines of Code**: ~11,300
1199
1397
  - **Modules**: ~66 Python files
1200
- - **Automated Tests**: No maintained in-repo test suite during P0; unified tests are planned for P1
1398
+ - **Automated Tests**: In-repo unittest suite is actively maintained (`python -m unittest discover -s tests -p 'test_*.py'`)
1201
1399
  - **Code Style**: PEP 8, type hints, Pydantic models
1202
1400
 
1203
1401
  ## Version History