#![allow(clippy::unwrap_used, clippy::expect_used)] //! Evaluating source and loading modules on a stock Erlang node. mod common; use beamdev::connection::ConnectionManager; use beamdev::eval::{Language, eval, load_module}; use beamdev::rpc::rpc_call; use common::{Runtime, TestNode}; use eetf::Term; async fn process_count(cm: &ConnectionManager) -> i32 { let out = eval(cm, "length(processes()).", Language::Erlang, Some(5_000)) .await .expect("count processes"); match out { Term::FixInteger(i) => i.value, other => panic!("expected an integer, got {:?}", other), } } #[tokio::test] async fn eval_reaches_functions_the_old_sandbox_forbade() { let node = TestNode::start(Runtime::Erlang).await; let out = eval( node.manager(), "erlang:process_info(self(), [message_queue_len]).", Language::Erlang, Some(5_000), ) .await .expect("eval"); assert!(matches!(&out, Term::List(_))); } #[tokio::test] async fn eval_sequences_multiple_expressions() { let node = TestNode::start(Runtime::Erlang).await; let out = eval( node.manager(), "X = 2, Y = 3, X * Y.", Language::Erlang, Some(5_000), ) .await .expect("eval"); assert!(matches!(&out, Term::FixInteger(i) if i.value == 6)); } #[tokio::test] async fn eval_reports_a_parse_error() { let node = TestNode::start(Runtime::Erlang).await; let err = eval( node.manager(), "this is not erlang.", Language::Erlang, Some(5_000), ) .await .expect_err("should not parse"); assert!(err.to_string().contains("parse failed"), "{err}"); } #[tokio::test] async fn load_module_compiles_and_loads_erlang_source() { let node = TestNode::start(Runtime::Erlang).await; let source = "-module(proto_mod).\n\ -export([hi/0]).\n\ hi() -> lists:map(fun(X) -> X * 2 end, [1, 2, 3])."; let loaded = load_module(node.manager(), source, Language::Erlang, Some(10_000)) .await .expect("load"); assert_eq!(loaded.module, "proto_mod"); assert!(loaded.beam_bytes > 0); // Erlang encodes small-integer lists as STRING_EXT, so [2,4,6] is a ByteList. let out = rpc_call(node.manager(), "proto_mod", "hi", vec![], Some(5_000)) .await .expect("call loaded module"); match &out { Term::ByteList(b) => assert_eq!(b.bytes, vec![2, 4, 6]), other => panic!("expected [2,4,6], got {:?}", other), } } /// rex addresses its reply to the pid we send as `From`, so that pid must live /// on the node we actually handshook as -- not a hardcoded name. Every test /// here connects under a generated local name, so reaching the node at all /// proves the reply came back to the right place. #[tokio::test] async fn rpc_works_with_a_generated_local_node_name() { let node = TestNode::start(Runtime::Erlang).await; let out = rpc_call(node.manager(), "erlang", "node", vec![], Some(5_000)) .await .expect("rpc should reach us at our real local node name"); assert!(matches!(&out, Term::Atom(a) if a.name == node.name)); } /// The RPC timeout bounds how long we wait, not how long the node works, so the /// runner has to kill the process itself. #[tokio::test] async fn a_runaway_expression_is_killed_and_leaves_nothing_behind() { let node = TestNode::start(Runtime::Erlang).await; let before = process_count(node.manager()).await; let err = eval( node.manager(), "timer:sleep(infinity).", Language::Erlang, Some(500), ) .await .expect_err("should be killed"); assert!(err.to_string().contains("timeout"), "{}", err); // The counting eval is itself a process, so allow for the one in flight. let after = process_count(node.manager()).await; assert!( after <= before, "leaked processes: {} before, {} after", before, after ); } #[tokio::test] async fn an_allocation_bomb_is_killed_and_leaves_nothing_behind() { let node = TestNode::start(Runtime::Erlang).await; let before = process_count(node.manager()).await; let err = eval( node.manager(), "lists:seq(1, 100000000).", Language::Erlang, Some(20_000), ) .await .expect_err("should be killed"); assert!(err.to_string().contains("killed"), "{}", err); let after = process_count(node.manager()).await; assert!( after <= before, "leaked processes: {} before, {} after", before, after ); } #[tokio::test] async fn an_exception_reports_its_class_and_reason() { let node = TestNode::start(Runtime::Erlang).await; let err = eval(node.manager(), "1 / 0.", Language::Erlang, Some(5_000)) .await .expect_err("should raise"); assert!(err.to_string().contains("badarith"), "{}", err); } /// The runner is not deployed on connect, so the first eval has to notice it is /// missing and load it. #[tokio::test] async fn the_first_eval_deploys_the_runner_itself() { let node = TestNode::start(Runtime::Erlang).await; let loaded = eval( node.manager(), "code:which(beamdev_runner) =/= non_existing.", Language::Erlang, Some(5_000), ) .await .expect("eval should deploy the runner and succeed"); assert!( matches!(&loaded, Term::Atom(a) if a.name == "true"), "{:?}", loaded ); } /// If the node loses the runner -- a restart, a purge -- the next eval puts it /// back rather than failing. #[tokio::test] async fn a_purged_runner_is_redeployed() { let node = TestNode::start(Runtime::Erlang).await; eval(node.manager(), "ok.", Language::Erlang, Some(5_000)) .await .expect("first eval"); rpc_call( node.manager(), "code", "purge", vec![eetf::Term::from(eetf::Atom::from("beamdev_runner"))], Some(5_000), ) .await .expect("purge"); rpc_call( node.manager(), "code", "delete", vec![eetf::Term::from(eetf::Atom::from("beamdev_runner"))], Some(5_000), ) .await .expect("delete"); let out = eval(node.manager(), "2 + 2.", Language::Erlang, Some(5_000)) .await .expect("eval should redeploy the runner"); assert!( matches!(&out, Term::FixInteger(i) if i.value == 4), "{:?}", out ); }