spec/roundtrip_spec.cr @ trunk
spec
scale = 0.75 fontscale = 1.500 box wid 1.380 ht 0.340 with .nw at (0.035, -0.035) fill 0xCBD5E1 color 0xCBD5E1 box wid 1.380 ht 0.340 with .nw at (0.000, 0.000) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.022) fill 0xffffff color 0xffffff line from (0.140, 0.000) to (0.140, -0.340) color white line from (1.240, 0.000) to (1.240, -0.340) color white text "require 'spec'" color white at (0.690, -0.170)→ /pikchrshow
crystal source
require "spec"
json block
{
"kind": "require",
"name": "spec",
"doc": null,
"children": []
}
json
scale = 0.75 fontscale = 1.500 box wid 1.330 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1 box wid 1.330 ht 0.340 with .nw at (0.000, -0.420) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.443) fill 0xffffff color 0xffffff line from (0.140, -0.420) to (0.140, -0.760) color white line from (1.190, -0.420) to (1.190, -0.760) color white text "require 'json'" color white at (0.665, -0.590)→ /pikchrshow
crystal source
require "json"
json block
{
"kind": "require",
"name": "json",
"doc": null,
"children": []
}
compiler/crystal/syntax
scale = 0.75 fontscale = 1.500 box wid 2.796 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.796 ht 0.340 with .nw at (0.000, -0.840) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -0.863) fill 0xffffff color 0xffffff line from (0.140, -0.840) to (0.140, -1.180) color white line from (2.656, -0.840) to (2.656, -1.180) color white text "require 'compiler/crystal/syntax'" color white at (1.398, -1.010)→ /pikchrshow
crystal source
require "compiler/crystal/syntax"
json block
{
"kind": "require",
"name": "compiler/crystal/syntax",
"doc": null,
"children": []
}
../src/gpcr/render
scale = 0.75 fontscale = 1.500 box wid 2.343 ht 0.340 with .nw at (0.035, -1.295) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.343 ht 0.340 with .nw at (0.000, -1.260) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.283) fill 0xffffff color 0xffffff line from (0.140, -1.260) to (0.140, -1.600) color white line from (2.203, -1.260) to (2.203, -1.600) color white text "require '../src/gpcr/render'" color white at (1.172, -1.430)→ /pikchrshow
crystal source
require "../src/gpcr/render"
json block
{
"kind": "require",
"name": "../src/gpcr/render",
"doc": null,
"children": []
}
../src/gpcr/draw
scale = 0.75 fontscale = 1.500 box wid 2.215 ht 0.340 with .nw at (0.035, -1.715) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.215 ht 0.340 with .nw at (0.000, -1.680) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.703) fill 0xffffff color 0xffffff line from (0.140, -1.680) to (0.140, -2.020) color white line from (2.075, -1.680) to (2.075, -2.020) color white text "require '../src/gpcr/draw'" color white at (1.108, -1.850)→ /pikchrshow
crystal source
require "../src/gpcr/draw"
json block
{
"kind": "require",
"name": "../src/gpcr/draw",
"doc": null,
"children": []
}
../src/gpcr/roundtrip
scale = 0.75 fontscale = 1.500 box wid 2.530 ht 0.340 with .nw at (0.035, -2.135) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.530 ht 0.340 with .nw at (0.000, -2.100) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.123) fill 0xffffff color 0xffffff line from (0.140, -2.100) to (0.140, -2.440) color white line from (2.390, -2.100) to (2.390, -2.440) color white text "require '../src/gpcr/roundtrip'" color white at (1.265, -2.270)→ /pikchrshow
crystal source
require "../src/gpcr/roundtrip"
json block
{
"kind": "require",
"name": "../src/gpcr/roundtrip",
"doc": null,
"children": []
}
private def compile_and_invoke(reconstructed : String, invoke_stmt : String, require_path : String = "../../src/gpcr") : String
checkout = File.expand_path(File.join(__DIR__, ".."))
tmp_path = File.join(checkout, "spec", "fixtures", "_invoke_check_#{Random.rand(1_000_000)}_tmp.cr")
full_src = %(require "#{require_path}"\n\n) + reconstructed + "\n#{invoke_stmt}\n"
begin
File.write(tmp_path, full_src)
stdout_io = IO::Memory.new
stderr_io = IO::Memory.new
result = Process.run("crystal", ["run", tmp_path], chdir: checkout, output: stdout_io, error: stderr_io)
result.success?.should be_true, "crystal run failed:\n#{stderr_io}"
stdout_io.to_s
ensure
File.delete(tmp_path) if File.exists?(tmp_path)
end
end
# Simple graph fixture — no doc, no require, only unambiguous arg types.
RT_GRAPH_SRC = <<-'CRYSTAL'
@[GPCR::Graph]
def blink
on_start
forever do
set_user_led true
wait 0.5
set_user_led false
wait 0.5
end
end
CRYSTAL
# Block fixture — body has a single call with no args.
RT_BLOCK_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :command, label: "hello world", category: "Test")]
def hello_world
on_start
end
CRYSTAL
# Block with a typed param. Body is deliberately invoke-safe (no on_start —
# that blocks forever waiting on GPCR::Runtime's "start" channel, which
# nothing fires in a standalone invoke check) so the round-trip spec can
# actually CALL the reconstructed def, not just compile it uncalled.
RT_BLOCK_INPUTS_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :reporter, label: "greet #{n} times", category: "Test", returns: :number)]
def greet(n : Int32) : Int32
n + 1
end
CRYSTAL
# fmt fixture — operator call body, typed Float64 param; used with face_map.
RT_FMT_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :reporter, label: "format #{v}", category: "Draw", returns: :string)]
def fmt(v : Float64) : String
"%.3f" % v
end
CRYSTAL
# Record fixture.
RT_RECORD_SRC = <<-'CRYSTAL'
@[GPCR::Record]
record Point, x : Int32, y : Int32
CRYSTAL
# Constant fixture — integer value, no ambiguity.
RT_CONSTANT_SRC = <<-'CRYSTAL'
@[GPCR::Constant]
MAX_SIZE = 100
CRYSTAL
# Table schema fixture — no rows, clean type names.
RT_TABLE_SRC = <<-'CRYSTAL'
@[GPCR::TableSchema]
class Coord
property x : Int32
property y : Int32
end
CRYSTAL
# Block with if/while/case body nodes.
RT_BLOCK_CONTROL_SRC = <<-'CRYSTAL'
@[GPCR::Block(shape: :command, label: "control demo", category: "Test")]
def control_demo
if true
on_start
end
while false
on_start
end
end
CRYSTAL
describe "gpcr_block_json_to_crystal roundtrip" do
describe ":graph kind — blink fixture" do
it "reconstructed source round-trips to the same JSON" do
original_json = gpcr_serialize_node(RT_GRAPH_SRC, "blink")
original_json.should_not be_empty
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("@[GPCR::Graph]")
reconstructed.should contain("def blink")
roundtrip_json = gpcr_serialize_node(reconstructed, "blink")
original_json.should eq(roundtrip_json)
end
it "reconstructed source contains the c_call body structure" do
json = gpcr_serialize_node(RT_GRAPH_SRC, "blink")
reconstructed = gpcr_block_json_to_crystal(json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("forever do")
reconstructed.should contain("set_user_led true")
reconstructed.should contain("wait 0.5")
reconstructed.should contain("end")
end
end
describe ":block kind" do
it "reconstructed source round-trips to the same JSON" do
original_json = gpcr_serialize_node(RT_BLOCK_SRC, "hello_world")
original_json.should_not be_empty
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("@[GPCR::Block(")
reconstructed.should contain("def hello_world")
roundtrip_json = gpcr_serialize_node(reconstructed, "hello_world")
original_json.should eq(roundtrip_json)
end
it "block with inputs round-trips WITH its parameter (ticket a4942e5c88) and is actually callable" do
original_json = gpcr_serialize_node(RT_BLOCK_INPUTS_SRC, "greet")
original_json.should_not be_empty
parsed_in = JSON.parse(original_json)
parsed_in["inputs"]?.should_not be_nil
parsed_in["inputs"].as_a.first["name"].as_s.should eq("n")
parsed_in["inputs"].as_a.first["type"].as_s.should eq("Int32")
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("def greet(n : Int32)")
# Invoke check: a dropped parameter would compile cleanly as def greet()
# but fail here with "wrong number of arguments" — proving the
# signature, not just the declaration, survived reconstruction.
compile_and_invoke(reconstructed, "puts greet(3)").strip.should eq("4")
roundtrip_json = gpcr_serialize_node(reconstructed, "greet")
original_json.should eq(roundtrip_json)
end
it "block with if/while body round-trips to the same JSON" do
original_json = gpcr_serialize_node(RT_BLOCK_CONTROL_SRC, "control_demo")
original_json.should_not be_empty
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
roundtrip_json = gpcr_serialize_node(reconstructed, "control_demo")
original_json.should eq(roundtrip_json)
end
it "fmt — operator-call body and typed param reconstruct as parseable Crystal" do
faces = GPCR.blocks_of
face_map = faces.reduce({} of String => GPCR::Face) { |m, f| m[f.name] = f; m }
original_json = gpcr_serialize_node(RT_FMT_SRC, "fmt", face_map)
original_json.should_not be_empty
parsed_in = JSON.parse(original_json)
parsed_in["inputs"]?.should_not be_nil
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("def fmt")
reconstructed.should contain("Float64")
reconstructed.should contain(%("%.3f" % v))
# Invoke check: proves the Float64 param is real, not just declared.
compile_and_invoke(reconstructed, "puts fmt(3.14159)").strip.should eq("3.142")
roundtrip_json = gpcr_serialize_node(reconstructed, "fmt", face_map)
original_json.should eq(roundtrip_json)
end
end
describe ":other body node — no-op (Q3)" do
it "returns empty string for :primitive root" do
json = %({"kind":"primitive","name":"foo","doc":null,"children":[]})
result = gpcr_block_json_to_crystal(json)
Crystal::Parser.new(result).parse
result.should eq("")
end
it "returns empty string for unknown root kind" do
json = %({"kind":"require","name":"foo/bar","doc":null,"children":[]})
result = gpcr_block_json_to_crystal(json)
Crystal::Parser.new(result).parse
result.should eq("")
end
it "other body nodes emit text verbatim" do
other_src = <<-'CRYSTAL'
@[GPCR::Graph]
def other_demo
x = 1 + 2
end
CRYSTAL
other_json = gpcr_serialize_node(other_src, "other_demo")
reconstructed = gpcr_block_json_to_crystal(other_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("def other_demo")
end
end
describe ":record kind (Q4)" do
it "reconstructed source round-trips to the same JSON" do
original_json = gpcr_serialize_node(RT_RECORD_SRC, "Point")
original_json.should_not be_empty
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("@[GPCR::Record]")
reconstructed.should contain("record Point")
reconstructed.should contain("x : Int32")
roundtrip_json = gpcr_serialize_node(reconstructed, "Point")
original_json.should eq(roundtrip_json)
end
end
describe ":constant kind (Q4)" do
it "reconstructed source round-trips to the same JSON" do
original_json = gpcr_serialize_node(RT_CONSTANT_SRC, "MAX_SIZE")
original_json.should_not be_empty
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("@[GPCR::Constant]")
reconstructed.should contain("MAX_SIZE = 100")
roundtrip_json = gpcr_serialize_node(reconstructed, "MAX_SIZE")
original_json.should eq(roundtrip_json)
end
end
describe ":table kind (Q4)" do
it "empty-rows table round-trips to the same JSON" do
original_json = gpcr_serialize_node(RT_TABLE_SRC, "Coord")
original_json.should_not be_empty
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("@[GPCR::TableSchema]")
reconstructed.should contain("struct Coord")
roundtrip_json = gpcr_serialize_node(reconstructed, "Coord")
original_json.should eq(roundtrip_json)
end
it "PACKAGES table with rows — string cells quoted, bool unquoted, JSON round-trips, compiles" do
pkg_src = File.read(File.join(__DIR__, "..", "examples", "toolchain_packages.cr"))
original_json = gpcr_serialize_node(pkg_src, "PACKAGES")
original_json.should_not be_empty
original_parsed = JSON.parse(original_json)
original_parsed["type_name"]?.try(&.as_s).should eq("Package")
reconstructed = gpcr_block_json_to_crystal(original_json)
Crystal::Parser.new(reconstructed).parse
reconstructed.should contain("@[GPCR::TableSchema]")
reconstructed.should contain("struct Package")
reconstructed.should contain("PACKAGES = [")
reconstructed.should contain("Package.new(")
reconstructed.should contain(%("--disable-nls --with-sysroot"))
# Compile check: constant names are distinct so no "already initialized constant" error.
checkout = File.expand_path(File.join(__DIR__, ".."))
tmp_path = File.join(checkout, "spec", "fixtures", "_pkg_roundtrip_tmp.cr")
begin
File.write(tmp_path, %(require "../../src/gpcr"\n\n) + reconstructed)
stderr_io = IO::Memory.new
compile_result = Process.run(
"crystal", ["build", "--no-codegen", tmp_path],
chdir: checkout,
output: Process::Redirect::Close,
error: stderr_io
)
compile_result.success?.should be_true, "crystal build --no-codegen failed:\n#{stderr_io}"
ensure
File.delete(tmp_path) if File.exists?(tmp_path)
end
roundtrip_json = gpcr_serialize_node(reconstructed, "PACKAGES")
original_json.should eq(roundtrip_json)
end
end
end