#![allow(clippy::unwrap_used, clippy::expect_used)] //! What `--mode` actually changes in a tool's output. //! //! The formatter's own tests cover each syntax in isolation; these pin what //! reaches a caller, which is a JSON value rather than a source snippet. mod common; use beamdev::server::EvalRequest; use beamdev::server::{BeamdevServer, FormatterMode}; use common::{Runtime, TestNode}; use rmcp::handler::server::wrapper::Parameters; use serde_json::Value; /// Evaluates `code` against a fresh node on a server in `mode`, returning the /// `result` field a caller would receive. async fn eval_in_mode(mode: FormatterMode, code: &str) -> Value { let node = TestNode::start(Runtime::Erlang).await; // The server connects for itself, under its own local node name. node.manager().disconnect().await; let server = BeamdevServer::new(mode, node.name.clone(), common::COOKIE.to_string()); let result = server .tool_eval(Parameters(EvalRequest { code: code.to_string(), language: None, timeout_ms: Some(5_000), })) .await .expect("eval"); assert_ne!(result.is_error, Some(true), "{:?}", result.content); result .structured_content .expect("structured content") .get("result") .expect("result field") .clone() } #[tokio::test] async fn atoms_carry_each_language_s_sigil() { let code = "[ok, value]."; assert_eq!( eval_in_mode(FormatterMode::Erlang, code).await, serde_json::json!(["ok", "value"]) ); assert_eq!( eval_in_mode(FormatterMode::Elixir, code).await, serde_json::json!([":ok", ":value"]) ); assert_eq!( eval_in_mode(FormatterMode::Lfe, code).await, serde_json::json!(["'ok", "'value"]) ); } /// Gleam has no atom literal. It used to annotate one with a source comment, /// which is nonsense in a JSON value. #[tokio::test] async fn a_gleam_atom_is_a_bare_value_not_an_annotated_one() { let out = eval_in_mode(FormatterMode::Gleam, "[ok, value].").await; assert_eq!(out, serde_json::json!(["ok", "value"])); let rendered = serde_json::to_string(&out).unwrap(); assert!( !rendered.contains("//"), "leaked a source comment: {}", rendered ); } /// A map becomes a JSON object, and its keys stay bare: only values are /// formatted, because a JSON key has to be a plain string. #[tokio::test] async fn map_keys_stay_bare_while_values_are_formatted() { let out = eval_in_mode(FormatterMode::Elixir, "#{key => value}.").await; assert_eq!(out, serde_json::json!({"key": ":value"})); } /// A two-element tuple led by an atom is special-cased into a JSON object, so /// `{ok, V}` reads as `{"ok": V}` rather than a two-element array. As with a /// map, the key is bare. #[tokio::test] async fn an_atom_led_pair_becomes_an_object_with_a_bare_key() { let out = eval_in_mode(FormatterMode::Elixir, "{ok, value}.").await; assert_eq!(out, serde_json::json!({"ok": ":value"})); } /// Structure is JSON in every mode: only the scalars differ. #[tokio::test] async fn lists_and_tuples_are_json_regardless_of_mode() { for mode in [ FormatterMode::Erlang, FormatterMode::Elixir, FormatterMode::Gleam, FormatterMode::Lfe, ] { let out = eval_in_mode(mode, "{1, [2, 3]}.").await; assert_eq!(out, serde_json::json!([1, [2, 3]]), "mode {}", mode); } } #[tokio::test] async fn references_are_rendered_per_language() { let erlang = eval_in_mode(FormatterMode::Erlang, "make_ref().").await; let elixir = eval_in_mode(FormatterMode::Elixir, "make_ref().").await; let gleam = eval_in_mode(FormatterMode::Gleam, "make_ref().").await; let lfe = eval_in_mode(FormatterMode::Lfe, "make_ref().").await; assert!(erlang.as_str().unwrap().starts_with("#Ref<"), "{}", erlang); assert!(lfe.as_str().unwrap().starts_with("#Ref<"), "{}", lfe); assert!( elixir.as_str().unwrap().starts_with("#Reference<"), "{}", elixir ); assert!(gleam.as_str().unwrap().starts_with("//ref<"), "{}", gleam); } /// A binary of valid UTF-8 becomes a plain JSON string without the formatter /// seeing it, so only a non-UTF-8 one reaches `format_binary`. #[tokio::test] async fn a_non_utf8_binary_is_rendered_per_language() { let code = "<<255, 254, 253>>."; assert_eq!( eval_in_mode(FormatterMode::Erlang, code).await, serde_json::json!("<<255,254,253>>") ); assert_eq!( eval_in_mode(FormatterMode::Elixir, code).await, serde_json::json!("<<255, 254, 253>>") ); assert_eq!( eval_in_mode(FormatterMode::Gleam, code).await, serde_json::json!("<<255, 254, 253>>") ); assert_eq!( eval_in_mode(FormatterMode::Lfe, code).await, serde_json::json!("#B(255 254 253)") ); } /// A bit string has no JSON equivalent, so it falls through to `format_term`. #[tokio::test] async fn a_bit_string_falls_through_to_the_formatter() { let code = "<<1:4>>."; assert_eq!( eval_in_mode(FormatterMode::Erlang, code).await, serde_json::json!("<<4 bits>>") ); assert_eq!( eval_in_mode(FormatterMode::Elixir, code).await, serde_json::json!("<<_::4 bits>>") ); assert_eq!( eval_in_mode(FormatterMode::Gleam, code).await, serde_json::json!("<<_::4 bits>>") ); assert_eq!( eval_in_mode(FormatterMode::Lfe, code).await, serde_json::json!("#B(4 bits)") ); } /// A bit string spanning more than one byte counts the whole bytes too. #[tokio::test] async fn a_multi_byte_bit_string_counts_every_bit() { assert_eq!( eval_in_mode(FormatterMode::Erlang, "<<255, 1:4>>.").await, serde_json::json!("<<12 bits>>") ); } /// A map key that is neither an atom nor a binary has no JSON form either, so /// it is rendered by the formatter and used as the object key. #[tokio::test] async fn a_tuple_map_key_is_rendered_per_language() { let code = "#{{a, b} => 1}."; assert_eq!( eval_in_mode(FormatterMode::Erlang, code).await, serde_json::json!({"{a, b}": 1}) ); assert_eq!( eval_in_mode(FormatterMode::Elixir, code).await, serde_json::json!({"{:a, :b}": 1}) ); assert_eq!( eval_in_mode(FormatterMode::Gleam, code).await, serde_json::json!({"#(a, b)": 1}) ); assert_eq!( eval_in_mode(FormatterMode::Lfe, code).await, serde_json::json!({"#('a 'b)": 1}) ); }