Skip to content

Commit 92a5192

Browse files
Merge pull request #8 from manuschillerdev/test-parity-ed345d3
Add high-signal test parity coverage
2 parents f062a42 + 607049a commit 92a5192

3 files changed

Lines changed: 1169 additions & 21 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Absurd SQL 0.4.0 test parity
2+
3+
## Purpose
4+
5+
This document tracks high-signal test parity for the Rust SDK against the upstream Absurd `0.4.0` Python, TypeScript, and Go SDK suites.
6+
7+
Coverage parity is not the goal. The goal is confidence in public Rust SDK behavior, durable workflow semantics, and intentional Rust deviations documented in `docs/parity/absurd-sql-0.4.0.md`.
8+
9+
## Target
10+
11+
- Absurd SQL tag: `0.4.0`
12+
- Absurd SQL commit: `05282a40c8dddc378acdc6933adc4c221583808a`
13+
- Rust crate version: `0.1.0`
14+
- Rust test suites:
15+
- unit tests in `src/**`
16+
- ignored database integration suite in `tests/integration.rs`
17+
18+
## Scope
19+
20+
Included:
21+
22+
- public Rust SDK behavior
23+
- database-backed durable semantics surfaced through the SDK
24+
- failure, cancellation, retry, worker, event, checkpoint, and task-result invariants
25+
- Rust-specific diagnostics and intentional API decisions
26+
27+
Excluded:
28+
29+
- duplicate sync/async coverage from Python and TypeScript
30+
- language-specific decorator, contextvar, or global-context behavior
31+
- Go driver matrix tests
32+
- raw claim/execute APIs that Rust intentionally keeps internal
33+
- `absurdctl`, cron, SQL migration, partition maintenance, and storage-engine tests owned by upstream Absurd
34+
- exact traceback-string parity, which Rust intentionally does not implement
35+
36+
## Test parity matrix
37+
38+
| Area | Rust status | Existing Rust evidence | High-signal action |
39+
|---|---|---|---|
40+
| Absurd SQL `0.4.0` schema target | Covered | CI installs upstream SQL; DB integration suite runs against it | None |
41+
| Connection defaults | Partial | `Client::from_env*` exists; no direct Rust test for env precedence | Add unit tests for explicit URL, `ABSURD_DATABASE_URL`, `DATABASE_URL`, `PGDATABASE`, and localhost fallback |
42+
| Queue create, list, drop | Covered | `queue_create_list_drop_round_trip` | None |
43+
| Partitioned queue creation | Covered | `partitioned_queue_create_round_trip` | Optional partitioned idempotency smoke; not required now |
44+
| Queue policy get/set | Covered | `queue_policy_round_trip`; queue policy wire-key unit tests | None |
45+
| Typed task registration and spawn | Covered | `basic_typed_task_round_trip` | None |
46+
| Spawn options | Partial | `spawn_options_use_sql_wire_keys`; header and idempotency integration tests | Add max-attempt override/default persistence; reject unregistered spawn without queue; reject registered queue mismatch |
47+
| Spawn headers | Covered | `spawn_headers_are_available_to_task_context`; hook tests | None |
48+
| Idempotent spawn | Covered | `idempotent_spawn_creates_one_task_and_executes_once` | Optional different-key and queue-scope cases; not required now |
49+
| Retry without explicit strategy | Covered | `failed_task_retries_immediately_until_success` | None |
50+
| Fixed retry strategy | Covered | `fixed_retry_strategy_delays_next_attempt` | None |
51+
| Exponential retry strategy | Missing | wire shape exists through `RetryStrategy`, but no DB behavior test | Add DB integration test using `absurd.fake_now` to validate increasing delay and max cap |
52+
| Explicit none retry strategy | Missing | `RetryStrategy::none()` exists; no DB behavior test | Add DB integration test that `kind: none` is persisted and requeues immediately when attempts remain |
53+
| Cancellation by max delay | Missing | wire serialization through `CancellationPolicy`; no DB behavior test | Add DB integration test where a delayed first claim cancels the task |
54+
| Cancellation by max duration | Missing | wire serialization through `CancellationPolicy`; no DB behavior test | Add DB integration test where retry after elapsed duration cancels the task |
55+
| Default task cancellation | Missing | `task_builder_sets_default_cancellation` only checks builder state | Add DB integration test proving task-level default cancellation is applied when spawn options omit cancellation |
56+
| Worker batch execution | Partial | `basic_typed_task_round_trip`; event batch tests | Add mixed success/failure batch test proving failure of one task does not prevent later claimed tasks from running |
57+
| Continuous worker lifecycle | Partial | `worker_close_waits_for_running_tasks` | Add concurrency-limit test for `start_worker` |
58+
| Worker error callback | Covered | `worker_on_error_reports_claim_failures`; `worker_on_error_reports_execution_failures`; unit callback tests | None |
59+
| Durable `TaskContext::step` checkpoint | Covered | `step_checkpoint_is_reused_after_retry`; `failed_step_is_not_checkpointed_and_reexecutes` | Add multistep retry test where only unfinished steps re-execute |
60+
| Decomposed steps | Partial | `decomposed_step_handle_reuses_completed_state_after_retry`; `failed_decomposed_step_is_not_checkpointed_and_reexecutes` | Add repeated `begin_step` / `complete_step` name numbering test |
61+
| Sleep / scheduled resume | Covered | `sleep_until_suspends_then_resumes_from_checkpoint` | None |
62+
| Event await/emit | Partial | `pre_emitted_event_is_available_to_late_waiter`; `emitted_event_wakes_all_waiters`; `event_timeout_can_be_caught_without_recreating_wait` | Add first-write-wins `emit_event` test |
63+
| Task result fetch/await | Partial | terminal fetch/await, failed/cancelled/missing, timeout, same-queue rejection, completed cross-queue await | Add cross-queue pending child wait; add checkpoint-survives-parent-retry-after-child-cleanup test |
64+
| Manual retry task | Covered | default, max-attempt override, spawn-new, missing-task, and non-failed-task tests | None |
65+
| Manual cancel task | Partial | pending cancel and terminal race tests | Add running cancel, sleeping cancel, completed/failed no-op, and missing-task error tests |
66+
| Cleanup tasks/events | Missing | No Rust integration test found; parity doc currently claims coverage | Add `Client::cleanup` / `cleanup_with_limit` integration test for terminal tasks and emitted events |
67+
| Hooks | Covered | before-spawn inject/preserve, wrap execution, and context access tests | None |
68+
| Failure payload shape | Covered | user error, invalid headers, task-result failure payload assertions | None |
69+
| Panic handling | Covered | `panic_failure_payload_records_diagnostic_shape` | None |
70+
| Raw task claim API | Not applicable | intentionally internal in Rust | Do not add public parity tests |
71+
| Sync client API | Not applicable | Rust SDK is intentionally async-only | Do not add |
72+
| Global/decorator task context | Not applicable | Rust uses explicit `TaskContext` | Do not add |
73+
| Logger injection | Not applicable | Rust uses `tracing` and `WorkerOptions::on_error` | Do not add |
74+
| Failure traceback strings | Not applicable | Rust asserts diagnostic payload with `traceback: null` | Do not chase upstream traceback-string tests |
75+
76+
## Recommended implementation backlog
77+
78+
### Required high-signal additions
79+
80+
1. `cleanup_removes_terminal_tasks_and_events_by_ttl`
81+
2. `cancellation_policy_cancels_by_max_delay`
82+
3. `cancellation_policy_cancels_by_max_duration`
83+
4. `default_task_cancellation_is_applied`
84+
5. `exponential_retry_strategy_delays_attempts`
85+
6. `retry_strategy_none_requeues_immediately`
86+
7. `spawn_resolution_applies_defaults_overrides_and_rejects_queue_mismatches`
87+
8. `work_batch_handles_mixed_success_and_failure`
88+
9. `worker_respects_concurrency_limit`
89+
10. `event_emit_is_first_write_wins`
90+
11. `context_await_task_result_waits_for_pending_child_in_other_queue`
91+
12. `context_await_task_result_checkpoint_survives_parent_retry_after_child_cleanup`
92+
13. `manual_cancel_running_sleeping_terminal_and_missing_cases`
93+
14. `multi_step_only_reexecutes_uncompleted_steps`
94+
15. `repeated_decomposed_step_names_are_numbered`
95+
16. `connection_defaults_resolve_env_precedence`
96+
97+
### Test design notes
98+
99+
- Prefer one focused integration test per durable invariant.
100+
- Use `SET absurd.fake_now` where possible instead of real sleeps for retry and cancellation timing.
101+
- Keep optional permutation tests out unless they catch a distinct class of regression.
102+
- Keep language-specific upstream tests out of the Rust suite unless Rust exposes an equivalent public surface.
103+
104+
## Validation commands
105+
106+
```sh
107+
cargo fmt --all --check
108+
cargo clippy --all-targets --all-features -- -D warnings
109+
cargo test --all-targets --all-features
110+
ABSURD_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/absurd_test cargo test-db
111+
```

