Skip to content

Commit 6705951

Browse files
authored
Merge pull request #542 from posit-dev/feature/client-tests
Draft integration tests for Ark
2 parents 7e957a2 + 5af6ba6 commit 6705951

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+841
-501
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
# Was necessary after switching to dev tree-sitter to fix this warning:
44
# > some crates are on edition 2021 which defaults to `resolver = "2"`, but
5-
# > virtual workspaces default to `resolver = "1"`
5+
# > virtual workspaces default to `resolver = "1"`.
6+
#
7+
# Also necessary to enable the `testing` feature of harp only when testing
8+
# (i.e. when building downstream packages like Ark with Harp's `testing`
9+
# feature set in `dev-dependencies`).
610
resolver = "2"
711

812
members = [

crates/amalthea/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ hex = "0.4.3"
1919
hmac = "0.12.1"
2020
log = "0.4.17"
2121
nix = "0.26.2"
22+
portpicker = "0.1.1"
23+
rand = "0.8.5"
2224
serde = { version = "1.0.154", features = ["derive"] }
2325
serde_json = { version = "1.0.94", features = ["preserve_order"]}
2426
sha2 = "0.10.6"
@@ -34,6 +36,4 @@ serde_repr = "0.1.17"
3436
tracing = "0.1.40"
3537

3638
[dev-dependencies]
37-
rand = "0.8.5"
38-
portpicker = "0.1.1"
3939
env_logger = "0.10.0"

crates/amalthea/tests/frontend/mod.rs renamed to crates/amalthea/src/fixtures/dummy_frontend.rs

+129-15
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
/*
2-
* mod.rs
2+
* dummy_frontend.rs
33
*
4-
* Copyright (C) 2022 Posit Software, PBC. All rights reserved.
4+
* Copyright (C) 2022-2024 Posit Software, PBC. All rights reserved.
55
*
66
*/
77

8-
use amalthea::connection_file::ConnectionFile;
9-
use amalthea::session::Session;
10-
use amalthea::socket::socket::Socket;
11-
use amalthea::wire::jupyter_message::JupyterMessage;
12-
use amalthea::wire::jupyter_message::Message;
13-
use amalthea::wire::jupyter_message::ProtocolMessage;
8+
use serde_json::Value;
9+
use stdext::assert_match;
1410

15-
pub struct Frontend {
11+
use crate::connection_file::ConnectionFile;
12+
use crate::session::Session;
13+
use crate::socket::socket::Socket;
14+
use crate::wire::execute_input::ExecuteInput;
15+
use crate::wire::execute_reply::ExecuteReply;
16+
use crate::wire::execute_request::ExecuteRequest;
17+
use crate::wire::jupyter_message::JupyterMessage;
18+
use crate::wire::jupyter_message::Message;
19+
use crate::wire::jupyter_message::ProtocolMessage;
20+
use crate::wire::status::ExecutionState;
21+
use crate::wire::wire_message::WireMessage;
22+
23+
pub struct DummyFrontend {
1624
pub _control_socket: Socket,
1725
pub shell_socket: Socket,
1826
pub iopub_socket: Socket,
@@ -27,7 +35,7 @@ pub struct Frontend {
2735
heartbeat_port: u16,
2836
}
2937

30-
impl Frontend {
38+
impl DummyFrontend {
3139
pub fn new() -> Self {
3240
use rand::Rng;
3341

@@ -117,7 +125,7 @@ impl Frontend {
117125

118126
/// Completes initialization of the frontend (usually done after the kernel
119127
/// is ready and connected)
120-
pub fn complete_intialization(&self) {
128+
pub fn complete_initialization(&self) {
121129
self.iopub_socket.subscribe().unwrap();
122130
}
123131

@@ -130,29 +138,100 @@ impl Frontend {
130138
id
131139
}
132140

141+
pub fn send_execute_request(&self, code: &str) -> String {
142+
self.send_shell(ExecuteRequest {
143+
code: String::from(code),
144+
silent: false,
145+
store_history: true,
146+
user_expressions: serde_json::Value::Null,
147+
allow_stdin: false,
148+
stop_on_error: false,
149+
})
150+
}
151+
133152
/// Sends a Jupyter message on the Stdin socket
134153
pub fn send_stdin<T: ProtocolMessage>(&self, msg: T) {
135154
let message = JupyterMessage::create(msg, None, &self.session);
136155
message.send(&self.stdin_socket).unwrap();
137156
}
138157

139158
/// Receives a Jupyter message from the Shell socket
140-
pub fn receive_shell(&self) -> Message {
159+
pub fn recv_shell(&self) -> Message {
141160
Message::read_from_socket(&self.shell_socket).unwrap()
142161
}
143162

163+
/// Receive from Shell and assert ExecuteReply message
164+
pub fn recv_shell_execute_reply(&self) -> ExecuteReply {
165+
let msg = self.recv_shell();
166+
167+
assert_match!(msg, Message::ExecuteReply(data) => {
168+
data.content
169+
})
170+
}
171+
144172
/// Receives a Jupyter message from the IOPub socket
145-
pub fn receive_iopub(&self) -> Message {
173+
pub fn recv_iopub(&self) -> Message {
146174
Message::read_from_socket(&self.iopub_socket).unwrap()
147175
}
148176

177+
/// Receive from IOPub and assert Busy message
178+
pub fn recv_iopub_busy(&self) -> () {
179+
let msg = self.recv_iopub();
180+
181+
assert_match!(msg, Message::Status(data) => {
182+
assert_eq!(data.content.execution_state, ExecutionState::Busy);
183+
});
184+
}
185+
186+
/// Receive from IOPub and assert Idle message
187+
pub fn recv_iopub_idle(&self) -> () {
188+
let msg = self.recv_iopub();
189+
190+
assert_match!(msg, Message::Status(data) => {
191+
assert_eq!(data.content.execution_state, ExecutionState::Idle);
192+
});
193+
}
194+
195+
/// Receive from IOPub and assert ExecuteInput message
196+
pub fn recv_iopub_execute_input(&self) -> ExecuteInput {
197+
let msg = self.recv_iopub();
198+
199+
assert_match!(msg, Message::ExecuteInput(data) => {
200+
data.content
201+
})
202+
}
203+
204+
/// Receive from IOPub and assert ExecuteResult message. Returns compulsory
205+
/// `plain/text` result.
206+
pub fn recv_iopub_execute_result(&self) -> String {
207+
let msg = self.recv_iopub();
208+
209+
assert_match!(msg, Message::ExecuteResult(data) => {
210+
assert_match!(data.content.data, Value::Object(map) => {
211+
assert_match!(map["text/plain"], Value::String(ref string) => {
212+
string.clone()
213+
})
214+
})
215+
})
216+
}
217+
218+
/// Receive from IOPub and assert ExecuteResult message. Returns compulsory
219+
/// `evalue` field.
220+
pub fn recv_iopub_execute_error(&self) -> String {
221+
let msg = self.recv_iopub();
222+
223+
assert_match!(msg, Message::ExecuteError(data) => {
224+
data.content.exception.evalue
225+
})
226+
}
227+
149228
/// Receives a Jupyter message from the Stdin socket
150-
pub fn receive_stdin(&self) -> Message {
229+
pub fn recv_stdin(&self) -> Message {
151230
Message::read_from_socket(&self.stdin_socket).unwrap()
152231
}
153232

154233
/// Receives a (raw) message from the heartbeat socket
155-
pub fn receive_heartbeat(&self) -> zmq::Message {
234+
pub fn recv_heartbeat(&self) -> zmq::Message {
156235
let mut msg = zmq::Message::new();
157236
self.heartbeat_socket.recv(&mut msg).unwrap();
158237
msg
@@ -178,4 +257,39 @@ impl Frontend {
178257
key: self.key.clone(),
179258
}
180259
}
260+
261+
/// Asserts that no socket has incoming data
262+
pub fn assert_no_incoming(&mut self) {
263+
let mut has_incoming = false;
264+
265+
if self.iopub_socket.has_incoming_data().unwrap() {
266+
has_incoming = true;
267+
Self::flush_incoming("IOPub", &self.iopub_socket);
268+
}
269+
if self.shell_socket.has_incoming_data().unwrap() {
270+
has_incoming = true;
271+
Self::flush_incoming("Shell", &self.shell_socket);
272+
}
273+
if self.stdin_socket.has_incoming_data().unwrap() {
274+
has_incoming = true;
275+
Self::flush_incoming("StdIn", &self.stdin_socket);
276+
}
277+
if self.heartbeat_socket.has_incoming_data().unwrap() {
278+
has_incoming = true;
279+
Self::flush_incoming("Heartbeat", &self.heartbeat_socket);
280+
}
281+
282+
if has_incoming {
283+
panic!("Sockets must be empty on exit (see details above)");
284+
}
285+
}
286+
287+
fn flush_incoming(name: &str, socket: &Socket) {
288+
println!("{name} has incoming data:");
289+
290+
while socket.has_incoming_data().unwrap() {
291+
dbg!(WireMessage::read_from_socket(socket).unwrap());
292+
println!("---");
293+
}
294+
}
181295
}

crates/amalthea/src/fixtures/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod dummy_frontend;

crates/amalthea/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
pub mod comm;
99
pub mod connection_file;
1010
pub mod error;
11+
pub mod fixtures;
1112
pub mod kernel;
1213
pub mod kernel_dirs;
1314
pub mod kernel_spec;

crates/amalthea/src/socket/socket.rs

+4
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ impl Socket {
180180
}
181181
}
182182

183+
pub fn has_incoming_data(&self) -> zmq::Result<bool> {
184+
Ok(self.socket.poll(zmq::PollEvents::POLLIN, 0)? != 0)
185+
}
186+
183187
/// Subscribes a SUB socket to all the published messages from a PUB socket.
184188
///
185189
/// Note that this needs to be called *after* the socket connection is

0 commit comments

Comments
 (0)