Skip to content

Commit 76ddd9c

Browse files
authored
Merge pull request #547 from posit-dev/feature/error-tests
Add integration test for R errors
2 parents 81e72f4 + 978026e commit 76ddd9c

File tree

6 files changed

+114
-37
lines changed

6 files changed

+114
-37
lines changed

crates/amalthea/src/fixtures/dummy_frontend.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ use crate::connection_file::ConnectionFile;
1212
use crate::session::Session;
1313
use crate::socket::socket::Socket;
1414
use crate::wire::execute_input::ExecuteInput;
15-
use crate::wire::execute_reply::ExecuteReply;
1615
use crate::wire::execute_request::ExecuteRequest;
1716
use crate::wire::jupyter_message::JupyterMessage;
1817
use crate::wire::jupyter_message::Message;
1918
use crate::wire::jupyter_message::ProtocolMessage;
19+
use crate::wire::jupyter_message::Status;
2020
use crate::wire::status::ExecutionState;
2121
use crate::wire::wire_message::WireMessage;
2222

@@ -160,12 +160,25 @@ impl DummyFrontend {
160160
Message::read_from_socket(&self.shell_socket).unwrap()
161161
}
162162

163-
/// Receive from Shell and assert ExecuteReply message
164-
pub fn recv_shell_execute_reply(&self) -> ExecuteReply {
163+
/// Receive from Shell and assert `ExecuteReply` message.
164+
/// Returns `execution_count`.
165+
pub fn recv_shell_execute_reply(&self) -> u32 {
165166
let msg = self.recv_shell();
166167

167168
assert_match!(msg, Message::ExecuteReply(data) => {
168-
data.content
169+
assert_eq!(data.content.status, Status::Ok);
170+
data.content.execution_count
171+
})
172+
}
173+
174+
/// Receive from Shell and assert `ExecuteReplyException` message.
175+
/// Returns `execution_count`.
176+
pub fn recv_shell_execute_reply_exception(&self) -> u32 {
177+
let msg = self.recv_shell();
178+
179+
assert_match!(msg, Message::ExecuteReplyException(data) => {
180+
assert_eq!(data.content.status, Status::Error);
181+
data.content.execution_count
169182
})
170183
}
171184

crates/amalthea/src/socket/shell.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ impl Shell {
221221
let r = req.send_reply(reply, &self.socket);
222222
r
223223
},
224+
// FIXME: Ark already created an `ExecuteReplyException` so we use
225+
// `.send_reply()` instead of `.send_error()`. Can we streamline this?
224226
Err(err) => req.send_reply(err, &self.socket),
225227
}
226228
}

crates/amalthea/src/wire/error_reply.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ use crate::wire::jupyter_message::MessageType;
1313
use crate::wire::jupyter_message::Status;
1414

