diff --git a/internal/vm/sinatra_bind.go b/internal/vm/sinatra_bind.go index a2af5cd8..846c84c2 100644 --- a/internal/vm/sinatra_bind.go +++ b/internal/vm/sinatra_bind.go @@ -301,8 +301,10 @@ func (vm *VM) registerSinatraContext(ctx *RClass) { // go-ruby-erb compiler and return the rendered String (which becomes the // response body). template is an inline template String or a :symbol naming // views/.erb; the compiled source evaluates in this handler's binding, so - // the action's @ivars and the request helpers resolve and `locals:` bind. See - // sinatra_erb.go. Layouts/partials and :erb escape_html are scoped follow-ups. + // the action's @ivars and the request helpers resolve and `locals:` bind. A + // layout (views/layout.erb by default, :layout => false/:name to override) + // wraps the view with its String exposed as `yield`. See sinatra_erb.go. + // Partials and :erb escape_html are scoped follow-ups. ctx.define("erb", func(vm *VM, v object.Value, args []object.Value, _ *Proc) object.Value { return vm.sinatraErb(self(v), args) }) diff --git a/internal/vm/sinatra_erb.go b/internal/vm/sinatra_erb.go index 8c22ff85..614c9b7f 100644 --- a/internal/vm/sinatra_erb.go +++ b/internal/vm/sinatra_erb.go @@ -25,10 +25,19 @@ import ( // auto-escape unless :erb => { escape_html: true } is configured — a scoped // follow-up), <% %> is control flow. // -// Two render paths land: an inline-String template (needs no filesystem) and a -// :symbol template naming views/.erb (read through the File binding, with -// the views dir taken from settings :views, default "./views"). Layouts and -// partials are flagged as a scoped follow-up (see registerSinatraContext). +// Two view render paths land: an inline-String template (needs no filesystem) +// and a :symbol template naming views/.erb (read through the File binding, +// with the views dir taken from settings :views, default "./views"). +// +// Layouts wrap that view exactly as MRI Sinatra does. By default `erb :view` +// renders views/layout.erb (if present) with the view's rendered String exposed +// as `yield`, so the layout's `<%= yield %>` interpolates the inner content; a +// missing default layout is silently skipped (MRI's `catch(:layout_missing)`). +// `erb :view, layout: false` renders the view with no layout; `layout: :name` +// (or a template String) selects a custom layout, and a missing *named* layout +// raises Errno::ENOENT (MRI eats the error only for the default layout). Partials +// (`erb :part` composed inside a view via a helper) and the :erb escape_html / +// app-level `set :erb, layout:` engine options remain a scoped follow-up. // sinatraTrimMode is the ERB trim mode Sinatra renders with: Tilt::ERBTemplate // maps a nil/true :trim option (Sinatra's default) to "<>", so a line holding @@ -49,7 +58,18 @@ func (vm *VM) sinatraErb(sc *SinatraCtx, args []object.Value) object.Value { if len(args) == 0 { raise("ArgumentError", "wrong number of arguments (given 0, expected 1..3)") } - tmpl := vm.sinatraErbTemplate(sc, args[0]) + names, vals := sinatraErbLocals(args) + // Render the view itself. A plain view is evaluated with no block, so a bare + // `yield` in a view (as opposed to a layout) raises LocalJumpError, matching MRI. + view := vm.sinatraErbRender(sc, vm.sinatraErbTemplate(sc, args[0]), names, vals, nil) + return vm.sinatraErbLayout(sc, args, view, names, vals) +} + +// sinatraErbRender compiles one ERB template String through go-ruby-erb and +// evaluates it in the handler's binding, binding the render locals and, when +// non-nil, the yield block (used to expose the wrapped view to a layout's +// `<%= yield %>`). It returns the rendered String. +func (vm *VM) sinatraErbRender(sc *SinatraCtx, tmpl string, names []string, vals []object.Value, block *Proc) object.Value { src, _, err := erbCompile(tmpl, erb.Options{TrimMode: sinatraTrimMode, EOutVar: sinatraEOutVar}) if err != nil { // go-ruby-erb never fails on a well-formed template (its err is reserved for @@ -57,8 +77,99 @@ func (vm *VM) sinatraErb(sc *SinatraCtx, args []object.Value) object.Value { // the branch is surfaced as an ArgumentError so the contract stays total. raise("ArgumentError", "%s", err.Error()) } - names, vals := sinatraErbLocals(args) - return vm.sinatraErbEval(sc, src, names, vals) + return vm.sinatraErbEval(sc, src, names, vals, block) +} + +// sinatraErbLayout applies Sinatra's layout rule to an already-rendered view: it +// reads the layout option from the `erb` options Hash (args[1]) and, if a layout +// applies, renders it with the same locals and a block whose `yield` returns the +// view's String, so the layout's `<%= yield %>` interpolates the inner content. +// +// The layout is chosen exactly as MRI Sinatra's render does: +// - no :layout key -> the default layout (views/layout.erb), missing = skipped +// - :layout => false/nil -> no layout (the view is returned verbatim) +// - :layout => true -> the default layout, but a missing file now raises +// - :layout => :name -> views/name.erb, a missing file raises +// - :layout => "src" -> an inline template String used as the layout +// +// (MRI eats a "layout missing" error only for the implicit default layout — an +// explicitly requested layout that is absent surfaces the Errno::ENOENT.) +func (vm *VM) sinatraErbLayout(sc *SinatraCtx, args []object.Value, view object.Value, names []string, vals []object.Value) object.Value { + val, present := sinatraLayoutOpt(args) + var ( + name string // a :symbol/true layout resolves to views/.erb + source string // a String layout is an inline template rendered directly + inline bool + eat bool // eat a missing layout file (only the implicit default layout) + ) + switch { + case !present: + name, eat = "layout", true + default: + switch v := val.(type) { + case object.Symbol: + name = string(v) + case *object.String: + source, inline = v.Str(), true + default: + // true selects the default layout (with the error surfaced, not eaten); + // false / nil / any other falsy value means render the view with no layout. + if !val.Truthy() { + return view + } + name = "layout" + } + } + + // The layout's `yield` returns the wrapped view's rendered String. + inner := view + yield := &Proc{native: func(*VM, []object.Value) object.Value { return inner }} + + if inline { + return vm.sinatraErbRender(sc, source, names, vals, yield) + } + tmpl, found := vm.sinatraReadLayout(sc, name, eat) + if !found { + // The implicit default layout is absent — return the un-wrapped view, exactly + // as MRI's catch(:layout_missing) does. + return view + } + return vm.sinatraErbRender(sc, tmpl, names, vals, yield) +} + +// sinatraLayoutOpt reads the :layout entry from the `erb` options Hash (the second +// positional argument), reporting whether the key was present so an absent key +// (default layout) is distinguished from an explicit `layout: false`. +func sinatraLayoutOpt(args []object.Value) (val object.Value, present bool) { + if len(args) >= 2 { + if h, ok := args[1].(*object.Hash); ok { + if v, ok := h.Get(object.Symbol("layout")); ok { + return v, true + } + } + } + return nil, false +} + +// sinatraReadLayout reads the layout template file views/.erb through the +// File binding, the views dir resolved as for a view (settings :views, default +// "./views"). When eat is true (the implicit default layout) a missing file is +// reported as found=false rather than raising, so the caller can skip the layout — +// MRI's catch(:layout_missing). When eat is false the file is read unconditionally, +// so a missing named layout raises Errno::ENOENT. +func (vm *VM) sinatraReadLayout(sc *SinatraCtx, name string, eat bool) (tmpl string, found bool) { + if eat && !vm.sinatraViewExists(sc, name) { + return "", false + } + return vm.sinatraErbReadView(sc, name), true +} + +// sinatraViewExists reports whether views/.erb exists, through the File +// binding (File.exist?), so the default-layout lookup obeys the same host +// filesystem seam as a view read. +func (vm *VM) sinatraViewExists(sc *SinatraCtx, name string) bool { + pathV := object.NewString(vm.sinatraViewsDir(sc) + "/" + name + ".erb") + return vm.send(vm.consts["File"].(*RClass), "exist?", []object.Value{pathV}, nil).Truthy() } // sinatraErbTemplate resolves the first `erb` argument to the template source: an @@ -82,19 +193,24 @@ func (vm *VM) sinatraErbTemplate(sc *SinatraCtx, tmpl object.Value) string { // filesystem seam as any File.read; a missing file raises Errno::ENOENT exactly as // under Sinatra. func (vm *VM) sinatraErbReadView(sc *SinatraCtx, name string) string { - dir := "./views" - if v, ok := sc.settings["views"]; ok { - if s, ok := v.(*object.String); ok { - dir = s.Str() - } - } - pathV := object.NewString(dir + "/" + name + ".erb") + pathV := object.NewString(vm.sinatraViewsDir(sc) + "/" + name + ".erb") // File.read returns the file's contents as a String (or raises Errno::ENOENT for // a missing view, which propagates exactly as under Sinatra). res := vm.send(vm.consts["File"].(*RClass), "read", []object.Value{pathV}, nil) return res.(*object.String).Str() } +// sinatraViewsDir resolves the app's views directory: the :views setting (a +// String) or Sinatra's default "./views" when unset or set to a non-String value. +func (vm *VM) sinatraViewsDir(sc *SinatraCtx) string { + if v, ok := sc.settings["views"]; ok { + if s, ok := v.(*object.String); ok { + return s.Str() + } + } + return "./views" +} + // sinatraErbLocals collects the local variables an `erb` render binds, in a stable // order (options[:locals] first, then the positional locals Hash, positional // entries overriding). Keys are Symbols or Strings; each becomes a local variable diff --git a/internal/vm/sinatra_erb_closed.go b/internal/vm/sinatra_erb_closed.go index cc1ac26d..a3af1fd3 100644 --- a/internal/vm/sinatra_erb_closed.go +++ b/internal/vm/sinatra_erb_closed.go @@ -9,8 +9,8 @@ import "github.com/go-embedded-ruby/ruby/internal/object" // CompileWithLocals) that a closed-world build drops from the link — the same // reason Binding#eval and Kernel#eval on source raise there. Routing/dispatch and // the rest of the Sinatra DSL still work; only `erb` view rendering raises. -func (vm *VM) sinatraErbEval(sc *SinatraCtx, src string, names []string, vals []object.Value) object.Value { - _, _, _, _ = sc, src, names, vals +func (vm *VM) sinatraErbEval(sc *SinatraCtx, src string, names []string, vals []object.Value, block *Proc) object.Value { + _, _, _, _, _ = sc, src, names, vals, block raise("NotImplementedError", "Sinatra#erb view rendering is unavailable in a closed-world binary (built with rbgo build --closed, without the front-end)") return nil // unreachable: raise panics } diff --git a/internal/vm/sinatra_erb_internal_test.go b/internal/vm/sinatra_erb_internal_test.go index 01c4b5a0..6497f4f7 100644 --- a/internal/vm/sinatra_erb_internal_test.go +++ b/internal/vm/sinatra_erb_internal_test.go @@ -189,6 +189,95 @@ end` } } +// sinatraLayoutApp writes card.erb / layout.erb / alt.erb into a fresh views dir +// and returns a Sinatra app (source) whose single /v route runs the given body, +// so a layout render exercises the whole bridge (view compile + eval, layout file +// read, and the yield block returning the view's String). +func sinatraLayoutApp(t *testing.T, route string) string { + t.Helper() + dir := slash(t.TempDir()) + write := func(name, body string) { + if err := os.WriteFile(filepath.FromSlash(dir+"/"+name), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + write("card.erb", "

<%= @who %>

") + write("layout.erb", "
<%= yield %>
") + write("alt.erb", "\"><%= yield %>") + return `require "sinatra/base" +class A < Sinatra::Base + disable :protection + set :host_authorization, { permitted_hosts: [] } + set :views, "` + dir + `" + before { @who = "World" } + ` + route + ` +end +_, _, b = A.new.call("REQUEST_METHOD"=>"GET","PATH_INFO"=>"/v","QUERY_STRING"=>"","rack.url_scheme"=>"http","HTTP_HOST"=>"ex") +print b.join` +} + +// TestSinatraErbLayout covers the layout render arms of sinatraErbLayout with a +// views dir that holds layout.erb: the implicit default layout wrapping a view +// (its `<%= yield %>` returning the view's String), layout: false / nil (no +// layout), layout: true (the explicit default layout), a custom layout: :name +// file (with render locals reaching the layout), and an inline-String layout. +func TestSinatraErbLayout(t *testing.T) { + cases := []struct{ route, want string }{ + // Implicit default layout — views/layout.erb wraps the view. + {`get("/v"){ erb :card }`, "

