Login
gpcr/spec/self_host_spec.cr
Login

[edit]

spec/self_host_spec.cr @ trunk

spec

require '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)

crystal source

require "spec"

json block

{
  "kind": "require",
  "name": "spec",
  "doc": null,
  "children": []
}

../src/gpcr

require '../src/gpcr'
scale = 0.75
fontscale = 1.500
box wid 1.783 ht 0.340 with .nw at (0.035, -0.455) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.783 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.643, -0.420) to (1.643, -0.760) color white
text "require '../src/gpcr'" color white at (0.891, -0.590)

crystal source

require "../src/gpcr"

json block

{
  "kind": "require",
  "name": "../src/gpcr",
  "doc": null,
  "children": []
}

../src/gpcr/std

require '../src/gpcr/std'
scale = 0.75
fontscale = 1.500
box wid 2.068 ht 0.340 with .nw at (0.035, -0.875) fill 0xCBD5E1 color 0xCBD5E1
box wid 2.068 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 (1.928, -0.840) to (1.928, -1.180) color white
text "require '../src/gpcr/std'" color white at (1.034, -1.010)

crystal source

require "../src/gpcr/std"

json block

{
  "kind": "require",
  "name": "../src/gpcr/std",
  "doc": null,
  "children": []
}

../src/gpcr/render

require '../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)

crystal source

require "../src/gpcr/render"

json block

{
  "kind": "require",
  "name": "../src/gpcr/render",
  "doc": null,
  "children": []
}

double_say

a block whose body is calls-only: passes the contract

double say  text 1 say  text say  text
scale = 0.75
fontscale = 1.500
box wid 1.841 ht 0.340 rad 0.035 with .nw at (0.035, -1.715) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.841 ht 0.340 rad 0.035 with .nw at (0.000, -1.680) fill 0x4C97FF color 0x4C97FF
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -1.703) fill 0xffffff color 0xffffff
text "double say " color white at (0.593, -1.850)
box wid 0.535 ht 0.205 rad 0.102 with .nw at (1.105, -1.748) fill 0xFFFFFF color 0xFFFFFF
text "text" color 0x263238 at (1.373, -1.850)
box wid 0.240 ht 0.220 rad 0.110 at (1.691, -1.820) fill 0x334155 color 0x334155
text "1" at (1.691, -1.820) color 0xf8fafc
box wid 1.305 ht 0.340 rad 0.035 with .nw at (0.035, -2.135) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.305 ht 0.340 rad 0.035 with .nw at (0.000, -2.100) fill 0x4C97FF color 0x4C97FF
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.123) fill 0xffffff color 0xffffff
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.472) fill 0x4C97FF color 0x4C97FF
text "say " color white at (0.302, -2.270)
box wid 0.535 ht 0.205 rad 0.102 with .nw at (0.570, -2.167) fill 0xFFFFFF color 0xFFFFFF
text "text" color 0x263238 at (0.837, -2.270)
box wid 1.305 ht 0.340 rad 0.035 with .nw at (0.035, -2.555) fill 0xCBD5E1 color 0xCBD5E1
box wid 1.305 ht 0.340 rad 0.035 with .nw at (0.000, -2.520) fill 0x4C97FF color 0x4C97FF
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.542) fill 0xffffff color 0xffffff
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.892) fill 0x4C97FF color 0x4C97FF
text "say " color white at (0.302, -2.690)
box wid 0.535 ht 0.205 rad 0.102 with .nw at (0.570, -2.587) fill 0xFFFFFF color 0xFFFFFF
text "text" color 0x263238 at (0.837, -2.690)
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.472) fill 0x4C97FF color 0x4C97FF
box wid 0.300 ht 0.065 rad 0.018 at (0.370, -2.892) fill 0x4C97FF color 0x4C97FF

crystal source

  @[GPCR::Block(shape: :command, label: "double say #{text}", category: "Output")]
  def double_say(text : String)
    say text
    say text
  end

FIXTURE_FACES = GPCR.blocks_of(SelfHostFixtures)
GPCR.check_blocks(SelfHostFixtures)

TOP_FACES = GPCR.blocks_of

private def build_bad_block_fixture : {Process::Status, String}
  # Crystal requires are relative, so the fixture lives inside the tree.
  tmp = File.tempfile("gpcr_bad_block", ".cr", dir: __DIR__)
  begin
    tmp.print <<-SRC
      require "../src/gpcr"
      require "../src/gpcr/std"

      @[GPCR::Block(shape: :command, label: "leaky")]
      def leaky
        @@state = 1
      end

      GPCR.check_blocks
      SRC
    tmp.flush
    err = IO::Memory.new
    status = Process.run("crystal", ["build", "--no-codegen", tmp.path], error: err)
    {status, err.to_s}
  ensure
    tmp.delete
  end