src/client.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -716,20 +716,27 @@ pub(crate) async fn claim_tasks(
716716
}
717717

718718
fn resolve_database_url(explicit: Option<&str>) -> String {
719+
resolve_database_url_with(explicit, |key| std::env::var(key).ok())
720+
}
721+
722+
fn resolve_database_url_with<F>(explicit: Option<&str>, mut env: F) -> String
723+
where
724+
F: FnMut(&str) -> Option<String>,
725+
{
719726
if let Some(explicit) = explicit.filter(|value| !value.trim().is_empty()) {
720727
return explicit.to_string();
721728
}
722-
if let Ok(url) = std::env::var("ABSURD_DATABASE_URL") {
729+
if let Some(url) = env("ABSURD_DATABASE_URL") {
723730
if !url.trim().is_empty() {
724731
return url;
725732
}
726733
}
727-
if let Ok(url) = std::env::var("DATABASE_URL") {
734+
if let Some(url) = env("DATABASE_URL") {
728735
if !url.trim().is_empty() {
729736
return url;
730737
}
731738
}
732-
if let Ok(pgdatabase) = std::env::var("PGDATABASE") {
739+
if let Some(pgdatabase) = env("PGDATABASE") {
733740
if !pgdatabase.trim().is_empty() {
734741
if pgdatabase.contains("://") || pgdatabase.contains('=') {
735742
return pgdatabase;
@@ -739,3 +746,66 @@ fn resolve_database_url(explicit: Option<&str>) -> String {
739746
}
740747
"postgresql://localhost/absurd".to_string()
741748
}
749+
750+
#[cfg(test)]
751+
mod tests {
752+
use super::*;
753+
use std::collections::HashMap;
754+
755+
#[test]
756+
fn connection_defaults_resolve_env_precedence() {
757+
let env = HashMap::from([
758+
("ABSURD_DATABASE_URL", "postgresql://localhost/absurd_env"),
759+
("DATABASE_URL", "postgresql://localhost/database_env"),
760+
("PGDATABASE", "pg_database_name"),
761+
]);
762+
763+
assert_eq!(
764+
resolve_database_url_with(Some("postgresql://explicit/database"), |key| env
765+
.get(key)
766+
.map(ToString::to_string)),
767+
"postgresql://explicit/database"
768+
);
769+
assert_eq!(
770+
resolve_database_url_with(Some(" "), |key| env.get(key).map(ToString::to_string)),
771+
"postgresql://localhost/absurd_env"
772+
);
773+
assert_eq!(
774+
resolve_database_url_with(None, |key| env.get(key).map(ToString::to_string)),
775+
"postgresql://localhost/absurd_env"
776+
);
777+
778+
let env_without_absurd = HashMap::from([
779+
("DATABASE_URL", "postgresql://localhost/database_env"),
780+
("PGDATABASE", "pg_database_name"),
781+
]);
782+
assert_eq!(
783+
resolve_database_url_with(None, |key| env_without_absurd
784+
.get(key)
785+
.map(ToString::to_string)),
786+
"postgresql://localhost/database_env"
787+
);
788+
789+
let env_with_plain_pgdatabase = HashMap::from([("PGDATABASE", "pg_database_name")]);
790+
assert_eq!(
791+
resolve_database_url_with(None, |key| env_with_plain_pgdatabase
792+
.get(key)
793+
.map(ToString::to_string)),
794+
"dbname=pg_database_name"
795+
);
796+
797+
let env_with_pgdatabase_url = HashMap::from([("PGDATABASE", "postgresql://localhost/pg")]);
798+
assert_eq!(
799+
resolve_database_url_with(None, |key| env_with_pgdatabase_url
800+
.get(key)
801+
.map(ToString::to_string)),
802+
"postgresql://localhost/pg"
803+
);
804+
805+
let empty_env = HashMap::<&str, &str>::new();
806+
assert_eq!(
807+
resolve_database_url_with(None, |key| empty_env.get(key).map(ToString::to_string)),
808+
"postgresql://localhost/absurd"
809+
);
810+
}
811+
}

0 commit comments

Comments
 (0)