use crate::core::backend::GeneratedFile;
use crate::core::config::Language;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use heck::ToUpperCamelCase;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
use super::client;
fn resolve_handle_config_type(
arg: &crate::e2e::config::ArgMapping,
options_type: Option<&str>,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<String> {
if arg.arg_type != "handle" {
return None;
}
options_type.map(str::to_string).or_else(|| {
let candidate = format!("{}Config", arg.name.to_upper_camel_case());
type_defs.iter().any(|ty| ty.name == candidate).then_some(candidate)
})
}
pub struct GoCodegen;
impl E2eCodegen for GoCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let mut files = Vec::new();
let data_enum_names: std::collections::HashSet<&str> = enums
.iter()
.filter(|e| {
e.variants
.iter()
.any(|v| !v.fields.is_empty() && v.fields.iter().any(|f| !f.name.is_empty()))
})
.map(|e| e.name.as_str())
.collect();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
let module_path = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.or_else(|| configured_go_module_path.clone())
.unwrap_or_else(|| call.module.clone());
let import_alias = overrides
.and_then(|o| o.alias.as_ref())
.cloned()
.unwrap_or_else(|| "pkg".to_string());
let go_pkg = e2e_config.resolve_package("go");
let go_module_path = go_pkg
.as_ref()
.and_then(|p| p.module.as_ref())
.cloned()
.or_else(|| configured_go_module_path.clone())
.unwrap_or_else(|| module_path.clone());
let replace_path = go_pkg
.as_ref()
.and_then(|p| p.path.as_ref())
.cloned()
.or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
let go_version = match e2e_config.dep_mode {
crate::e2e::config::DependencyMode::Registry => {
let registry_version = e2e_config
.registry
.packages
.get("go")
.and_then(|p| p.version.as_ref())
.cloned();
registry_version
.or_else(|| config.resolved_version().map(|v| format!("v{v}")))
.or_else(|| go_pkg.as_ref().and_then(|p| p.version.as_ref()).cloned())
.unwrap_or_else(|| "v0.0.0".to_string())
}
crate::e2e::config::DependencyMode::Local => go_pkg
.as_ref()
.and_then(|p| p.version.as_ref())
.cloned()
.unwrap_or_else(|| {
config
.resolved_version()
.map(|v| format!("v{v}"))
.unwrap_or_else(|| "v0.0.0".to_string())
}),
};
let effective_replace = match e2e_config.dep_mode {
crate::e2e::config::DependencyMode::Registry => None,
crate::e2e::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
};
let effective_go_version = if effective_replace.is_some() {
fix_go_major_version(&go_module_path, &go_version)
} else {
go_version.clone()
};
files.push(GeneratedFile {
path: output_base.join("go.mod"),
content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
generated_header: false,
});
let emits_executable_test =
|fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
emits_executable_test(f)
&& f.assertions.iter().any(|a| {
matches!(
a.assertion_type.as_str(),
"contains" | "contains_all" | "contains_any" | "not_contains"
) && {
if a.field.as_ref().is_none_or(|f| f.is_empty()) {
e2e_config
.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
)
.result_is_array
} else {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
let per_call_resolver = FieldResolver::new(
e2e_config.effective_fields(cc),
e2e_config.effective_fields_optional(cc),
e2e_config.effective_result_fields(cc),
e2e_config.effective_fields_array(cc),
&std::collections::HashSet::new(),
);
let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
per_call_resolver.is_array(resolved_name)
}
}
})
});
if needs_json_stringify {
files.push(GeneratedFile {
path: output_base.join("helpers_test.go"),
content: render_helpers_test_go(),
generated_header: true,
});
}
let has_file_fixtures = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| f.http.is_none() && !f.needs_mock_server());
let needs_mock_server = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| f.http.is_none() && f.needs_mock_server());
let needs_http_tests = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.http.is_some());
let needs_main_test = has_file_fixtures
|| needs_http_tests
|| groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
if f.http.is_none() && f.needs_mock_server() {
return true;
}
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
go_override.and_then(|o| o.client_factory.as_deref()).is_some()
});
let has_http_fixtures = needs_http_tests;
if needs_main_test {
files.push(GeneratedFile {
path: output_base.join("main_test.go"),
content: render_main_test_go(
&e2e_config.test_documents_dir,
needs_mock_server,
has_http_fixtures,
&e2e_config.env,
),
generated_header: true,
});
if has_http_fixtures {
let harness_content = render_harness_main(e2e_config, groups, &module_path);
files.push(GeneratedFile {
path: output_base.join("cmd").join("harness").join("main.go"),
content: harness_content,
generated_header: true,
});
}
}
for group in groups {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.collect();
if active.is_empty() {
continue;
}
let filename = format!("{}_test.go", sanitize_filename(&group.category));
let content = render_test_file(
&group.category,
&active,
GoTestFileContext {
go_module_path: &module_path,
import_alias: &import_alias,
e2e_config,
adapters: &config.adapters,
data_enum_names: &data_enum_names,
config,
type_defs,
enums,
},
);
files.push(GeneratedFile {
path: output_base.join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"go"
}
}
fn fix_go_major_version(module_path: &str, version: &str) -> String {
let major = module_path
.rsplit('/')
.next()
.and_then(|seg| seg.strip_prefix('v'))
.and_then(|n| n.parse::<u64>().ok())
.filter(|&n| n >= 2);
let Some(n) = major else {
return version.to_string();
};
let expected_prefix = format!("v{n}.");
if version.starts_with(&expected_prefix) {
return version.to_string();
}
format!("v{n}.0.0")
}
fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
let mut out = String::new();
let _ = writeln!(out, "module {go_module_path}/e2e");
let _ = writeln!(out);
let _ = writeln!(out, "go 1.26");
let _ = writeln!(out);
let _ = writeln!(out, "require (");
let _ = writeln!(out, "\t{go_module_path} {version}");
let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
let _ = writeln!(out, ")");
if let Some(path) = replace_path {
let _ = writeln!(out);
let _ = writeln!(out, "replace {go_module_path} => {path}");
}
out
}
fn render_env_setup(env: &std::collections::HashMap<String, String>) -> String {
if env.is_empty() {
return String::new();
}
let mut keys: Vec<&String> = env.keys().collect();
keys.sort();
let mut out = String::new();
for k in keys {
let v = &env[k];
let _ = writeln!(out, "\tif _, ok := os.LookupEnv(\"{k}\"); !ok {{");
let _ = writeln!(out, "\t\t_ = os.Setenv(\"{k}\", \"{v}\")");
let _ = writeln!(out, "\t}}");
}
out
}
fn render_main_test_go(
test_documents_dir: &str,
needs_mock_server_bootstrap: bool,
has_http_fixtures: bool,
env: &std::collections::HashMap<String, String>,
) -> String {
let mut out = String::new();
let _ = writeln!(out, "package e2e_test");
let _ = writeln!(out);
let _ = writeln!(out, "import (");
if needs_mock_server_bootstrap {
let _ = writeln!(out, "\t\"bufio\"");
let _ = writeln!(out, "\t\"encoding/json\"");
}
let _ = writeln!(out, "\t\"os\"");
if needs_mock_server_bootstrap || has_http_fixtures {
let _ = writeln!(out, "\t\"os/exec\"");
}
let _ = writeln!(out, "\t\"path/filepath\"");
let _ = writeln!(out, "\t\"runtime\"");
let _ = writeln!(out, "\t\"testing\"");
if needs_mock_server_bootstrap {
let _ = writeln!(out, "\t\"fmt\"");
let _ = writeln!(out, "\t\"io\"");
let _ = writeln!(out, "\t\"net/http\"");
let _ = writeln!(out, "\t\"strings\"");
let _ = writeln!(out, "\t\"time\"");
} else if has_http_fixtures {
let _ = writeln!(out, "\t\"fmt\"");
let _ = writeln!(out, "\t\"io\"");
let _ = writeln!(out, "\t\"net\"");
let _ = writeln!(out, "\t\"time\"");
}
let _ = writeln!(out, ")");
let _ = writeln!(out);
let _ = writeln!(out, "func TestMain(m *testing.M) {{");
let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
let _ = writeln!(out);
let env_setup = render_env_setup(env);
if !env_setup.is_empty() {
out.push_str(&env_setup);
let _ = writeln!(out);
}
let _ = writeln!(
out,
"\t// Change to the configured test-documents directory (if it exists) so that fixture"
);
let _ = writeln!(
out,
"\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
);
let _ = writeln!(
out,
"\t// from e2e/go/. Repos without document fixtures skip chdir and run from e2e/go/."
);
let _ = writeln!(
out,
"\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
);
let _ = writeln!(
out,
"\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
);
let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
let _ = writeln!(out, "\t\t\tpanic(err)");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
if needs_mock_server_bootstrap {
let _ = writeln!(out, "\tif os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
let _ = writeln!(out, "\t\tos.Exit(m.Run())");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
let _ = writeln!(
out,
"\tmockBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
);
let _ = writeln!(
out,
"\tmockManifest := filepath.Join(dir, \"..\", \"rust\", \"Cargo.toml\")"
);
let _ = writeln!(out, "\tif _, err := os.Stat(mockBin); os.IsNotExist(err) {{");
let _ = writeln!(out, "\t\tfmt.Fprintln(os.Stderr, \"Building mock-server...\")");
let _ = writeln!(
out,
"\t\tbuild := exec.Command(\"cargo\", \"build\", \"--release\", \"--manifest-path\", mockManifest, \"--bin\", \"mock-server\")"
);
let _ = writeln!(out, "\t\tbuild.Stdout = os.Stderr");
let _ = writeln!(out, "\t\tbuild.Stderr = os.Stderr");
let _ = writeln!(out, "\t\tif err := build.Run(); err != nil {{");
let _ = writeln!(out, "\t\t\tpanic(fmt.Sprintf(\"mock-server build failed: %v\", err))");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
let _ = writeln!(out, "\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")");
let _ = writeln!(out, "\tcmd := exec.Command(mockBin, fixturesDir)");
let _ = writeln!(out, "\tcmdEnv := os.Environ()");
for k in env.keys() {
let _ = writeln!(out, "\tif v := os.Getenv(\"{k}\"); v != \"\" {{");
let _ = writeln!(out, "\t\tcmdEnv = append(cmdEnv, \"{k}=\" + v)");
let _ = writeln!(out, "\t}}");
}
let _ = writeln!(out, "\tcmdEnv = append(cmdEnv, \"MOCK_SERVER_NO_STDIN_WATCH=1\")");
let _ = writeln!(out, "\tcmd.Env = cmdEnv");
let _ = writeln!(out, "\tstdout, err := cmd.StdoutPipe()");
let _ = writeln!(out, "\tif err != nil {{ panic(err) }}");
let _ = writeln!(out, "\tcmd.Stderr = os.Stderr");
let _ = writeln!(out, "\tif err := cmd.Start(); err != nil {{ panic(err) }}");
let _ = writeln!(
out,
"\t// Defer cleanup to a helper to avoid 'exitAfterDefer' linter violation."
);
let _ = writeln!(
out,
"\t// The helper owns process cleanup via defer; TestMain calls os.Exit"
);
let _ = writeln!(
out,
"\t// after the helper returns, so defer cleanup completes properly."
);
let _ = writeln!(out, "\tcode := runTests(m, cmd, stdout)");
let _ = writeln!(out, "\tos.Exit(code)");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(
out,
"// runTests executes the test suite with process cleanup via defer."
);
let _ = writeln!(out, "// By returning int and calling os.Exit in TestMain, we avoid");
let _ = writeln!(out, "// the 'exitAfterDefer' linter error.");
let _ = writeln!(
out,
"func runTests(m *testing.M, cmd *exec.Cmd, stdout io.ReadCloser) int {{"
);
let _ = writeln!(out, "\tdefer func() {{ _ = cmd.Process.Kill() }}()");
let _ = writeln!(out);
let _ = writeln!(out, "\tscanner := bufio.NewScanner(stdout)");
let _ = writeln!(out, "\tscanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)");
let _ = writeln!(
out,
"\t// The mock-server emits two sentinel lines on stdout: MOCK_SERVER_URL=<url>"
);
let _ = writeln!(
out,
"\t// (always) and MOCK_SERVERS={{\"<fixture_id>\":\"<per-fixture-url>\",...}} (when"
);
let _ = writeln!(
out,
"\t// any fixture has origin-root routes that need a per-fixture listener). We"
);
let _ = writeln!(
out,
"\t// read until we have seen MOCK_SERVER_URL and either MOCK_SERVERS or a non"
);
let _ = writeln!(out, "\t// MOCK_SERVER line, then drain the rest in the background.");
let _ = writeln!(out, "\thaveURL := false");
let _ = writeln!(out, "\tfor scanner.Scan() {{");
let _ = writeln!(out, "\t\tline := scanner.Text()");
let _ = writeln!(out, "\t\t//nolint:gocritic");
let _ = writeln!(out, "\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
let _ = writeln!(
out,
"\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
);
let _ = writeln!(out, "\t\t\thaveURL = true");
let _ = writeln!(out, "\t\t\tcontinue");
let _ = writeln!(out, "\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
let _ = writeln!(out, "\t\t\tpayload := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
let _ = writeln!(out, "\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", payload)");
let _ = writeln!(out, "\t\t\tvar servers map[string]string");
let _ = writeln!(
out,
"\t\t\tif err := json.Unmarshal([]byte(payload), &servers); err == nil {{"
);
let _ = writeln!(out, "\t\t\t\tfor fid, furl := range servers {{");
let _ = writeln!(
out,
"\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(fid), furl)"
);
let _ = writeln!(out, "\t\t\t\t}}");
let _ = writeln!(out, "\t\t\t}}");
let _ = writeln!(out, "\t\t\tbreak");
let _ = writeln!(out, "\t\t}} else if haveURL {{");
let _ = writeln!(out, "\t\t\tbreak");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tif os.Getenv(\"MOCK_SERVER_URL\") == \"\" {{");
let _ = writeln!(out, "\t\tpanic(\"mock-server did not emit MOCK_SERVER_URL\")");
let _ = writeln!(out, "\t}}");
let _ = writeln!(
out,
"\t// Drain remaining stdout asynchronously so the pipe doesn't fill."
);
let _ = writeln!(out, "\tgo func() {{ for scanner.Scan() {{ }} }}()");
let _ = writeln!(out);
let _ = writeln!(
out,
"\t// Poll the mock-server URL until it answers (axum::serve start race)."
);
let _ = writeln!(out, "\t{{");
let _ = writeln!(out, "\t\turl := os.Getenv(\"MOCK_SERVER_URL\")");
let _ = writeln!(out, "\t\tready := false");
let _ = writeln!(out, "\t\tfor i := 0; i < 400; i++ {{");
let _ = writeln!(out, "\t\t\tresp, err := http.Get(url)");
let _ = writeln!(out, "\t\t\tif err == nil {{");
let _ = writeln!(out, "\t\t\t\t_ = resp.Body.Close()");
let _ = writeln!(out, "\t\t\t\tready = true");
let _ = writeln!(out, "\t\t\t\tbreak");
let _ = writeln!(out, "\t\t\t}}");
let _ = writeln!(out, "\t\t\ttime.Sleep(50 * time.Millisecond)");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t\tif !ready {{");
let _ = writeln!(out, "\t\t\tpanic(\"mock-server did not become ready within 20s\")");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
let _ = writeln!(out, "\treturn m.Run()");
let _ = writeln!(out, "}}");
return out;
}
if !has_http_fixtures {
let _ = writeln!(out, "\tos.Exit(m.Run())");
let _ = writeln!(out, "}}");
return out;
}
let _ = writeln!(
out,
"\t// If SUT_URL is already set, a parent process started a shared harness."
);
let _ = writeln!(out, "\t// Use it as-is and do NOT spawn our own.");
let _ = writeln!(out, "\tif os.Getenv(\"SUT_URL\") != \"\" {{");
let _ = writeln!(out, "\t\tos.Exit(m.Run())");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
let _ = writeln!(out, "\t// Spawn the harness executable.");
let _ = writeln!(
out,
"\tharnessBin := filepath.Join(dir, \"cmd\", \"harness\", \"harness\")"
);
let _ = writeln!(out, "\tcmd := exec.Command(harnessBin)");
let _ = writeln!(out, "\tcmd.Stderr = os.Stderr");
let _ = writeln!(out, "\t// Keep pipes open so harness doesn't exit immediately.");
let _ = writeln!(out, "\tstdin, err := cmd.StdinPipe()");
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\tpanic(fmt.Sprintf(\"stdin pipe: %v\", err))");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tstdout, err := cmd.StdoutPipe()");
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\tpanic(fmt.Sprintf(\"stdout pipe: %v\", err))");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\tif err := cmd.Start(); err != nil {{");
let _ = writeln!(out, "\t\tpanic(fmt.Sprintf(\"start harness: %v\", err))");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
let _ = writeln!(out, "\tcode := runHarnessTests(m, cmd, stdin, stdout)");
let _ = writeln!(out, "\tos.Exit(code)");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(
out,
"// runHarnessTests executes the test suite and cleans up harness resources."
);
let _ = writeln!(
out,
"// This helper avoids 'exitAfterDefer' linter violations by letting"
);
let _ = writeln!(
out,
"// implicit resource cleanup happen before TestMain calls os.Exit."
);
let _ = writeln!(
out,
"func runHarnessTests(m *testing.M, cmd *exec.Cmd, stdin io.WriteCloser, stdout io.ReadCloser) int {{"
);
let _ = writeln!(out, "\t// Drain stdout so the pipe doesn't block.");
let _ = writeln!(out, "\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
let _ = writeln!(out);
let _ = writeln!(out, "\t// Poll TCP port 8012 until harness is ready (15s timeout).");
let _ = writeln!(out, "\thost := \"127.0.0.1\"");
let _ = writeln!(out, "\tport := \"8012\"");
let _ = writeln!(out, "\tsutURL := \"http://\" + host + \":\" + port");
let _ = writeln!(out, "\tdeadline := time.Now().Add(15 * time.Second)");
let _ = writeln!(out, "\tfor time.Now().Before(deadline) {{");
let _ = writeln!(
out,
"\t\tconn, err := net.DialTimeout(\"tcp\", host+\":\"+port, 500*time.Millisecond)"
);
let _ = writeln!(out, "\t\tif err == nil {{");
let _ = writeln!(out, "\t\t\tconn.Close()");
let _ = writeln!(out, "\t\t\tbreak");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t\tif cmd.ProcessState != nil {{");
let _ = writeln!(out, "\t\t\t// Harness exited early.");
let _ = writeln!(out, "\t\t\tstderr, _ := io.ReadAll(os.Stderr)");
let _ = writeln!(out, "\t\t\tpanic(fmt.Sprintf(\"harness died: %s\", stderr))");
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t\ttime.Sleep(100 * time.Millisecond)");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out);
let _ = writeln!(out, "\tos.Setenv(\"SUT_URL\", sutURL)");
let _ = writeln!(out, "\tcode := m.Run()");
let _ = writeln!(out);
let _ = writeln!(out, "\t// Cleanup: close stdin and wait for harness.");
let _ = writeln!(out, "\t_ = stdin.Close()");
let _ = writeln!(out, "\t_ = cmd.Process.Signal(os.Interrupt)");
let _ = writeln!(out, "\t_ = cmd.Wait()");
let _ = writeln!(out);
let _ = writeln!(out, "\treturn code");
let _ = writeln!(out, "}}");
out
}
fn render_helpers_test_go() -> String {
let mut out = String::new();
let _ = writeln!(out, "package e2e_test");
let _ = writeln!(out);
let _ = writeln!(out, "import \"encoding/json\"");
let _ = writeln!(out);
let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
let _ = writeln!(
out,
"// Array fields use jsonString instead of fmt.Sprint to preserve structure."
);
let _ = writeln!(out, "//nolint:unused");
let _ = writeln!(out, "func jsonString(value any) string {{");
let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\treturn \"\"");
let _ = writeln!(out, "\t}}");
let _ = writeln!(out, "\treturn string(encoded)");
let _ = writeln!(out, "}}");
out
}
fn render_harness_main(_e2e_config: &E2eConfig, groups: &[FixtureGroup], go_module_path: &str) -> String {
use minijinja::{Environment, context};
let mut fixtures_map = serde_json::Map::new();
for group in groups {
for fixture in &group.fixtures {
if fixture.http.is_none() {
continue;
}
let http_data = fixture.http.as_ref().unwrap();
let fixture_json = serde_json::json!({
"http": {
"handler": {
"route": &http_data.handler.route,
"method": &http_data.handler.method,
"body_schema": http_data.handler.body_schema.clone(),
},
"expected_response": {
"status_code": http_data.expected_response.status_code,
"body": &http_data.expected_response.body,
"headers": &http_data.expected_response.headers,
}
}
});
fixtures_map.insert(fixture.id.clone(), fixture_json);
}
}
let fixtures_json_obj = serde_json::Value::Object(fixtures_map);
let fixtures_json_str = serde_json::to_string(&fixtures_json_obj).unwrap_or_default();
let fixtures_json = fixtures_json_str.replace('\\', "\\\\").replace('"', "\\\"");
let mut env = Environment::new();
let harness_template = include_str!("../templates/go/harness_main.go.jinja");
env.add_template("harness", harness_template).ok();
let import_alias = go_module_path.rsplit('/').next().unwrap_or("pkg").to_string();
let template = env.get_template("harness").unwrap();
let output = template
.render(context! {
imports => vec![go_module_path],
import_alias => import_alias,
register_route_method => "RegisterRoute",
run_method => "Run",
start_background_method => "StartBackground",
port => 8012,
fixtures_json => fixtures_json,
})
.unwrap_or_default();
let mut out = hash::header(CommentStyle::DoubleSlash);
out.push_str(&output);
out
}
mod assertions;
mod json_values;
mod method_calls;
mod setup;
mod test_backend;
mod test_file;
mod test_function;
mod visitors;
pub use test_backend::emit_test_backend;
use test_file::{GoTestFileContext, render_test_file};
use test_function::fixture_has_go_callable;
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn render_env_setup_empty_returns_empty_string() {
let env = HashMap::new();
let out = render_env_setup(&env);
assert_eq!(out, "", "empty env should produce empty output");
}
#[test]
fn render_env_setup_single_var_contains_key_and_value() {
let mut env = HashMap::new();
env.insert("KREUZCRAWL_ALLOW_PRIVATE_NETWORK".to_string(), "true".to_string());
let out = render_env_setup(&env);
assert!(
out.contains("KREUZCRAWL_ALLOW_PRIVATE_NETWORK"),
"output should contain env var name: {out}"
);
assert!(out.contains("true"), "output should contain env var value: {out}");
assert!(
out.contains("os.LookupEnv"),
"output should use os.LookupEnv for setdefault behavior: {out}"
);
assert!(out.contains("os.Setenv"), "output should call os.Setenv: {out}");
}
#[test]
fn render_env_setup_multiple_vars_are_sorted() {
let mut env = HashMap::new();
env.insert("ZEBRA".to_string(), "value1".to_string());
env.insert("APPLE".to_string(), "value2".to_string());
env.insert("BANANA".to_string(), "value3".to_string());
let out = render_env_setup(&env);
let apple_idx = out.find("APPLE").expect("should contain APPLE");
let banana_idx = out.find("BANANA").expect("should contain BANANA");
let zebra_idx = out.find("ZEBRA").expect("should contain ZEBRA");
assert!(
apple_idx < banana_idx && banana_idx < zebra_idx,
"env vars should be sorted alphabetically: {out}"
);
}
#[test]
fn render_main_test_go_includes_env_setup_at_start() {
let mut env = HashMap::new();
env.insert("TEST_VAR".to_string(), "test_value".to_string());
let out = render_main_test_go("test_documents", false, false, &env);
let dir_idx = out
.find("dir := filepath.Dir(filename)")
.expect("should contain dir assignment");
let test_var_idx = out.find("TEST_VAR").expect("should contain TEST_VAR");
assert!(dir_idx < test_var_idx, "env setup should come after dir initialization");
}
}