defmodule CljElixir.REPLTest do use ExUnit.Case, async: true alias CljElixir.REPL describe "REPL.new" do test "creates initial state" do state = REPL.new() assert state.bindings == [] assert state.history == [] assert state.counter == 1 end end describe "REPL.eval" do test "evaluates simple expression" do state = REPL.new() assert {:ok, "3", _} = REPL.eval("(+ 1 2)", state) end test "pr-str formats output" do state = REPL.new() {:ok, result, _} = REPL.eval("\"hello\"", state) assert result == "\"hello\"" end test "nil result" do state = REPL.new() {:ok, result, _} = REPL.eval("nil", state) assert result == "nil" end test "map result" do state = REPL.new() {:ok, result, _} = REPL.eval("{:a 1 :b 2}", state) assert result =~ ":a" assert result =~ "1" end test "increments counter" do state = REPL.new() {:ok, _, state2} = REPL.eval("1", state) assert state2.counter == 2 {:ok, _, state3} = REPL.eval("2", state2) assert state3.counter == 3 end test "stores history" do state = REPL.new() {:ok, _, state2} = REPL.eval("(+ 1 2)", state) assert state2.history == ["(+ 1 2)"] {:ok, _, state3} = REPL.eval("(+ 3 4)", state2) assert state3.history == ["(+ 3 4)", "(+ 1 2)"] end test "error returns error tuple" do state = REPL.new() {:error, msg, _} = REPL.eval("(defmodule REPLErrTest (invalid-syntax", state) assert is_binary(msg) assert msg =~ "Error" or msg =~ "error" end test "defmodule persists across evals" do state = REPL.new() {:ok, _, state2} = REPL.eval(""" (defmodule REPLTestMod (defn hello [] :hi)) """, state) {:ok, result, _} = REPL.eval("(REPLTestMod/hello)", state2) assert result == ":hi" end test "tuple result" do state = REPL.new() {:ok, result, _} = REPL.eval("#el[:ok 42]", state) assert result == "#el[:ok 42]" end test "list result" do state = REPL.new() {:ok, result, _} = REPL.eval("(list 1 2 3)", state) assert result == "(1 2 3)" end end describe "ns tracking" do test "ns sets current namespace" do state = REPL.new() assert REPL.current_ns(state) == "user" {:ok, _, state2} = REPL.eval("(ns ReplNsTest1)", state) assert state2.current_ns == "ReplNsTest1" assert REPL.current_ns(state2) == "ReplNsTest1" end test "ns with defn creates module" do state = REPL.new() {:ok, _, state2} = REPL.eval(""" (ns ReplNsTest2) (defn greet [name] (str "hello " name)) """, state) assert state2.current_ns == "ReplNsTest2" {:ok, result, _} = REPL.eval("(ReplNsTest2/greet \"world\")", state2) assert result == "\"hello world\"" end test "bare defn in active ns updates module" do state = REPL.new() # Set up initial module {:ok, _, state2} = REPL.eval(""" (ns ReplNsTest3) (defn hello [] :hi) """, state) # Add a new function without ns {:ok, result, state3} = REPL.eval("(defn world [] :earth)", state2) assert result == "#'ReplNsTest3/world" # Both functions should work {:ok, r1, _} = REPL.eval("(ReplNsTest3/hello)", state3) assert r1 == ":hi" {:ok, r2, _} = REPL.eval("(ReplNsTest3/world)", state3) assert r2 == ":earth" end test "redefine function in active ns" do state = REPL.new() {:ok, _, state2} = REPL.eval(""" (ns ReplNsTest4) (defn greet [] "v1") """, state) {:ok, r1, _} = REPL.eval("(ReplNsTest4/greet)", state2) assert r1 == "\"v1\"" # Redefine greet {:ok, _, state3} = REPL.eval("(defn greet [] \"v2\")", state2) {:ok, r2, _} = REPL.eval("(ReplNsTest4/greet)", state3) assert r2 == "\"v2\"" end test "bare defn without ns evals plain" do state = REPL.new() # No namespace set — def outside module should fail {:error, _, _} = REPL.eval("(defn orphan [] :lost)", state) end test "expressions eval normally in ns context" do state = REPL.new() {:ok, _, state2} = REPL.eval(""" (ns ReplNsTest5) (defn add [a b] (+ a b)) """, state) # Expression (no def) goes through plain eval {:ok, result, _} = REPL.eval("(ReplNsTest5/add 3 4)", state2) assert result == "7" end test "mixed defs and exprs in ns context" do state = REPL.new() {:ok, _, state2} = REPL.eval(""" (ns ReplNsTest6) (defn double [x] (* x 2)) """, state) # Eval def + expression together {:ok, result, state3} = REPL.eval(""" (defn triple [x] (* x 3)) (ReplNsTest6/triple 5) """, state2) # Result should be the expression value assert result == "15" # Both functions should work {:ok, r1, _} = REPL.eval("(ReplNsTest6/double 4)", state3) assert r1 == "8" end test "switching namespace" do state = REPL.new() {:ok, _, state2} = REPL.eval(""" (ns ReplNsTestA) (defn a-fn [] :from-a) """, state) {:ok, _, state3} = REPL.eval(""" (ns ReplNsTestB) (defn b-fn [] :from-b) """, state2) assert state3.current_ns == "ReplNsTestB" # Both modules still work {:ok, r1, _} = REPL.eval("(ReplNsTestA/a-fn)", state3) assert r1 == ":from-a" {:ok, r2, _} = REPL.eval("(ReplNsTestB/b-fn)", state3) assert r2 == ":from-b" end end describe "REPL.balanced?" do test "balanced parens" do assert REPL.balanced?("(+ 1 2)") assert REPL.balanced?("(defn foo [x] (+ x 1))") assert REPL.balanced?("42") assert REPL.balanced?("") end test "unbalanced parens" do refute REPL.balanced?("(+ 1 2") refute REPL.balanced?("(defn foo [x]") refute REPL.balanced?("(let [x 1") end test "balanced with nested" do assert REPL.balanced?("(let [x (+ 1 2)] (* x x))") end test "string contents not counted" do assert REPL.balanced?("(str \"(hello)\")") assert REPL.balanced?("(str \"[not real]\")") end test "comment contents not counted" do assert REPL.balanced?("(+ 1 2) ; this has unbalanced (") end test "mixed delimiters" do assert REPL.balanced?("(let [{:keys [a b]} {:a 1 :b 2}] (+ a b))") refute REPL.balanced?("(let [{:keys [a b]} {:a 1 :b 2}] (+ a b)") end end end