beamr 0.4.6

A Rust runtime with the BEAM's execution model, targeting Gleam
Documentation
use std::sync::Arc;

use crate::atom::{Atom, AtomTable};
use crate::native::ProcessContext;
use crate::process::Process;
use crate::term::Term;
use crate::term::binary_ref::BinaryRef;
use crate::term::boxed::{Cons, Float, Map, Tuple};

use super::json_bifs::{
    bif_json_decode, bif_json_encode, bif_json_encode_binary, bif_json_encode_float,
    bif_json_encode_integer,
};

fn context(process: &mut Process) -> ProcessContext<'_> {
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(Arc::new(AtomTable::with_common_atoms())));
    context.attach_process(process, 0);
    context
}

fn binary_string(term: Term) -> String {
    let binary = BinaryRef::new(term).expect("binary result");
    String::from_utf8(binary.as_bytes().to_vec()).expect("utf8")
}

#[test]
fn encode_integer_formats_small_and_big() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);

    let small = bif_json_encode_integer(&[Term::small_int(-42)], &mut ctx).expect("small");
    assert_eq!(binary_string(small), "-42");

    let big = ctx.alloc_bigint(false, &[u64::MAX]).expect("bignum");
    let encoded = bif_json_encode_integer(&[big], &mut ctx).expect("big");
    assert_eq!(binary_string(encoded), "18446744073709551615");
}

#[test]
fn encode_float_keeps_a_decimal_point() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);

    let one = ctx.alloc_float(1.0).expect("float");
    let encoded = bif_json_encode_float(&[one], &mut ctx).expect("encode");
    assert_eq!(binary_string(encoded), "1.0");

    let fractional = ctx.alloc_float(-2.5).expect("float");
    let encoded = bif_json_encode_float(&[fractional], &mut ctx).expect("encode");
    assert_eq!(binary_string(encoded), "-2.5");

    let infinity = ctx.alloc_float(f64::INFINITY).expect("float");
    assert!(bif_json_encode_float(&[infinity], &mut ctx).is_err());
}

#[test]
fn encode_binary_escapes_json_string_characters() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);

    let mut raw = b"say \"hi\" back".to_vec();
    raw.push(b'\\');
    raw.push(b'\n');
    raw.push(0x01);
    let input = ctx.alloc_binary(&raw).expect("binary");
    let encoded = bif_json_encode_binary(&[input], &mut ctx).expect("encode");
    let mut expected = String::from("\"say \\\"hi\\\" back");
    expected.push_str("\\\\");
    expected.push_str("\\n");
    expected.push_str("\\u0001");
    expected.push('"');
    assert_eq!(binary_string(encoded), expected);
}

#[test]
fn encode_term_handles_nested_structures() {
    let mut process = Process::new(1, 512);
    let mut ctx = context(&mut process);

    let label = ctx.alloc_binary(b"total").expect("key");
    let list = {
        let one = ctx.alloc_cons(Term::small_int(2), Term::NIL).expect("cons");
        ctx.alloc_cons(Term::small_int(1), one).expect("cons")
    };
    let map = ctx.alloc_map(&[label], &[list]).expect("map");
    let encoded = bif_json_encode(&[map], &mut ctx).expect("encode");
    assert_eq!(binary_string(encoded), r#"{"total":[1,2]}"#);
}

#[test]
fn decode_parses_objects_arrays_and_scalars() {
    let mut process = Process::new(1, 4096);
    let mut ctx = context(&mut process);

    let input = ctx
        .alloc_binary(br#" {"a": [1, -2.5, "xA", true, false, null], "b": 9} "#)
        .expect("binary");
    let decoded = bif_json_decode(&[input], &mut ctx).expect("decode");
    let map = Map::new(decoded).expect("object decodes to a map");
    assert_eq!(map.len(), 2);

    let key_a = ctx.alloc_binary(b"a").expect("key");
    let array = map.get(key_a).expect("a present");
    let cons = Cons::new(array).expect("array decodes to a list");
    assert_eq!(cons.head().as_small_int(), Some(1));
    let cons = Cons::new(cons.tail()).expect("second");
    let float = Float::new(cons.head()).expect("float element");
    assert!((float.value() - -2.5).abs() < f64::EPSILON);
    let cons = Cons::new(cons.tail()).expect("third");
    let text = BinaryRef::new(cons.head()).expect("string element");
    assert_eq!(text.as_bytes(), b"xA");
    let cons = Cons::new(cons.tail()).expect("fourth");
    assert_eq!(cons.head(), Term::atom(Atom::TRUE));
    let cons = Cons::new(cons.tail()).expect("fifth");
    assert_eq!(cons.head(), Term::atom(Atom::FALSE));
    let cons = Cons::new(cons.tail()).expect("sixth");
    let table = ctx.atom_table_arc().expect("atoms");
    assert_eq!(cons.head(), Term::atom(table.intern("null")));
    assert!(cons.tail().is_nil());

    let key_b = ctx.alloc_binary(b"b").expect("key");
    assert_eq!(map.get(key_b).and_then(Term::as_small_int), Some(9));
}

#[test]
fn decode_reports_otp_error_reasons() {
    let mut process = Process::new(1, 512);
    let mut ctx = context(&mut process);
    let table = ctx.atom_table_arc().expect("atoms");

    let truncated = ctx.alloc_binary(b"{\"a\": 1").expect("binary");
    let error = bif_json_decode(&[truncated], &mut ctx).expect_err("truncated input");
    assert_eq!(error, Term::atom(table.intern("unexpected_end")));

    let invalid = ctx.alloc_binary(b"{\"a\" 1}").expect("binary");
    let error = bif_json_decode(&[invalid], &mut ctx).expect_err("invalid byte");
    let tuple = Tuple::new(error).expect("invalid_byte tuple");
    assert_eq!(tuple.get(0), Some(Term::atom(table.intern("invalid_byte"))));
    assert_eq!(
        tuple.get(1).and_then(Term::as_small_int),
        Some(i64::from(b'1'))
    );

    let trailing = ctx.alloc_binary(b"1 x").expect("binary");
    let error = bif_json_decode(&[trailing], &mut ctx).expect_err("trailing garbage");
    let tuple = Tuple::new(error).expect("invalid_byte tuple");
    assert_eq!(tuple.get(0), Some(Term::atom(table.intern("invalid_byte"))));
}

#[test]
fn decode_handles_bignums_and_surrogate_pairs() {
    let mut process = Process::new(1, 1024);
    let mut ctx = context(&mut process);

    let big = ctx
        .alloc_binary(b"123456789012345678901234567890")
        .expect("binary");
    let decoded = bif_json_decode(&[big], &mut ctx).expect("bignum decodes");
    let encoded = bif_json_encode_integer(&[decoded], &mut ctx).expect("round trip");
    assert_eq!(binary_string(encoded), "123456789012345678901234567890");

    let emoji = ctx.alloc_binary(b"\"\\uD83D\\uDE00\"").expect("binary");
    let decoded = bif_json_decode(&[emoji], &mut ctx).expect("surrogate pair decodes");
    let text = BinaryRef::new(decoded).expect("string");
    assert_eq!(text.as_bytes(), "😀".as_bytes());
}