Skip to content

Commit dd51481

Browse files
authored
Merge pull request #19 from gripmock/asserts-fix
asserts + errors
2 parents a5e0eca + 0b4452a commit dd51481

File tree

3 files changed

+78
-4
lines changed

3 files changed

+78
-4
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
resolver = "2"
33

44
[workspace.package]
5-
version = "1.4.4"
5+
version = "1.4.5"
66
edition = "2024"
77
authors = ["bavix"]
88
license = "MIT"

docs/.vitepress/config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ export default defineConfig({
110110

111111
socialLinks: [
112112
{ icon: 'github', link: 'https://github.com/gripmock/grpctestify-rust' },
113-
{ icon: 'discord', link: 'https://discord.gg/gripmock' }
114113
],
115114

116115
footer: {

src/execution/runner.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::sync::Arc;
2020
use tokio::sync::mpsc;
2121
use tokio_stream::StreamExt;
2222
use tokio_stream::wrappers::ReceiverStream;
23+
use tonic::metadata::KeyAndValueRef;
2324

2425
/// Execution plan for inspect workflow visualization
2526
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -777,6 +778,7 @@ impl TestRunner {
777778

778779
let mut variables: HashMap<String, Value> = HashMap::new();
779780
let mut last_message: Option<Value> = None;
781+
let mut last_error_message: Option<String> = None;
780782
let mut captured_headers: HashMap<String, String> = HashMap::new();
781783
let mut captured_trailers: HashMap<String, String> = HashMap::new();
782784
let mut failure_reasons: Vec<String> = Vec::new();
@@ -1076,8 +1078,34 @@ impl TestRunner {
10761078
}
10771079
}
10781080

1079-
// Standalone Asserts - consumes a new message
1080-
match response_stream.as_mut().unwrap().next().await {
1081+
// Standalone ASSERTS usually consumes a new message.
1082+
// If stream is unavailable but we already captured an ERROR,
1083+
// evaluate assertions against that error context.
1084+
let Some(stream) = response_stream.as_mut() else {
1085+
if !self.no_assert
1086+
&& let SectionContent::Assertions(lines) = &section.content
1087+
{
1088+
if let Some(error_message) = &last_error_message {
1089+
let error_value = Value::String(error_message.clone());
1090+
self.run_assertions(
1091+
lines,
1092+
&error_value,
1093+
&captured_headers,
1094+
&captured_trailers,
1095+
&mut failure_reasons,
1096+
format!("after ERROR at line {}", section.start_line),
1097+
);
1098+
} else {
1099+
failure_reasons.push(format!(
1100+
"ASSERTS section at line {} has no active response/error context",
1101+
section.start_line
1102+
));
1103+
}
1104+
}
1105+
continue;
1106+
};
1107+
1108+
match stream.next().await {
10811109
Some(Ok(crate::grpc::client::StreamItem::Message(msg))) => {
10821110
last_message = Some(msg.clone());
10831111

@@ -1119,6 +1147,9 @@ impl TestRunner {
11191147
}
11201148
}
11211149
Some(Err(status)) => {
1150+
last_error_message = Some(status.message().to_string());
1151+
captured_trailers
1152+
.extend(Self::metadata_map_to_hashmap(status.metadata()));
11221153
if !self.no_assert {
11231154
failure_reasons.push(format!(
11241155
"Expected message for ASSERTS section at line {}, but received Error: {}",
@@ -1202,6 +1233,10 @@ impl TestRunner {
12021233
let (matches, got, mismatch_reason) = if let Some(status) =
12031234
e.downcast_ref::<tonic::Status>()
12041235
{
1236+
last_error_message = Some(status.message().to_string());
1237+
captured_trailers.extend(
1238+
Self::metadata_map_to_hashmap(status.metadata()),
1239+
);
12051240
let status_name = Self::grpc_code_name_from_numeric(
12061241
status.code() as i64,
12071242
)
@@ -1293,6 +1328,9 @@ impl TestRunner {
12931328
// Expect an error from the stream
12941329
match response_stream.as_mut().unwrap().next().await {
12951330
Some(Err(status)) => {
1331+
last_error_message = Some(status.message().to_string());
1332+
captured_trailers
1333+
.extend(Self::metadata_map_to_hashmap(status.metadata()));
12961334
let status_name =
12971335
Self::grpc_code_name_from_numeric(status.code() as i64)
12981336
.unwrap_or("Unknown");
@@ -1492,6 +1530,26 @@ impl TestRunner {
14921530
self.response_handler.validate_document(document, response)
14931531
}
14941532

1533+
fn metadata_map_to_hashmap(metadata: &tonic::metadata::MetadataMap) -> HashMap<String, String> {
1534+
let mut out = HashMap::new();
1535+
for entry in metadata.iter() {
1536+
match entry {
1537+
KeyAndValueRef::Ascii(key, value) => {
1538+
if let Ok(v) = value.to_str() {
1539+
out.insert(key.to_string(), v.to_string());
1540+
}
1541+
}
1542+
KeyAndValueRef::Binary(key, value) => {
1543+
out.insert(
1544+
key.to_string(),
1545+
String::from_utf8_lossy(value.as_encoded_bytes()).into_owned(),
1546+
);
1547+
}
1548+
}
1549+
}
1550+
out
1551+
}
1552+
14951553
fn substitute_variables(&self, value: &mut Value, variables: &HashMap<String, Value>) {
14961554
match value {
14971555
Value::String(s) => {
@@ -2026,4 +2084,21 @@ mod tests {
20262084
assert_eq!(values.len(), 1);
20272085
assert_eq!(values[0], json!({"key": "value"}));
20282086
}
2087+
2088+
#[test]
2089+
fn test_metadata_map_to_hashmap_extracts_ascii_values() {
2090+
let mut metadata = tonic::metadata::MetadataMap::new();
2091+
metadata.insert("code", "EXTERNAL_SERVICE_ERROR_CODE".parse().unwrap());
2092+
metadata.insert("message", "External service error message".parse().unwrap());
2093+
2094+
let trailers = TestRunner::metadata_map_to_hashmap(&metadata);
2095+
assert_eq!(
2096+
trailers.get("code"),
2097+
Some(&"EXTERNAL_SERVICE_ERROR_CODE".to_string())
2098+
);
2099+
assert_eq!(
2100+
trailers.get("message"),
2101+
Some(&"External service error message".to_string())
2102+
);
2103+
}
20292104
}

0 commit comments

Comments
 (0)