Skip to content

Commit eeec30c

Browse files
committed
chore(cymbal): add shimforge to simplify tests
Replace the telemetry HTTP mock and its flush-and-poll loop with a thread-local shimforge mock of common_posthog::capture_exception. The test asserts the handoff directly - HTTP 500, exactly one capture, UnhandledError::SqlxError(PoolClosed), and the request_id, batch_event_count and team_count properties - instead of polling an HTTP mock for up to five seconds. Add shimforge 0.1.3 as a pinned dev-dependency with its lock entries.
1 parent 814ecd8 commit eeec30c

3 files changed

Lines changed: 50 additions & 49 deletions

File tree

rust/Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/cymbal/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ common-temporal = { path = "../common/temporal" }
6060

6161

6262
[dev-dependencies]
63+
shimforge = "=0.1.3"
6364
common-compression = { path = "../common/compression" }
6465
# For constructing IO-flavored RedisErrors in redis_heal classifier tests
6566
redis = { workspace = true }
Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,22 @@
11
use std::sync::Arc;
2-
use std::time::Duration;
32

43
use axum::{body::Body, http::Request};
54
use common_redis::MockRedisClient;
6-
use cymbal::{app_context::AppContext, modes::processing::ProcessingConfig, router::get_router};
7-
use httpmock::prelude::*;
8-
use serde_json::json;
5+
use cymbal::{
6+
app_context::AppContext, error::UnhandledError, modes::processing::ProcessingConfig,
7+
router::get_router,
8+
};
9+
use serde_json::{json, Value};
10+
use shimforge::{mock, Session};
911
use sqlx::PgPool;
1012
use tower::ServiceExt;
1113
use uuid::Uuid;
1214

1315
mod common;
1416
mod utils;
1517

16-
// One test per binary: common_posthog::init configures a process-wide global
17-
// client, so a second init with a different mock server would be ignored.
1818
#[sqlx::test(migrations = "./tests/test_migrations")]
1919
async fn pipeline_failure_is_captured_as_posthog_exception(db: PgPool) {
20-
let posthog = MockServer::start_async().await;
21-
let capture = posthog
22-
.mock_async(|when, then| {
23-
when.method(POST)
24-
.path("/i/v1/analytics/events")
25-
.body_contains("\"$exception\"")
26-
.body_contains("UnhandledError")
27-
.body_contains("\"service\":\"cymbal-test\"")
28-
.body_contains("\"request_id\"");
29-
then.status(200).body("{\"results\":{}}");
30-
})
31-
.await;
32-
// Catch-all so an unexpected payload shape fails the specific assertion
33-
// below instead of surfacing as a connection-level SDK error.
34-
let fallback = posthog
35-
.mock_async(|when, then| {
36-
when.path_contains("/");
37-
then.status(200).body("{\"results\":{}}");
38-
})
39-
.await;
40-
41-
common_posthog::init("cymbal-test", Some("test-api-key"), &posthog.base_url())
42-
.await
43-
.expect("posthog init");
44-
4520
let (addr, _) = common::spawn_stub_server(common::ServerBehavior::Happy).await;
4621
let mut config = ProcessingConfig::init_with_defaults().unwrap();
4722
config.remote_resolution_host = "127.0.0.1".to_string();
@@ -68,11 +43,32 @@ async fn pipeline_failure_is_captured_as_posthog_exception(db: PgPool) {
6843
},
6944
}]);
7045

46+
// The route calls capture on this task; SDK delivery is covered by
47+
// common/posthog/tests/panic_capture.rs.
48+
let mut session = Session::new();
49+
let capture = mock!(
50+
session,
51+
common_posthog::capture_exception::<UnhandledError>,
52+
fn(Arc<UnhandledError>, [(&'static str, Value); 3])
53+
);
54+
capture
55+
.expect()
56+
.with(|error, properties| {
57+
matches!(&**error, UnhandledError::SqlxError(sqlx::Error::PoolClosed))
58+
&& properties.contains(&("request_id", json!("capture-test-request")))
59+
&& properties.contains(&("batch_event_count", json!(1)))
60+
&& properties.contains(&("team_count", json!(1)))
61+
})
62+
.once()
63+
.returns_default();
64+
// Enforced when `session` drops, not at call time.
65+
7166
let response = router
7267
.oneshot(
7368
Request::builder()
7469
.method("POST")
7570
.header("content-type", "application/json")
71+
.header("x-request-id", "capture-test-request")
7672
.uri("/process")
7773
.body(Body::from(serde_json::to_vec(&event).unwrap()))
7874
.unwrap(),
@@ -83,22 +79,4 @@ async fn pipeline_failure_is_captured_as_posthog_exception(db: PgPool) {
8379
response.status(),
8480
reqwest::StatusCode::INTERNAL_SERVER_ERROR
8581
);
86-
87-
// The capture is fire-and-forget, and the SDK only buffers it: one event
88-
// never reaches `flush_at`, so delivery would otherwise wait on the 5s
89-
// `flush_interval_ms`. Flush every turn so this waits on the spawned send
90-
// rather than racing that interval.
91-
for _ in 0..100 {
92-
posthog_rs::flush().await;
93-
if capture.hits_async().await > 0 {
94-
break;
95-
}
96-
tokio::time::sleep(Duration::from_millis(50)).await;
97-
}
98-
assert_eq!(
99-
capture.hits_async().await,
100-
1,
101-
"expected a matching $exception capture; total capture requests seen: {}",
102-
fallback.hits_async().await
103-
);
10482
}

0 commit comments

Comments
 (0)