1515
/// Represents an error that occurred after processing a request on a
16-
/// ROUTER/DEALER socket
16+
/// ROUTER/DEALER socket.
17+
///
18+
/// This is the payload of a response to a request. Note that, as an exception,
19+
/// responses to `"execute_request"` include an `execution_count` field. We
20+
/// represent these with an `ExecuteReplyException`.
1721
#[derive(Debug, Serialize, Deserialize, Clone)]
1822
pub struct ErrorReply {
1923
/// The status; always Error
@@ -25,9 +29,11 @@ pub struct ErrorReply {
2529
}
2630

2731
/// Note that the message type of an error reply is generally adjusted to match
28-
/// its request type (e.g. foo_request => foo_reply)
32+
/// its request type (e.g. foo_request => foo_reply). The message type
33+
/// implemented here is only a placeholder and should not appear in any
34+
/// serialized/deserialized message.
2935
impl MessageType for ErrorReply {
3036
fn message_type() -> String {
31-
String::from("error")
37+
String::from("*error payload*")
3238
}
3339
}

crates/amalthea/src/wire/execute_error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use serde::Serialize;
1111
use crate::wire::exception::Exception;
1212
use crate::wire::jupyter_message::MessageType;
1313

14-
/// Represents an exception that occurred while executing code
14+
/// Represents an exception that occurred while executing code.
15+
/// This is sent to IOPub. Not to be confused with `ExecuteReplyException`
16+
/// which is a special case of a message of type `"execute_reply"` sent to Shell
17+
/// in response to an `"execute_request"`.
1518
#[derive(Debug, Serialize, Deserialize, Clone)]
1619
pub struct ExecuteError {
1720
/// The exception that occurred during execution

crates/amalthea/src/wire/jupyter_message.rs

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -168,57 +168,92 @@ impl TryFrom<&WireMessage> for Message {
168168
/// messages that are received from the frontend.
169169
fn try_from(msg: &WireMessage) -> Result<Self, Error> {
170170
let kind = msg.header.msg_type.clone();
171+
171172
if kind == KernelInfoRequest::message_type() {
172173
return Ok(Message::KernelInfoRequest(JupyterMessage::try_from(msg)?));
173-
} else if kind == KernelInfoReply::message_type() {
174+
}
175+
if kind == KernelInfoReply::message_type() {
174176
return Ok(Message::KernelInfoReply(JupyterMessage::try_from(msg)?));
175-
} else if kind == IsCompleteRequest::message_type() {
177+
}
178+
if kind == IsCompleteRequest::message_type() {
176179
return Ok(Message::IsCompleteRequest(JupyterMessage::try_from(msg)?));
177-
} else if kind == IsCompleteReply::message_type() {
180+
}
181+
if kind == IsCompleteReply::message_type() {
178182
return Ok(Message::IsCompleteReply(JupyterMessage::try_from(msg)?));
179-
} else if kind == InspectRequest::message_type() {
183+
}
184+
if kind == InspectRequest::message_type() {
180185
return Ok(Message::InspectRequest(JupyterMessage::try_from(msg)?));
181-
} else if kind == InspectReply::message_type() {
186+
}
187+
if kind == InspectReply::message_type() {
182188
return Ok(Message::InspectReply(JupyterMessage::try_from(msg)?));
183-
} else if kind == ExecuteRequest::message_type() {
189+
}
190+
if kind == ExecuteReplyException::message_type() {
191+
if let Ok(data) = JupyterMessage::try_from(msg) {
192+
return Ok(Message::ExecuteReplyException(data));
193+
}
194+
// else fallthrough to try `ExecuteRequest` which has the same message type
195+
}
196+
if kind == ExecuteRequest::message_type() {
184197
return Ok(Message::ExecuteRequest(JupyterMessage::try_from(msg)?));
185-
} else if kind == ExecuteReply::message_type() {
198+
}
199+
if kind == ExecuteReply::message_type() {
186200
return Ok(Message::ExecuteReply(JupyterMessage::try_from(msg)?));
187-
} else if kind == ExecuteResult::message_type() {
201+
}
202+
if kind == ExecuteResult::message_type() {
188203
return Ok(Message::ExecuteResult(JupyterMessage::try_from(msg)?));
189-
} else if kind == ExecuteInput::message_type() {
204+
}
205+
if kind == ExecuteError::message_type() {
206+
return Ok(Message::ExecuteError(JupyterMessage::try_from(msg)?));
207+
}
208+
if kind == ExecuteInput::message_type() {
190209
return Ok(Message::ExecuteInput(JupyterMessage::try_from(msg)?));
191-
} else if kind == CompleteRequest::message_type() {
210+
}
211+
if kind == CompleteRequest::message_type() {
192212
return Ok(Message::CompleteRequest(JupyterMessage::try_from(msg)?));
193-
} else if kind == CompleteReply::message_type() {
213+
}
214+
if kind == CompleteReply::message_type() {
194215
return Ok(Message::CompleteReply(JupyterMessage::try_from(msg)?));
195-
} else if kind == ShutdownRequest::message_type() {
216+
}
217+
if kind == ShutdownRequest::message_type() {
196218
return Ok(Message::ShutdownRequest(JupyterMessage::try_from(msg)?));
197-
} else if kind == KernelStatus::message_type() {
219+
}
220+
if kind == KernelStatus::message_type() {
198221
return Ok(Message::Status(JupyterMessage::try_from(msg)?));
199-
} else if kind == CommInfoRequest::message_type() {
222+
}
223+
if kind == CommInfoRequest::message_type() {
200224
return Ok(Message::CommInfoRequest(JupyterMessage::try_from(msg)?));
201-
} else if kind == CommInfoReply::message_type() {
225+
}
226+
if kind == CommInfoReply::message_type() {
202227
return Ok(Message::CommInfoReply(JupyterMessage::try_from(msg)?));
203-
} else if kind == CommOpen::message_type() {
228+
}
229+
if kind == CommOpen::message_type() {
204230
return Ok(Message::CommOpen(JupyterMessage::try_from(msg)?));
205-
} else if kind == CommWireMsg::message_type() {
231+
}
232+
if kind == CommWireMsg::message_type() {
206233
return Ok(Message::CommMsg(JupyterMessage::try_from(msg)?));
207-
} else if kind == CommClose::message_type() {
234+
}
235+
if kind == CommClose::message_type() {
208236
return Ok(Message::CommClose(JupyterMessage::try_from(msg)?));
209-
} else if kind == InterruptRequest::message_type() {
237+
}
238+
if kind == InterruptRequest::message_type() {
210239
return Ok(Message::InterruptRequest(JupyterMessage::try_from(msg)?));
211-
} else if kind == InterruptReply::message_type() {
240+
}
241+
if kind == InterruptReply::message_type() {
212242
return Ok(Message::InterruptReply(JupyterMessage::try_from(msg)?));
213-
} else if kind == InputReply::message_type() {
243+
}
244+
if kind == InputReply::message_type() {
214245
return Ok(Message::InputReply(JupyterMessage::try_from(msg)?));
215-
} else if kind == InputRequest::message_type() {
246+
}
247+
if kind == InputRequest::message_type() {
216248
return Ok(Message::InputRequest(JupyterMessage::try_from(msg)?));
217-
} else if kind == StreamOutput::message_type() {
249+
}
250+
if kind == StreamOutput::message_type() {
218251
return Ok(Message::StreamOutput(JupyterMessage::try_from(msg)?));
219-
} else if kind == UiFrontendRequest::message_type() {
252+
}
253+
if kind == UiFrontendRequest::message_type() {
220254
return Ok(Message::CommRequest(JupyterMessage::try_from(msg)?));
221-
} else if kind == JsonRpcReply::message_type() {
255+
}
256+
if kind == JsonRpcReply::message_type() {
222257
return Ok(Message::CommReply(JupyterMessage::try_from(msg)?));
223258
}
224259
return Err(Error::UnknownMessageType(kind));

crates/ark/tests/kernel.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use amalthea::wire::jupyter_message::Message;
2-
use amalthea::wire::jupyter_message::Status;
32
use amalthea::wire::kernel_info_request::KernelInfoRequest;
43
use ark::fixtures::DummyArkFrontend;
54
use stdext::assert_match;
@@ -28,11 +27,30 @@ fn test_execute_request() {
2827
frontend.send_execute_request("42");
2928
frontend.recv_iopub_busy();
3029

31-
assert_eq!(frontend.recv_iopub_execute_input().code, "42");
30+
let input = frontend.recv_iopub_execute_input();
31+
assert_eq!(input.code, "42");
3232
assert_eq!(frontend.recv_iopub_execute_result(), "[1] 42");
3333

3434
frontend.recv_iopub_idle();
3535

36-
let reply = frontend.recv_shell_execute_reply();
37-
assert_eq!(reply.status, Status::Ok);
36+
assert_eq!(frontend.recv_shell_execute_reply(), input.execution_count)
37+
}
38+
39+
#[test]
40+
fn test_execute_request_error() {
41+
let frontend = DummyArkFrontend::lock();
42+
43+
frontend.send_execute_request("stop('foobar')");
44+
frontend.recv_iopub_busy();
45+
46+
let input = frontend.recv_iopub_execute_input();
47+
assert_eq!(input.code, "stop('foobar')");
48+
assert!(frontend.recv_iopub_execute_error().contains("foobar"));
49+
50+
frontend.recv_iopub_idle();
51+
52+
assert_eq!(
53+
frontend.recv_shell_execute_reply_exception(),
54+
input.execution_count
55+
);
3856
}

0 commit comments

Comments
 (0)