use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::core::config::e2e::CallConfig;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, FunctionDef, TypeDef, TypeRef};
use crate::e2e::codegen::E2eCodegen;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::{Assertion, Fixture, FixtureGroup};
use super::GoCodegen;
use super::go_batch::{GoBatchCase, GoBatchLayout, GoCaseOutcome, run_go_batch};
const PASSING_ASSERTIONS: [(&str, &str); 5] = [
("equals", "value"),
("contains", "value"),
("starts_with", "\"value"),
("ends_with", "value\""),
("matches_regex", "value"),
];
const FAILING_ASSERTIONS: [(&str, &str, &str); 5] = [
("equals", "absent", "equals mismatch"),
("contains", "absent", "expected to contain"),
("starts_with", "absent", "expected to start"),
("ends_with", "absent", "expected to end"),
("matches_regex", "^absent$", "expected value to match regex"),
];
const MARSHAL_FAILURE_CONTROL: &str = "helper_marshal_failure";
const GENERATED_PACKAGE_CASE_COUNT: usize = 11;
const SAMPLE_PACKAGE_SOURCE: &str = "package sample\ntype Choice interface { isChoice() }\ntype ChoiceValue string\nfunc (ChoiceValue) isChoice() {}\ntype Envelope struct { Choice Choice }\nfunc Inspect() (*Envelope, error) { return &Envelope{Choice: ChoiceValue(\"value\")}, nil }\n";
fn sealed_choice_ir() -> (Vec<TypeDef>, Vec<EnumDef>, Vec<FunctionDef>) {
let types = vec![TypeDef {
name: "Envelope".into(),
fields: vec![FieldDef {
name: "choice".into(),
ty: TypeRef::Named("Choice".into()),
..Default::default()
}],
..Default::default()
}];
let enums = vec![EnumDef {
name: "Choice".into(),
variants: vec![EnumVariant {
name: "Value".into(),
fields: vec![FieldDef {
name: "value".into(),
ty: TypeRef::String,
..Default::default()
}],
..Default::default()
}],
..Default::default()
}];
let functions = vec![FunctionDef {
name: "inspect".into(),
return_type: TypeRef::Named("Envelope".into()),
..Default::default()
}];
(types, enums, functions)
}
fn fixture_with_assertion(assertion_type: &str, expected: &str) -> FixtureGroup {
FixtureGroup {
category: "shape".into(),
fixtures: vec![Fixture {
id: format!("choice_{assertion_type}"),
description: "sealed choice assertion".into(),
assertions: vec![Assertion {
assertion_type: assertion_type.into(),
field: Some("choice".into()),
value: Some(serde_json::json!(expected)),
..Default::default()
}],
..Default::default()
}],
}
}
fn generate_package(assertion_type: &str, expected: &str) -> Vec<crate::core::backend::GeneratedFile> {
let (types, enums, functions) = sealed_choice_ir();
let config = E2eConfig {
call: CallConfig {
function: "inspect".into(),
module: "example.com/sample".into(),
returns_result: true,
..Default::default()
},
..Default::default()
};
GoCodegen
.generate(
&[fixture_with_assertion(assertion_type, expected)],
&config,
&Default::default(),
&types,
&enums,
&functions,
&[],
)
.expect("generate complete Go e2e package")
}
struct GeneratedPackageCase {
case: GoBatchCase,
go_mod: String,
module_dir: PathBuf,
}
fn generated_package_case(name: &str, assertion_type: &str, expected: &str) -> GeneratedPackageCase {
let files = generate_package(assertion_type, expected);
let manifest = files
.iter()
.find(|file| file.path.file_name() == Some(OsStr::new("go.mod")))
.expect("generated Go e2e package includes go.mod");
let module_dir = manifest
.path
.parent()
.expect("generated go.mod has a parent directory")
.to_path_buf();
let mut case_files = Vec::new();
for file in &files {
let relative = file
.path
.strip_prefix(&module_dir)
.expect("generated Go e2e files live beside the generated go.mod");
if relative == Path::new("go.mod") {
continue;
}
case_files.push((relative.to_string_lossy().into_owned(), file.content.clone()));
}
GeneratedPackageCase {
case: GoBatchCase {
name: name.to_owned(),
files: case_files,
},
go_mod: manifest.content.clone(),
module_dir,
}
}
fn marshal_failure_control_case() -> GoBatchCase {
let helper = super::render_helpers_test_go();
assert!(
helper.contains("func jsonString(t *testing.T, value any) string"),
"{helper}"
);
GoBatchCase {
name: MARSHAL_FAILURE_CONTROL.to_owned(),
files: vec![
("helpers_test.go".to_owned(), helper),
(
"failure_test.go".to_owned(),
"package e2e_test\nimport \"testing\"\nfunc TestMarshalFailure(t *testing.T) { jsonString(t, make(chan int)) }\n"
.to_owned(),
),
],
}
}
fn module_path_of(go_mod: &str) -> String {
go_mod
.lines()
.find_map(|line| line.strip_prefix("module "))
.expect("generated go.mod declares a module path")
.trim()
.to_owned()
}
#[test]
fn generated_data_interface_packages_compile_and_run_in_one_go_test() {
let mut generated = Vec::new();
for (assertion_type, expected) in PASSING_ASSERTIONS {
generated.push(generated_package_case(
&format!("pass_{assertion_type}"),
assertion_type,
expected,
));
}
for (assertion_type, expected, _) in FAILING_ASSERTIONS {
generated.push(generated_package_case(
&format!("fail_{assertion_type}"),
assertion_type,
expected,
));
}
let manifest = generated[0].go_mod.clone();
let module_dir = generated[0].module_dir.clone();
for case in &generated {
assert_eq!(
case.go_mod, manifest,
"case `{}` generated a different go.mod; it can no longer share a module root",
case.case.name
);
assert_eq!(
case.module_dir, module_dir,
"case `{}` moved output_base",
case.case.name
);
}
let mut cases: Vec<GoBatchCase> = generated.into_iter().map(|entry| entry.case).collect();
cases.push(marshal_failure_control_case());
let inventory: Vec<String> = cases.iter().map(|case| case.name.clone()).collect();
assert_eq!(
inventory.len(),
GENERATED_PACKAGE_CASE_COUNT,
"the generated-package inventory changed; update GENERATED_PACKAGE_CASE_COUNT deliberately"
);
let layout = GoBatchLayout {
root_files: vec![
(
PathBuf::from("packages/go/go.mod"),
"module example.com/sample\n\ngo 1.26\n".to_owned(),
),
(PathBuf::from("packages/go/sample.go"), SAMPLE_PACKAGE_SOURCE.to_owned()),
(module_dir.join("go.mod"), manifest.clone()),
],
module_dir,
module_path: module_path_of(&manifest),
extra_args: vec!["-mod=mod".to_owned()],
};
let report = run_go_batch(&layout, &cases);
report.assert_inventory(&inventory);
for (assertion_type, _) in PASSING_ASSERTIONS {
let name = format!("pass_{assertion_type}");
report.assert_outcome(&name, GoCaseOutcome::Passed);
assert!(
report.case(&name).test_case_count >= 1,
"case `{name}` selected no Go test to run:\n{}",
report.case(&name).output
);
}
for (assertion_type, _, diagnostic) in FAILING_ASSERTIONS {
let name = format!("fail_{assertion_type}");
report.assert_outcome(&name, GoCaseOutcome::Failed);
report.assert_output_contains(&name, diagnostic);
}
report.assert_outcome(MARSHAL_FAILURE_CONTROL, GoCaseOutcome::Failed);
report.assert_output_contains(MARSHAL_FAILURE_CONTROL, "marshal assertion value as JSON");
assert_eq!(
report.total_test_cases(),
GENERATED_PACKAGE_CASE_COUNT,
"the batch executed a different number of Go tests than it has cases"
);
}
#[test]
fn generated_equals_data_interface_emits_json_helper() {
let files = generate_package("equals", "value");
assert!(
files.iter().any(|file| file.path.ends_with("helpers_test.go")),
"equals emits jsonString and must emit its package helper"
);
let shape = files
.iter()
.find(|file| file.path.ends_with("shape_test.go"))
.expect("shape_test.go is generated");
assert_eq!(
shape.content.matches("jsonString(t, result.Choice)").count(),
1,
"generated assertion must call the real JSON helper:\n{}",
shape.content
);
}