spec/table_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": []
}
compiler/crystal/syntax
scale = 0.75 fontscale = 1.500 box wid 2.796 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.796 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 (2.656, -0.420) to (2.656, -0.760) color white text "require 'compiler/crystal/syntax'" color white at (1.398, -0.590)→ /pikchrshow
crystal source
require "compiler/crystal/syntax"
json block
{
"kind": "require",
"name": "compiler/crystal/syntax",
"doc": null,
"children": []
}
../src/gpcr/table
scale = 0.75 fontscale = 1.500 box wid 2.216 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.216 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.076, -0.840) to (2.076, -1.180) color white text "require '../src/gpcr/table'" color white at (1.108, -1.010)→ /pikchrshow
crystal source
require "../src/gpcr/table"
json block
{
"kind": "require",
"name": "../src/gpcr/table",
"doc": null,
"children": []
}
../src/gpcr/csv_io
scale = 0.75 fontscale = 1.500 box wid 2.334 ht 0.340 with .nw at (0.035, -1.295) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.334 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.194, -1.260) to (2.194, -1.600) color white text "require '../src/gpcr/csv_io'" color white at (1.167, -1.430)→ /pikchrshow
crystal source
require "../src/gpcr/csv_io"
json block
{
"kind": "require",
"name": "../src/gpcr/csv_io",
"doc": null,
"children": []
}
../src/gpcr/ops
scale = 0.75 fontscale = 1.500 box wid 2.117 ht 0.340 with .nw at (0.035, -1.715) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.117 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 (1.977, -1.680) to (1.977, -2.020) color white text "require '../src/gpcr/ops'" color white at (1.059, -1.850)→ /pikchrshow
crystal source
require "../src/gpcr/ops"
json block
{
"kind": "require",
"name": "../src/gpcr/ops",
"doc": null,
"children": []
}
../src/gpcr/apply
scale = 0.75 fontscale = 1.500 box wid 2.255 ht 0.340 with .nw at (0.035, -2.135) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.255 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.115, -2.100) to (2.115, -2.440) color white text "require '../src/gpcr/apply'" color white at (1.127, -2.270)→ /pikchrshow
crystal source
require "../src/gpcr/apply"
json block
{
"kind": "require",
"name": "../src/gpcr/apply",
"doc": null,
"children": []
}
../src/gpcr/speak
scale = 0.75 fontscale = 1.500 box wid 2.304 ht 0.340 with .nw at (0.035, -2.555) fill 0xCBD5E1 color 0xCBD5E1 box wid 2.304 ht 0.340 with .nw at (0.000, -2.520) fill 0x7C83E6 color 0x7C83E6 box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.542) fill 0xffffff color 0xffffff line from (0.140, -2.520) to (0.140, -2.860) color white line from (2.164, -2.520) to (2.164, -2.860) color white text "require '../src/gpcr/speak'" color white at (1.152, -2.690)→ /pikchrshow
crystal source
require "../src/gpcr/speak"
json block
{
"kind": "require",
"name": "../src/gpcr/speak",
"doc": null,
"children": []
}
TABLE_SRC = <<-CRYSTAL
struct Package
property name : String
property version : String
property enabled : Bool
def initialize(@name, @version, @enabled)
end
end
packages = [] of Package
packages << Package.new("binutils", "2.42", true)
packages << Package.new("gcc", "13.3.0", true)
CRYSTAL
def crystal_parse_ok?(src : String) : Bool
Crystal::Parser.new(src).parse
true
rescue
false
end
describe "GPCR::Table — model" do
describe "struct_name_for" do
it "singularises a plain plural" do
GPCR::Table.struct_name_for("packages").should eq("Package")
end
it "CamelCases a snake_case plural" do
GPCR::Table.struct_name_for("build_stages").should eq("BuildStage")
end
it "handles a single-word name ending in s" do
GPCR::Table.struct_name_for("robots").should eq("Robot")
end
it "leaves a name that doesn't end in s unchanged" do
GPCR::Table.struct_name_for("config").should eq("Config")
end
end
describe "parse_type" do
it "maps text to String" do
GPCR::Table.parse_type("text").should eq("String")
end
it "maps number to Int32" do
GPCR::Table.parse_type("number").should eq("Int32")
end
it "maps flag to Bool" do
GPCR::Table.parse_type("flag").should eq("Bool")
end
it "maps float to Float64" do
GPCR::Table.parse_type("float").should eq("Float64")
end
it "falls back to String for unknown types" do
GPCR::Table.parse_type("widget").should eq("String")
end
end
describe "emit_def" do
it "produces parseable Crystal" do
td = GPCR::Table::TableDef.new(
"packages",
[
GPCR::Table::Column.new("name", "String"),
GPCR::Table::Column.new("version", "String"),
GPCR::Table::Column.new("enabled", "Bool"),
]
)
src = GPCR::Table.emit_def(td)
crystal_parse_ok?(src).should be_true
end
it "uses struct not record so cells are mutable" do
td = GPCR::Table::TableDef.new(
"items",
[GPCR::Table::Column.new("x", "Int32")]
)
src = GPCR::Table.emit_def(td)
src.should contain("struct Item")
src.should contain("property x : Int32")
end
it "declares the typed empty array" do
td = GPCR::Table::TableDef.new(
"robots",
[GPCR::Table::Column.new("name", "String")]
)
src = GPCR::Table.emit_def(td)
src.should contain("robots = [] of Robot")
end
end
end
describe "GPCR::Ops — table op schema" do
describe "AddRowOp" do
it "parses from JSON" do
j = %({"op":"add-row","table":"packages","columns":["name","version"],"values":["gcc","13.3"]})
ops = GPCR::Ops.parse_batch(j)
ops.size.should eq(1)
op = ops[0].as(GPCR::Ops::AddRowOp)
op.table.should eq("packages")
op.columns.should eq(["name", "version"])
op.values.should eq(["gcc", "13.3"])
end
it "round-trips through op_to_json" do
op = GPCR::Ops::AddRowOp.new("packages", ["name"], ["binutils"])
json = GPCR::Ops.op_to_json(op)
reparsed = GPCR::Ops.parse_batch(json).first.as(GPCR::Ops::AddRowOp)
reparsed.table.should eq("packages")
reparsed.columns.should eq(["name"])
reparsed.values.should eq(["binutils"])
end
end
describe "SetCellOp" do
it "parses from JSON with IndexTarget" do
j = %({"op":"set-cell","table":"packages","target":{"index":1,"gen":0},"column":"version","value":"2.43"})
ops = GPCR::Ops.parse_batch(j)
op = ops[0].as(GPCR::Ops::SetCellOp)
op.table.should eq("packages")
op.column.should eq("version")
op.value.should eq("2.43")
end
it "round-trips through op_to_json" do
target = GPCR::Ops::IndexTarget.new(2, 0)
op = GPCR::Ops::SetCellOp.new("packages", target, "enabled", "false")
json = GPCR::Ops.op_to_json(op)
reparsed = GPCR::Ops.parse_batch(json).first.as(GPCR::Ops::SetCellOp)
reparsed.table.should eq("packages")
reparsed.column.should eq("enabled")
end
end
describe "DeleteRowOp" do
it "parses from JSON" do
j = %({"op":"delete-row","table":"packages","target":{"index":1,"gen":0}})
ops = GPCR::Ops.parse_batch(j)
op = ops[0].as(GPCR::Ops::DeleteRowOp)
op.table.should eq("packages")
end
it "round-trips" do
target = GPCR::Ops::IndexTarget.new(3, 0)
op = GPCR::Ops::DeleteRowOp.new("packages", target)
json = GPCR::Ops.op_to_json(op)
reparsed = GPCR::Ops.parse_batch(json).first.as(GPCR::Ops::DeleteRowOp)
reparsed.table.should eq("packages")
end
end
end
describe "GPCR::Speak — table grammar" do
describe "add row" do
it "parses add row with column-value pairs" do
result = GPCR::Speak.parse("add row packages name binutils version 2.42")
op = result.as(GPCR::Ops::AddRowOp)
op.table.should eq("packages")
op.columns.should eq(["name", "version"])
op.values.should eq(["binutils", "2.42"])
end
it "coerces word numbers in values" do
result = GPCR::Speak.parse("add row items order one")
op = result.as(GPCR::Ops::AddRowOp)
op.values.should eq(["1"])
end
it "returns ParseError for odd-length pairs" do
result = GPCR::Speak.parse("add row packages name")
result.should be_a(GPCR::Speak::ParseError)
end
end
describe "set cell" do
it "parses set COLUMN of row N in TABLE to VALUE" do
result = GPCR::Speak.parse("set version of row 1 in packages to 2.43")
op = result.as(GPCR::Ops::SetCellOp)
op.table.should eq("packages")
op.column.should eq("version")
op.value.should eq("2.43")
target = op.target.as(GPCR::Ops::IndexTarget)
target.index.should eq(1)
end
it "coerces word numbers in the row index" do
result = GPCR::Speak.parse("set name of row two in packages to musl")
op = result.as(GPCR::Ops::SetCellOp)
op.target.as(GPCR::Ops::IndexTarget).index.should eq(2)
end
end
describe "delete row" do
it "parses delete row N in TABLE" do
result = GPCR::Speak.parse("delete row 2 in packages")
op = result.as(GPCR::Ops::DeleteRowOp)
op.table.should eq("packages")
op.target.as(GPCR::Ops::IndexTarget).index.should eq(2)
end
it "returns ParseError for missing table" do
result = GPCR::Speak.parse("delete row 1")
result.should be_a(GPCR::Speak::ParseError)
end
end
describe "disambiguation" do
it "still routes 'add wait...' to block grammar" do
result = GPCR::Speak.parse("add wait 0.5 after block 1")
result.should be_a(GPCR::Ops::AddOp)
end
it "still routes 'delete block N' to block grammar" do
result = GPCR::Speak.parse("delete block 3")
result.should be_a(GPCR::Ops::DeleteOp)
end
it "still routes 'set ms of block N' to block grammar" do
result = GPCR::Speak.parse("set ms of block 2 to 500")
result.should be_a(GPCR::Ops::SetOp)
end
end
end
describe "GPCR::Ops — apply engine round-trips" do
describe "apply_add_row" do
it "inserts a new row after the last existing row" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
op = GPCR::Ops::AddRowOp.new("packages", ["name", "version", "enabled"], ["musl", "1.2.5", "false"])
buf.apply_one(op)
buf.source.should contain("musl")
buf.source.should contain("Package.new(\"musl\", \"1.2.5\", false)")
end
it "result parses as valid Crystal" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
op = GPCR::Ops::AddRowOp.new("packages", ["name", "version", "enabled"], ["musl", "1.2.5", "false"])
buf.apply_one(op)
crystal_parse_ok?(buf.source).should be_true
end
it "inserts numeric values without quotes" do
src = <<-CR
struct Stage
property order : Int32
property name : String
def initialize(@order, @name)
end
end
build_stages = [] of Stage
build_stages << Stage.new(1, "binutils")
CR
buf = GPCR::Ops::Buffer.new(src)
op = GPCR::Ops::AddRowOp.new("build_stages", ["order", "name"], ["2", "gcc"])
buf.apply_one(op)
buf.source.should contain("Stage.new(2, \"gcc\")")
end
end
describe "apply_set_cell" do
it "rewrites the targeted column in a row" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
target = GPCR::Ops::IndexTarget.new(1, buf.gen)
op = GPCR::Ops::SetCellOp.new("packages", target, "version", "2.43")
buf.apply_one(op)
buf.source.should contain("Package.new(\"binutils\", \"2.43\", true)")
end
it "result parses as valid Crystal" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
target = GPCR::Ops::IndexTarget.new(2, buf.gen)
op = GPCR::Ops::SetCellOp.new("packages", target, "enabled", "false")
buf.apply_one(op)
crystal_parse_ok?(buf.source).should be_true
end
it "raises OpError on stale generation" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
target = GPCR::Ops::IndexTarget.new(1, 99)
op = GPCR::Ops::SetCellOp.new("packages", target, "version", "2.43")
expect_raises(GPCR::Ops::OpError, /stale generation/) { buf.apply_one(op) }
end
end
describe "apply_delete_row" do
it "removes the targeted row" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
target = GPCR::Ops::IndexTarget.new(1, buf.gen)
op = GPCR::Ops::DeleteRowOp.new("packages", target)
buf.apply_one(op)
buf.source.should_not contain("binutils")
buf.source.should contain("gcc")
end
it "result parses as valid Crystal" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
target = GPCR::Ops::IndexTarget.new(2, buf.gen)
op = GPCR::Ops::DeleteRowOp.new("packages", target)
buf.apply_one(op)
crystal_parse_ok?(buf.source).should be_true
end
end
describe "undo after table op" do
it "restores source after add-row + undo" do
buf = GPCR::Ops::Buffer.new(TABLE_SRC)
original = buf.source
op = GPCR::Ops::AddRowOp.new("packages", ["name", "version", "enabled"], ["musl", "1.2.5", "false"])
buf.apply_one(op)
buf.apply_one(GPCR::Ops::UndoOp.new)
buf.source.should eq(original)
end
end
end
describe "GPCR::CSVio — round-trip" do
it "writes and reads back headers and rows" do
path = File.join(Dir.tempdir, "gpcr_test_#{Process.pid}.csv")
begin
headers = ["name", "version", "enabled"]
rows = [["binutils", "2.42", "true"], ["gcc", "13.3.0", "true"]]
GPCR::CSVio.write(path, headers, rows)
h2, r2 = GPCR::CSVio.read(path)
h2.should eq(headers)
r2.should eq(rows)
ensure
File.delete(path) if File.exists?(path)
end
end
it "returns empty arrays for an empty file" do
path = File.join(Dir.tempdir, "gpcr_test_empty_#{Process.pid}.csv")
begin
File.write(path, "")
h, r = GPCR::CSVio.read(path)
h.should be_empty
r.should be_empty
ensure
File.delete(path) if File.exists?(path)
end
end
end