end

describe "GPCR self-hosting" do
  describe "face reflection (GPCR.blocks_of)" do
    it "derives faces for @[GPCR::Block] defs, carrying the def name" do
      face = FIXTURE_FACES.find { |f| f.name == "double_say" }.not_nil!
      face.shape.should eq "command"
      face.label.should eq "double say \#{text}"
      face.inputs.map(&.name).should eq ["text"]
    end

    it "walks the top-level scope by default, seeing primitives and blocks alike" do
      names = TOP_FACES.map(&.name)
      names.should contain "receive_event" # primitive
      names.should contain "on_start"      # block defined in GPCR
      names.should contain "say"
      TOP_FACES.find { |f| f.name == "on_start" }.not_nil!.shape.should eq "hat"
    end
  end

  describe "Renderer.face_to_prim" do
    it "splits a reflected label into text and slot parts" do
      face = TOP_FACES.find { |f| f.name == "wait" }.not_nil!
      prim = GPCR::Renderer.face_to_prim(face)
      prim.parts.should eq [{:text, "wait "}, {:slot, "secs"}, {:text, " seconds"}]
      prim.params.should eq ["secs"]
    end
  end

  describe "the @[GPCR::Block] body contract (GPCR.check_blocks)" do
    it "rejects a non-renderable body at compile time, naming def and node kind" do
      status, err = build_bad_block_fixture
      status.success?.should be_false
      err.should contain "non-renderable element"
      err.should contain "leaky: ClassVar"
    end
  end

  describe "the event bus (runtime primitives)" do
    it "delivers a fired event to a waiting receive_event and say writes the stream" do
      out_io = IO::Memory.new
      GPCR::Runtime.output = out_io
      begin
        done = Channel(Nil).new
        spawn do
          on_start
          say "hi"
          done.send nil
        end
        Fiber.yield
        send_event "start"
        done.receive
        out_io.to_s.should eq "hi\n"
      ensure
        GPCR::Runtime.output = STDOUT
      end
    end

    it "fire-and-forget: an event with no waiting subscriber is dropped, not queued" do
      send_event "nobody-listening" # must not raise or block
    end
  end

  describe "the pure user program (examples/blink.cr)" do
    example_bin = File.tempname("blink_bin")

    it "compiles from nothing but a require and a graph" do
      src = File.read(Path[__DIR__].join("..", "examples", "blink.cr"))
      src.lines.reject(&.blank?).first.should eq %(require "../src/gpcr/main")
      src.should_not contain "spawn" # no boilerplate: the runtime include covers it
      src.should_not contain "ARGV"
      example = Path[__DIR__].join("..", "examples", "blink.cr").normalize.to_s
      err = IO::Memory.new
      status = Process.run("crystal", ["build", example, "-o", example_bin], error: err)
      status.success?.should be_true
    end

    it "runs its graphs for a bounded time via the standard entry point" do
      out_io = IO::Memory.new
      Process.run(example_bin, ["run", "0.6"], output: out_io).success?.should be_true
      out_io.to_s.should contain "\e[93m*\e[0m" # simulated LED on
      out_io.to_s.should contain "."            # simulated LED off
    end

    it "renders its own embedded source: require as pikchr block, faces across files" do
      out_io = IO::Memory.new
      Process.run(example_bin, ["render"], output: out_io).success?.should be_true
      md = out_io.to_s
      md.should contain "## ../src/gpcr/main"
      md.should contain "require '../src/gpcr/main'"
      md.should contain "*blink: LED on/off loop after start*"
      md.should contain %(text "on start")
      md.should contain %(text "set user LED " color white)
      md.should contain %(text "true" color 0x263238)
      md.should contain %(text "wait " color white)
      md.should contain %(text "0.5" color 0x263238)
      md.should contain %(text " seconds" color white)
      md.should contain %(text "forever")
    end

    it "reports its faces as JSON, palette included" do
      out_io = IO::Memory.new
      Process.run(example_bin, ["faces"], output: out_io).success?.should be_true
      names = Array(GPCR::Face).from_json(out_io.to_s).map(&.name)
      ["on_start", "forever", "set_user_led", "wait", "argument", "receive_event"].each do |n|
        names.should contain n
      end
    end
  end
end