World

"}, + // layout: false / nil — render the view with no layout. + {`get("/v"){ erb :card, layout: false }`, "

World

"}, + {`get("/v"){ erb :card, layout: nil }`, "

World

"}, + // layout: true — the default layout, explicitly requested. + {`get("/v"){ erb :card, layout: true }`, "

World

"}, + // layout: :alt — a custom layout file; the render local (css) reaches it. + {`get("/v"){ erb :card, { layout: :alt }, css: "box" }`, `

World

`}, + // An inline-String layout wraps the view via its `<%= yield %>`. + {`get("/v"){ erb :card, layout: "<%= yield %>" }`, "

World

"}, + } + for _, c := range cases { + if got := runFS(t, sinatraLayoutApp(t, c.route)); got != c.want { + t.Errorf("route %q: got=%q want=%q", c.route, got, c.want) + } + } +} + +// TestSinatraErbLayoutMissing covers the missing-layout arms: the implicit default +// layout is silently skipped when views/layout.erb is absent (MRI's +// catch(:layout_missing)), while an explicitly requested layout that is absent — +// a named layout: :sym or layout: true — raises Errno::ENOENT. +func TestSinatraErbLayoutMissing(t *testing.T) { + dir := slash(t.TempDir()) + if err := os.WriteFile(filepath.FromSlash(dir+"/card.erb"), []byte(""), 0o644); err != nil { + t.Fatal(err) + } + app := func(route string) string { + return `require "sinatra/base" +class A < Sinatra::Base + disable :protection + set :host_authorization, { permitted_hosts: [] } + set :views, "` + dir + `" + before { @who = "World" } + ` + route + ` +end +_, _, b = A.new.call("REQUEST_METHOD"=>"GET","PATH_INFO"=>"/v","QUERY_STRING"=>"","rack.url_scheme"=>"http","HTTP_HOST"=>"ex") +print b.join` + } + cases := []struct{ route, want string }{ + // Implicit default layout absent -> skipped, the view renders un-wrapped. + {`get("/v"){ erb :card }`, ""}, + // A named layout that is absent raises Errno::ENOENT. + {`get("/v"){ begin; erb :card, layout: :nope; rescue => e; e.class.to_s; end }`, "Errno::ENOENT"}, + // layout: true with an absent layout.erb also raises (MRI eats only the implicit default). + {`get("/v"){ begin; erb :card, layout: true; rescue => e; e.class.to_s; end }`, "Errno::ENOENT"}, + } + for _, c := range cases { + if got := runFS(t, app(c.route)); got != c.want { + t.Errorf("route %q: got=%q want=%q", c.route, got, c.want) + } + } +} + // TestSinatraErbCompileError covers sinatraErb's otherwise-unreachable // compile-error branch: go-ruby-erb never fails on a well-formed template, so the // failure is injected through the erbCompile seam and must surface as a Ruby diff --git a/internal/vm/sinatra_erb_open.go b/internal/vm/sinatra_erb_open.go index 94a8f10c..98b0e013 100644 --- a/internal/vm/sinatra_erb_open.go +++ b/internal/vm/sinatra_erb_open.go @@ -15,10 +15,14 @@ import ( // binding. It compiles the source in a child scope over a synthetic parent frame // holding the locals (compiler.CompileWithLocals), seeds that parent env with the // locals' values in slot order and runs the ISeq with the context as self. The -// compiled template's final expression is its output buffer, so exec returns the -// rendered String. It uses the front-end directly, so a closed-world build -// replaces it with the stub in sinatra_erb_closed.go. -func (vm *VM) sinatraErbEval(sc *SinatraCtx, src string, names []string, vals []object.Value) object.Value { +// block, when non-nil, is the template's yield target — a layout is evaluated +// with a block that returns the wrapped view's rendered String, so `<%= yield %>` +// in the layout interpolates the inner content, exactly as MRI Sinatra renders a +// layout with `render(...) { output }`. The compiled template's final expression +// is its output buffer, so exec returns the rendered String. It uses the +// front-end directly, so a closed-world build replaces it with the stub in +// sinatra_erb_closed.go. +func (vm *VM) sinatraErbEval(sc *SinatraCtx, src string, names []string, vals []object.Value, block *Proc) object.Value { prog, perr := parser.Parse(src) if perr != nil { // go-ruby-erb emits valid Ruby for any well-formed template, so a parse error @@ -38,5 +42,5 @@ func (vm *VM) sinatraErbEval(sc *SinatraCtx, src string, names []string, vals [] slots := make([]object.Value, len(vals)) copy(slots, vals) parent := &Env{slots: slots} - return vm.exec(iseq, sc, nil, vm.classOf(sc), "", parent, nil, nil, nil) + return vm.exec(iseq, sc, nil, vm.classOf(sc), "", parent, block, nil, nil) } diff --git a/scripts/conformance/webapp/apps/sinatra_erb_oracle.rb b/scripts/conformance/webapp/apps/sinatra_erb_oracle.rb index abe6c307..771f2a80 100644 --- a/scripts/conformance/webapp/apps/sinatra_erb_oracle.rb +++ b/scripts/conformance/webapp/apps/sinatra_erb_oracle.rb @@ -6,8 +6,13 @@ # `erb` helper re-exposes: <%= %> interpolation of a request param, an @ivar set # in a before-filter (real Sinatra runs before/route against ONE instance, so it # is visible to the view), <% %> control flow, `locals:` passed both positionally -# and inside the options Hash, ERB trim behaviour, and a :symbol template read -# from the app's :views directory. +# and inside the options Hash, ERB trim behaviour, a :symbol template read from +# the app's :views directory, and — via a second app whose :views dir also holds +# a layout.erb — Sinatra's layout rule: the default layout wrapping a view with +# `<%= yield %>` returning the view's String, `layout: false` (no layout), a +# custom `layout: :name`, `layout: true`, an inline-String layout, and the +# Errno::ENOENT a missing named layout / view raises (rescued to its class name so +# the byte-exact diff does not depend on Sinatra's environment-specific error page). # # The identical file runs unchanged under MRI + the sinatra gem; the expected # output in sinatra_erb_oracle_test.go was captured from that run (gem 4.2.1, @@ -78,17 +83,103 @@ class App < Sinatra::Base end end -ENVS = [ - { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/inline", "QUERY_STRING" => "name=amy", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }, - { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/loop", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }, - { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/locals", "QUERY_STRING" => "name=bob", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }, - { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/opts_locals", "QUERY_STRING" => "name=eve", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }, - { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/trim", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }, - { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/view", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }, +# A second views dir that DOES hold a layout.erb (plus a custom alt.erb), so the +# second app exercises the layout rule. Keeping it separate from VIEWS leaves the +# first app's :symbol view (/view) un-wrapped, proving the no-layout path too. +LVIEWS = Dir.mktmpdir("erb-oracle-lviews") +File.write( + File.join(LVIEWS, "card.erb"), + "

<%= greeting %> <%= @who %>

\n
    \n<% items.each do |i| %>\n
  • <%= i %>
  • \n<% end %>\n
\n" +) +# The default layout: surrounding markup with `<%= yield %>` interpolating the +# wrapped view and an @ivar the before-filter set (the layout renders in the same +# handler binding as the view). `yield` is kept inline (not alone on a line) so the +# fixture stays inside the ERB/erubi trim-agreement subset the oracle relies on — +# a standalone `<%= %>` line is the one documented go-ruby-erb LIBRARY divergence. +File.write( + File.join(LVIEWS, "layout.erb"), + "\nSite\n
<%= yield %>
\n
by <%= @who %>
\n" +) +# A custom layout selected with `layout: :alt`, using a render local (proving the +# locals reach the layout, not just the view) around `<%= yield %>`. +File.write( + File.join(LVIEWS, "alt.erb"), + "
\"><%= yield %>
\n" +) + +class LApp < Sinatra::Base + disable :protection + set :host_authorization, { permitted_hosts: [] } + set :views, LVIEWS + + before { @who = "World" } + + # Default layout: `erb :card` renders views/card.erb and wraps it in + # views/layout.erb, whose `<%= yield %>` interpolates the view's String. + get "/l_default" do + erb :card, {}, greeting: "Hey", items: %w[x y] + end + + # `layout: false` — render the view with no layout. + get "/l_none" do + erb :card, { layout: false }, greeting: "Hey", items: %w[x y] + end + + # `layout: :alt` — a custom layout file; the render locals (css) reach it. + get "/l_custom" do + erb :card, { layout: :alt }, greeting: "Hi", items: %w[a], css: "box" + end + + # `layout: true` — the default layout, explicitly requested. + get "/l_true" do + erb :card, { layout: true }, greeting: "Yo", items: %w[q] + end + + # An inline-String layout, whose `<%= yield %>` wraps the view. + get "/l_inline" do + erb :card, { layout: "<%= yield %>\n" }, greeting: "In", items: %w[z] + end + + # A missing NAMED layout raises Errno::ENOENT (MRI eats the error only for the + # implicit default layout, not an explicitly requested one). + get "/l_missing_layout" do + begin + erb :card, { layout: :nope }, greeting: "X", items: [] + rescue => e + e.class.to_s + end + end + + # A missing view raises Errno::ENOENT. + get "/l_missing_view" do + begin + erb :ghost + rescue => e + e.class.to_s + end + end +end + +# [app, env] pairs — the first six keep App's original requests (and golden) +# byte-identical; the rest exercise LApp's layout rule. +REQS = [ + [App, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/inline", "QUERY_STRING" => "name=amy", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [App, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/loop", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [App, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/locals", "QUERY_STRING" => "name=bob", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [App, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/opts_locals", "QUERY_STRING" => "name=eve", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [App, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/trim", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [App, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/view", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_default", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_none", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_custom", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_true", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_inline", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_missing_layout", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], + [LApp, { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/l_missing_view", "QUERY_STRING" => "", "rack.url_scheme" => "http", "HTTP_HOST" => "ex" }], ] -ENVS.each_with_index do |env, i| - status, _headers, body = App.call(env) +REQS.each_with_index do |(app, env), i| + status, _headers, body = app.call(env) parts = [] body.each { |p| parts << p } puts "== req #{i} ==" diff --git a/scripts/conformance/webapp/sinatra_erb_oracle_test.go b/scripts/conformance/webapp/sinatra_erb_oracle_test.go index f63527d3..0643dbda 100644 --- a/scripts/conformance/webapp/sinatra_erb_oracle_test.go +++ b/scripts/conformance/webapp/sinatra_erb_oracle_test.go @@ -4,9 +4,10 @@ import "testing" // sinatraErbGem421Golden is the response dump of apps/sinatra_erb_oracle.rb // captured from the REAL `sinatra` gem 4.2.1 (MRI ruby 4.0.5) with its ERB view -// engine: six requests through the classic Sinatra::Base #call(env) adapter, each -// rendering an ERB view and dumping [status, body]. This is the golden oracle for -// the templating half of the Sinatra binding: rbgo, rendering through the bound +// engine: thirteen requests through the classic Sinatra::Base #call(env) adapter, +// each rendering an ERB view (six inline/symbol templates, then seven exercising +// the layout rule) and dumping [status, body]. This is the golden oracle for the +// templating half of the Sinatra binding: rbgo, rendering through the bound // go-ruby-erb compiler, must reproduce it byte-for-byte. It is a captured constant // so the suite stays hermetic (no ruby, no gem, runs under qemu on every 64-bit // target); regenerate by running the fixture under MRI + the sinatra gem, per @@ -36,6 +37,59 @@ body<<

Hey World

  • y
  • >> +== req 6 == +status=200 +body<< +Site +

    Hey World

    +
      +
    • x
    • +
    • y
    • +
    +
    +
    by World
    +>> +== req 7 == +status=200 +body<<

    Hey World

    +
      +
    • x
    • +
    • y
    • +
    +>> +== req 8 == +status=200 +body<<

    Hi World

    +
      +
    • a
    • +
    +
    +>> +== req 9 == +status=200 +body<< +Site +

    Yo World

    +
      +
    • q
    • +
    +
    +
    by World
    +>> +== req 10 == +status=200 +body<<

    In World

    +
      +
    • z
    • +
    +
    +>> +== req 11 == +status=200 +body<> +== req 12 == +status=200 +body<> ` // TestSinatraErbGemOracle proves the Sinatra `erb` view helper is MRI-identical @@ -44,8 +98,11 @@ body<<

    Hey World

    // captured gem output. This is the run-conformance headline for the templating // half of the web phase — inline-String and :symbol/file templates, <%= %> // interpolation of a query param and an @ivar set in a before-filter, <% %> -// control flow, positional and options[:locals] locals, and ERB trim behaviour — -// all rendered by rbgo (via the bound go-ruby-erb compiler, evaluated in the +// control flow, positional and options[:locals] locals, ERB trim behaviour, and +// the layout rule (default views/layout.erb with `<%= yield %>` returning the +// view, layout: false, a custom layout: :name, layout: true, an inline-String +// layout, and the Errno::ENOENT a missing named layout / view raises) — all +// rendered by rbgo (via the bound go-ruby-erb compiler, evaluated in the // handler's binding) exactly as by the reference gem. func TestSinatraErbGemOracle(t *testing.T) { if ok, detail := featureLoadable("sinatra/base"); !ok {