{#- Go app harness for server-pattern e2e tests
<!-- go-ffi-nonblock -->
This harness program is spawned as a subprocess by main_test.go and runs the
SUT app, registering handlers per fixture. It loads all fixtures, creates
handlers that return expected responses, and serves on a configured port.
Context variables (passed from Go codegen):
- imports: list of import paths to include (typically the package being tested)
- app_class: class name for SUT app (e.g., "App")
- route_builder_class: class name for route builder (e.g., "RouteBuilder")
- method_enum_import: import path for Method (e.g., "my_app")
- method_enum_class: class name for method enum (e.g., "GET", "POST")
- register_route_method: app method to register routes (e.g., "RegisterRoute")
- start_background_method: non-blocking serve entrypoint (e.g., "StartBackground")
- port: binding port (e.g., 8012)
- fixtures_json: raw JSON string with all fixtures (auto-serialized)
#}
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
{%- for import_name in imports %}
{{ import_alias }} "{{ import_name }}"
{%- endfor %}
)
// Fixture describes an HTTP test fixture with its handler route and expected response.
type Fixture struct {
HTTP *HTTPFixture `json:"http"`
}
// HTTPFixture contains handler and expected response details.
type HTTPFixture struct {
Handler HandlerDetails `json:"handler"`
ExpectedResponse ResponseDetails `json:"expected_response"`
}
// HandlerDetails specifies the route and HTTP method for the handler.
type HandlerDetails struct {
Route string `json:"route"`
Method string `json:"method"`
BodySchema interface{} `json:"body_schema"`
}
// ResponseDetails defines the expected status, body, and headers.
type ResponseDetails struct {
StatusCode uint16 `json:"status_code"`
Body interface{} `json:"body"`
Headers map[string]string `json:"headers"`
}
const port = {{ port }}
const host = "127.0.0.1"
// Load fixtures from the JSON payload embedded at codegen time.
// Use a quoted string with escaped newlines to avoid backtick conflicts.
var fixturesJSON = "{{ fixtures_json }}"
func main() {
var fixtures map[string]Fixture
if err := json.Unmarshal([]byte(fixturesJSON), &fixtures); err != nil {
log.Fatalf("unmarshal fixtures: %v", err)
}
app, err := {{ import_alias }}.NewApp()
if err != nil {
log.Fatalf("new app: %v", err)
}
// Map fixture method names (UPPERCASE) to framework method enum constants (PascalCase).
methodMap := map[string]{{ import_alias }}.Method{
"GET": {{ import_alias }}.MethodGet,
"POST": {{ import_alias }}.MethodPost,
"PUT": {{ import_alias }}.MethodPut,
"PATCH": {{ import_alias }}.MethodPatch,
"DELETE": {{ import_alias }}.MethodDelete,
"HEAD": {{ import_alias }}.MethodHead,
"OPTIONS": {{ import_alias }}.MethodOptions,
"CONNECT": {{ import_alias }}.MethodConnect,
"TRACE": {{ import_alias }}.MethodTrace,
}
// Register a handler for each fixture.
for fixtureID, fixture := range fixtures {
if fixture.HTTP == nil {
continue
}
http := fixture.HTTP
route := http.Handler.Route
method := http.Handler.Method
bodySchema := http.Handler.BodySchema
expectedStatus := http.ExpectedResponse.StatusCode
expectedBody := http.ExpectedResponse.Body
expectedHeaders := http.ExpectedResponse.Headers
// Build a closure that captures the expected response for this fixture.
// The handler ignores request parameters and returns the recorded response.
handler := makeHandler(expectedStatus, expectedBody, expectedHeaders)
// Register the route with the full fixture-namespaced path: /fixtures/<fixture_id>{route}
fullRoute := fmt.Sprintf("/fixtures/%s%s", fixtureID, route)
// Convert method string (GET, POST, etc.) to the framework's method enum.
m, ok := methodMap[strings.ToUpper(method)]
if !ok {
log.Printf("skipping fixture %s: unknown method %s", fixtureID, method)
continue
}
builder, err := {{ import_alias }}.RouteBuilderNew(m, fullRoute)
if err != nil {
log.Fatalf("create route builder: %v", err)
}
// If there's a body schema, attach it to the builder.
if bodySchema != nil {
bodySchemaJSON, _ := json.Marshal(bodySchema)
builder = builder.RequestSchemaJSON(json.RawMessage(bodySchemaJSON))
}
// Register the route with the handler.
// Note: The builder is consumed by RegisterRoute (ownership transferred to Rust).
// Do NOT call builder.Free() — the Rust FFI layer already owns and frees it.
if err := app.{{ register_route_method }}(handler, builder); err != nil {
log.Fatalf("register route %s: %v", fullRoute, err)
}
}
// Start the server on a background OS thread. StartBackground blocks until
// the TCP socket is bound, so the server is guaranteed to be accepting
// connections when this call returns. This avoids the goroutine + polling
// pattern that previously exhausted cgo OS threads.
serverHandle, err := app.{{ start_background_method }}(host, port)
if err != nil {
log.Fatalf("start background server on %s:%d: %v", host, port, err)
}
// Signal readiness to the parent test process.
fmt.Printf("Harness listening on %s:%d\n", host, port)
os.Stdout.Sync()
// Graceful shutdown on interrupt.
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
serverHandle.Stop()
}
// makeHandler returns a handler function that always returns the expected response.
func makeHandler(statusCode uint16, body interface{}, headers map[string]string) func([]byte) ([]byte, error) {
return func(reqBody []byte) ([]byte, error) {
// Wrap the response in the framework's Response struct.
resp := map[string]interface{}{
"status_code": statusCode,
"content": body,
"headers": headers,
}
respBody, _ := json.Marshal(resp)
return respBody, nil
}
}