use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::collections::BTreeSet;
use std::sync::Arc;
fn policy_with_net() -> Policy {
let mut p = Policy::pure();
p.allow_effects = ["net".to_string()].into_iter().collect::<BTreeSet<_>>();
p
}
fn run(src: &str, func: &str, args: Vec<Value>, policy: Policy) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(func, args).expect("vm")
}
fn redis_url() -> Option<String> {
std::env::var("REDIS_TEST_URL").ok()
}
fn unwrap_err_str(v: Value) -> String {
match v {
Value::Variant { name, args } if name == "Err" => match args.into_iter().next() {
Some(Value::Str(s)) => s.to_string(),
other => panic!("expected Err(Str), got {other:?}"),
},
other => panic!("expected Err, got {other:?}"),
}
}
const TYPE_CHECK_SRC: &str = r#"
import "std.redis" as redis
import "std.io" as io
fn check_connect(url :: Str) -> [net] Result[ConnRedis, Str] {
redis.connect(url)
}
fn check_close(conn :: ConnRedis) -> [net] Unit {
redis.close(conn)
}
fn check_get(conn :: ConnRedis, key :: Str) -> [net] Option[Str] {
redis.get(conn, key)
}
fn check_set(conn :: ConnRedis, key :: Str, val :: Str) -> [net] Unit {
redis.set(conn, key, val)
}
fn check_set_ex(conn :: ConnRedis, key :: Str, val :: Str, ttl :: Int) -> [net] Unit {
redis.set_ex(conn, key, val, ttl)
}
fn check_del(conn :: ConnRedis, key :: Str) -> [net] Unit {
redis.del(conn, key)
}
fn check_exists(conn :: ConnRedis, key :: Str) -> [net] Bool {
redis.exists(conn, key)
}
fn check_expire(conn :: ConnRedis, key :: Str, ttl :: Int) -> [net] Unit {
redis.expire(conn, key, ttl)
}
fn check_publish(conn :: ConnRedis, ch :: Str, msg :: Str) -> [net] Int {
redis.publish(conn, ch, msg)
}
fn check_lpush(conn :: ConnRedis, key :: Str, val :: Str) -> [net] Int {
redis.lpush(conn, key, val)
}
fn check_rpush(conn :: ConnRedis, key :: Str, val :: Str) -> [net] Int {
redis.rpush(conn, key, val)
}
fn check_brpop(conn :: ConnRedis, key :: Str, timeout :: Int) -> [net] Option[Str] {
redis.brpop(conn, key, timeout)
}
fn check_llen(conn :: ConnRedis, key :: Str) -> [net] Int {
redis.llen(conn, key)
}
fn check_hset(conn :: ConnRedis, key :: Str, field :: Str, val :: Str) -> [net] Unit {
redis.hset(conn, key, field, val)
}
fn check_hget(conn :: ConnRedis, key :: Str, field :: Str) -> [net] Option[Str] {
redis.hget(conn, key, field)
}
fn check_hdel(conn :: ConnRedis, key :: Str, field :: Str) -> [net] Unit {
redis.hdel(conn, key, field)
}
fn check_hgetall(conn :: ConnRedis, key :: Str) -> [net] List[(Str, Str)] {
redis.hgetall(conn, key)
}
# subscribe handler carries an open effect row: io.print must type-check here.
fn check_subscribe(conn :: ConnRedis, ch :: Str) -> [net] Unit {
redis.subscribe(conn, ch, fn(channel :: Str, msg :: Str) -> [io] Unit {
io.print(msg)
})
}
# psubscribe handler carries an open effect row: io.print must type-check here.
fn check_psubscribe(conn :: ConnRedis, pat :: Str) -> [net] Unit {
redis.psubscribe(conn, pat, fn(pattern :: Str, channel :: Str, msg :: Str) -> [io] Unit {
io.print(msg)
})
}
"#;
#[test]
fn all_signatures_type_check() {
let prog = parse_source(TYPE_CHECK_SRC).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors in std.redis surface:\n{errs:#?}");
}
}
const CONNECT_SRC: &str = r#"
import "std.redis" as redis
fn try_connect(url :: Str) -> [net] Result[ConnRedis, Str] {
redis.connect(url)
}
"#;
#[test]
fn connect_to_unreachable_host_returns_err() {
let v = run(
CONNECT_SRC,
"try_connect",
vec![Value::Str("redis://127.0.0.1:1".into())],
policy_with_net(),
);
let msg = unwrap_err_str(v);
assert!(
msg.starts_with("redis.connect:"),
"expected redis.connect: prefix, got `{msg}`",
);
}
#[test]
fn connect_outside_net_host_policy_returns_err() {
let mut policy = policy_with_net();
policy.allow_net_host = vec!["allowed.example.com".to_string()];
let prog = parse_source(CONNECT_SRC).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
let r = vm.call(
"try_connect",
vec![Value::Str("redis://blocked.example.com:6379".into())],
);
assert!(r.is_err(), "expected policy Err, got {r:?}");
let msg = format!("{:?}", r.unwrap_err());
assert!(
msg.contains("blocked.example.com"),
"error should mention the blocked host, got `{msg}`",
);
}
#[test]
fn ops_on_closed_handle_error() {
let Some(url) = redis_url() else { return; };
let src = r#"
import "std.redis" as redis
fn open_close_then_get(url :: Str) -> [net] Option[Str] {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.close(conn)
redis.get(conn, "k")
},
Err(_) => None,
}
}
"#;
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy_with_net()).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
let r = vm.call("open_close_then_get", vec![Value::Str(url.into())]);
assert!(r.is_err(), "expected closed-handle error, got {r:?}");
let msg = format!("{:?}", r.unwrap_err());
assert!(
msg.contains("closed or unknown ConnRedis handle"),
"expected closed-handle message, got `{msg}`",
);
}
const ROUND_TRIP_SRC: &str = r#"
import "std.redis" as redis
fn set_get(url :: Str, key :: Str, val :: Str) -> [net] Option[Str] {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.set(conn, key, val)
redis.get(conn, key)
},
Err(_) => None,
}
}
fn get_missing(url :: Str, key :: Str) -> [net] Option[Str] {
match redis.connect(url) {
Ok(conn) => redis.get(conn, key),
Err(_) => None,
}
}
fn del_then_exists(url :: Str, key :: Str) -> [net] Bool {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.set(conn, key, "x")
let _ := redis.del(conn, key)
redis.exists(conn, key)
},
Err(_) => true,
}
}
fn set_ex_then_exists(url :: Str, key :: Str) -> [net] Bool {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.set_ex(conn, key, "v", 60)
redis.exists(conn, key)
},
Err(_) => false,
}
}
fn publish_returns_int(url :: Str, ch :: Str) -> [net] Bool {
match redis.connect(url) {
Ok(conn) => {
let n := redis.publish(conn, ch, "hello")
n >= 0
},
Err(_) => false,
}
}
fn list_roundtrip(url :: Str, key :: Str) -> [net] Int {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.del(conn, key)
let _ := redis.lpush(conn, key, "a")
let _ := redis.rpush(conn, key, "b")
redis.llen(conn, key)
},
Err(_) => 0 - 1,
}
}
fn hash_roundtrip(url :: Str, key :: Str) -> [net] Option[Str] {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.del(conn, key)
let _ := redis.hset(conn, key, "field1", "value1")
redis.hget(conn, key, "field1")
},
Err(_) => None,
}
}
fn hash_del_then_get(url :: Str, key :: Str) -> [net] Option[Str] {
match redis.connect(url) {
Ok(conn) => {
let _ := redis.del(conn, key)
let _ := redis.hset(conn, key, "f", "v")
let _ := redis.hdel(conn, key, "f")
redis.hget(conn, key, "f")
},
Err(_) => None,
}
}
"#;
fn unique_key(tag: &str) -> String {
format!(
"lex-test:{}-{}-{}",
std::process::id(),
tag,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
)
}
#[test]
fn set_get_round_trip() {
let Some(url) = redis_url() else { return; };
let key = unique_key("set-get");
let v = run(
ROUND_TRIP_SRC,
"set_get",
vec![
Value::Str(url.into()),
Value::Str(key.into()),
Value::Str("hello-redis".into()),
],
policy_with_net(),
);
assert_eq!(
v,
Value::Variant {
name: "Some".into(),
args: vec![Value::Str("hello-redis".into())],
},
"expected Some(\"hello-redis\"), got {v:?}",
);
}
#[test]
fn get_missing_returns_none() {
let Some(url) = redis_url() else { return; };
let key = unique_key("get-missing");
let v = run(
ROUND_TRIP_SRC,
"get_missing",
vec![Value::Str(url.into()), Value::Str(key.into())],
policy_with_net(),
);
assert_eq!(v, Value::Variant { name: "None".into(), args: vec![] });
}
#[test]
fn del_removes_key() {
let Some(url) = redis_url() else { return; };
let key = unique_key("del");
let v = run(
ROUND_TRIP_SRC,
"del_then_exists",
vec![Value::Str(url.into()), Value::Str(key.into())],
policy_with_net(),
);
assert_eq!(v, Value::Bool(false));
}
#[test]
fn set_ex_key_exists() {
let Some(url) = redis_url() else { return; };
let key = unique_key("set-ex");
let v = run(
ROUND_TRIP_SRC,
"set_ex_then_exists",
vec![Value::Str(url.into()), Value::Str(key.into())],
policy_with_net(),
);
assert_eq!(v, Value::Bool(true));
}
#[test]
fn publish_returns_non_negative() {
let Some(url) = redis_url() else { return; };
let v = run(
ROUND_TRIP_SRC,
"publish_returns_int",
vec![
Value::Str(url.into()),
Value::Str(unique_key("publish-ch").into()),
],
policy_with_net(),
);
assert_eq!(v, Value::Bool(true));
}
#[test]
fn lpush_rpush_llen() {
let Some(url) = redis_url() else { return; };
let key = unique_key("list");
let v = run(
ROUND_TRIP_SRC,
"list_roundtrip",
vec![Value::Str(url.into()), Value::Str(key.into())],
policy_with_net(),
);
assert_eq!(v, Value::Int(2));
}
#[test]
fn hset_hget_round_trip() {
let Some(url) = redis_url() else { return; };
let key = unique_key("hash");
let v = run(
ROUND_TRIP_SRC,
"hash_roundtrip",
vec![Value::Str(url.into()), Value::Str(key.into())],
policy_with_net(),
);
assert_eq!(
v,
Value::Variant {
name: "Some".into(),
args: vec![Value::Str("value1".into())],
},
);
}
#[test]
fn hdel_removes_field() {
let Some(url) = redis_url() else { return; };
let key = unique_key("hash-del");
let v = run(
ROUND_TRIP_SRC,
"hash_del_then_get",
vec![Value::Str(url.into()), Value::Str(key.into())],
policy_with_net(),
);
assert_eq!(v, Value::Variant { name: "None".into(), args: vec![] });
}