pub mod assertion_recipes;
#[cfg(test)]
mod assertion_type_funnel_pairing;
pub(crate) mod assertion_type_skip;
pub mod assertion_types;
pub mod brew;
pub mod c;
pub(crate) mod call_ir;
pub mod client;
pub mod client_factory;
pub mod csharp;
pub mod dart;
mod dart_visitors;
pub(crate) mod declared_error_variant;
pub mod elixir;
pub(crate) mod error_path_assertions;
pub(crate) mod field_skip;
pub mod gleam;
pub mod go;
pub mod homebrew;
pub(crate) mod inert_example;
pub mod java;
mod java_mvnw;
pub mod kotlin;
pub mod kotlin_android;
pub(crate) mod not_error_presence;
pub mod php;
pub mod php_ext;
mod presentation;
pub mod python;
pub mod r;
pub mod recipe;
pub mod ruby;
pub mod rust;
pub mod streaming_assertions;
pub mod swift;
mod swift_visitors;
pub mod typescript;
pub mod wasm;
#[cfg(test)]
mod working_directory_guard_tests;
pub mod zig;
mod zig_visitors;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, MethodDef, TypeDef};
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InclusionDecision {
Include,
Exclude(&'static str),
}
impl InclusionDecision {
pub fn is_included(&self) -> bool {
matches!(self, Self::Include)
}
}
pub fn fixture_inclusion(fixture: &Fixture, language: &str, e2e_config: &E2eConfig) -> InclusionDecision {
if !e2e_config.exclude_categories.is_empty() && e2e_config.exclude_categories.contains(&fixture.resolved_category())
{
return InclusionDecision::Exclude("excluded category");
}
if let Some(skip) = &fixture.skip
&& skip.should_skip(language)
{
return InclusionDecision::Exclude("fixture skip directive");
}
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
if call_config.skip_languages.iter().any(|l| l == language) {
return InclusionDecision::Exclude("call skips language");
}
let is_http_fixture = fixture.mock_response.is_some() || fixture.http.is_some();
if !is_http_fixture && call_config.function.is_empty() && !call_config.overrides.contains_key(language) {
return InclusionDecision::Exclude("no callable function");
}
InclusionDecision::Include
}
pub(crate) fn should_include_fixture(fixture: &Fixture, language: &str, e2e_config: &E2eConfig) -> bool {
fixture_inclusion(fixture, language, e2e_config).is_included()
}
pub(crate) fn percent_encode_query(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for &byte in value.as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => out.push(byte as char),
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
pub(crate) fn transform_json_keys_for_language(value: &serde_json::Value, wire_case: &str) -> serde_json::Value {
use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase};
let rewrite_key: fn(&str) -> String = match wire_case {
"camelCase" => |k| k.to_lower_camel_case(),
"PascalCase" => |k| k.to_pascal_case(),
"SCREAMING_SNAKE_CASE" => |k| k.to_shouty_snake_case(),
"kebab-case" => |k| k.to_kebab_case(),
"SCREAMING-KEBAB-CASE" => |k| k.to_shouty_kebab_case(),
_ => |k| k.to_snake_case(),
};
fn walk(value: &serde_json::Value, rewrite_key: fn(&str) -> String) -> serde_json::Value {
match value {
serde_json::Value::Object(obj) => {
let new_obj: serde_json::Map<String, serde_json::Value> = obj
.iter()
.map(|(k, v)| (rewrite_key(k), walk(v, rewrite_key)))
.collect();
serde_json::Value::Object(new_obj)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(|v| walk(v, rewrite_key)).collect())
}
other => other.clone(),
}
}
walk(value, rewrite_key)
}
pub(crate) const MOCK_URL_PLACEHOLDER: &str = "$mock_url";
pub(crate) fn value_contains_mock_url_placeholder(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::String(value) => value.contains(MOCK_URL_PLACEHOLDER),
serde_json::Value::Array(values) => values.iter().any(value_contains_mock_url_placeholder),
serde_json::Value::Object(values) => values.values().any(value_contains_mock_url_placeholder),
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => false,
}
}
pub(crate) fn mock_url_env_key(fixture_id: &str) -> String {
format!("MOCK_SERVER_{}", fixture_id.to_uppercase())
}
pub(crate) fn declared_error_value(fixture: &crate::e2e::fixture::Fixture) -> Option<&str> {
fixture
.assertions
.iter()
.find(|assertion| assertion.assertion_type == "error")
.and_then(|assertion| assertion.value.as_ref())
.and_then(serde_json::Value::as_str)
}
pub(crate) fn preserved_url_literal(preserve: bool, value: &serde_json::Value) -> Option<&str> {
if !preserve {
return None;
}
value.as_str()
}
pub(crate) fn preserved_url_list(preserve: bool, value: &serde_json::Value) -> Option<Vec<&str>> {
if !preserve {
return None;
}
value.as_array()?.iter().map(serde_json::Value::as_str).collect()
}
pub(crate) const STRICT_ASSERTIONS_ENV: &str = "ALEF_E2E_STRICT_ASSERTIONS";
fn strict_assertions_default() -> bool {
true
}
pub(crate) fn strict_assertions_enabled() -> bool {
std::env::var(STRICT_ASSERTIONS_ENV)
.ok()
.map_or_else(strict_assertions_default, |raw| !is_falsy_flag(&raw))
}
fn is_falsy_flag(raw: &str) -> bool {
raw == "0" || raw.eq_ignore_ascii_case("false")
}
const COARSE_FIELD_ORACLE_LANGUAGES: &[&str] = &["gleam", "brew"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SkipVerdict {
UnacknowledgedGap,
Limitation,
AwaitingGeneratorSupport,
Acknowledged(crate::e2e::fixture::AssertionSkipKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SkipOrigin {
Field,
AssertionType,
}
#[derive(Debug, Clone)]
pub(crate) struct SkipRecord {
pub(crate) language: String,
pub(crate) fixture_id: String,
pub(crate) field: String,
pub(crate) verdict: SkipVerdict,
pub(crate) origin: SkipOrigin,
}
thread_local! {
static SKIP_LEDGER: std::cell::RefCell<Vec<SkipRecord>> = const { std::cell::RefCell::new(Vec::new()) };
}
pub(crate) fn take_skip_records() -> Vec<SkipRecord> {
SKIP_LEDGER.with(|ledger| std::mem::take(&mut *ledger.borrow_mut()))
}
pub(crate) fn peek_skip_records(language: &str, fixture_id: &str) -> Vec<SkipRecord> {
SKIP_LEDGER.with(|ledger| {
ledger
.borrow()
.iter()
.filter(|record| record.language == language && record.fixture_id == fixture_id)
.cloned()
.collect()
})
}
pub(crate) fn skip_summary(records: &[SkipRecord]) -> Option<String> {
if records.is_empty() {
return None;
}
use crate::e2e::fixture::AssertionSkipKind;
let fixtures: std::collections::BTreeSet<&str> = records.iter().map(|r| r.fixture_id.as_str()).collect();
let count = |predicate: fn(&SkipVerdict) -> bool| records.iter().filter(|r| predicate(&r.verdict)).count();
let awaiting = count(|v| {
matches!(
v,
SkipVerdict::AwaitingGeneratorSupport | SkipVerdict::Acknowledged(AssertionSkipKind::NotRepresentable)
)
});
let limitations = count(|v| {
matches!(
v,
SkipVerdict::Limitation | SkipVerdict::Acknowledged(AssertionSkipKind::LanguageLimitation)
)
});
let gaps = count(|v| matches!(v, SkipVerdict::UnacknowledgedGap));
let type_skips = records.iter().filter(|r| r.origin == SkipOrigin::AssertionType).count();
let type_skip_suffix = if type_skips > 0 {
format!(", {type_skips} from an assertion type this backend cannot render rather than an unavailable field")
} else {
String::new()
};
Some(format!(
"{} assertion(s) skipped across {} fixture(s): {awaiting} awaiting alef support, \
{limitations} language/ABI limitation(s), {gaps} unresolved field path(s){type_skip_suffix}",
records.len(),
fixtures.len(),
))
}
pub(crate) fn fail_on_unavailable_field_markers(
body: &str,
language: &str,
fixture_id: &str,
assertions: &[crate::e2e::fixture::Assertion],
) {
let coarse_oracle = COARSE_FIELD_ORACLE_LANGUAGES.contains(&language);
for line in body.lines() {
let Some((field, skip)) = field_skip::FieldSkip::extract_classified(line) else {
continue;
};
let declared = assertions
.iter()
.filter(|assertion| assertion.field.as_deref() == Some(field))
.find_map(|assertion| assertion.skip.as_ref())
.filter(|skip| skip.should_skip(language));
let verdict = match (declared, skip.class()) {
(Some(declaration), _) => SkipVerdict::Acknowledged(declaration.kind()),
(None, field_skip::SkipClass::GeneratorGap) => SkipVerdict::AwaitingGeneratorSupport,
(None, field_skip::SkipClass::AuthoringGap) if !coarse_oracle => SkipVerdict::UnacknowledgedGap,
(None, _) => SkipVerdict::Limitation,
};
SKIP_LEDGER.with(|ledger| {
ledger.borrow_mut().push(SkipRecord {
language: language.to_string(),
fixture_id: fixture_id.to_string(),
field: field.to_string(),
verdict,
origin: SkipOrigin::Field,
});
});
}
}
pub(crate) fn fail_on_unsupported_assertion_type_markers(body: &str, language: &str, fixture_id: &str) {
for line in body.lines() {
let Some((token, skip)) = assertion_type_skip::AssertionTypeSkip::extract_classified(line) else {
continue;
};
let verdict = match skip.class() {
field_skip::SkipClass::GeneratorGap => SkipVerdict::AwaitingGeneratorSupport,
field_skip::SkipClass::LanguageLimitation | field_skip::SkipClass::AuthoringGap => SkipVerdict::Limitation,
};
SKIP_LEDGER.with(|ledger| {
ledger.borrow_mut().push(SkipRecord {
language: language.to_string(),
fixture_id: fixture_id.to_string(),
field: token.to_string(),
verdict,
origin: SkipOrigin::AssertionType,
});
});
}
}
pub(crate) fn strict_assertion_failure(records: &[SkipRecord], strict: bool) -> Option<anyhow::Error> {
if !strict {
return None;
}
let gaps: Vec<&SkipRecord> = records
.iter()
.filter(|r| r.verdict == SkipVerdict::UnacknowledgedGap)
.collect();
if gaps.is_empty() {
return None;
}
let detail = gaps
.iter()
.map(|r| format!(" [{}] fixture `{}`: field `{}`", r.language, r.fixture_id, r.field))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.join("\n");
Some(anyhow::anyhow!(
"{} e2e assertion(s) reference a field the availability oracle cannot resolve, so they \
would have been silently dropped and the generated tests would have passed while \
asserting nothing:\n{detail}\n\nEither fix the field path (or the field-availability \
config) so the assertion runs, or declare on the assertion why it cannot:\n \
\"skip\": {{ \"kind\": \"not_representable\", \"reason\": \"...\" }} — alef cannot \
express this shape yet (an assertion *kind* such as \"the call errored\", a property of \
the call rather than the result, or an assertion over a stream's events)\n \
\"skip\": {{ \"kind\": \"language_limitation\", \"languages\": [\"<lang>\"], \
\"reason\": \"...\" }} — this binding genuinely cannot reach the field\nEither way the \
skip stays counted in the end-of-run summary, in the bucket that names who owns it. Set \
{}=0 to downgrade this to a warning for one run.",
gaps.len(),
STRICT_ASSERTIONS_ENV,
))
}
#[cfg(test)]
mod unavailable_field_marker_tests {
use super::{
SkipVerdict, fail_on_unavailable_field_markers, is_falsy_flag, skip_summary, strict_assertion_failure,
take_skip_records,
};
use crate::e2e::fixture::{Assertion, AssertionSkip, AssertionSkipDirective, AssertionSkipKind};
fn verdicts_for(body: &str, language: &str, assertions: &[Assertion]) -> Vec<SkipVerdict> {
let _ = take_skip_records();
fail_on_unavailable_field_markers(body, language, "smoke", assertions);
take_skip_records().into_iter().map(|r| r.verdict).collect()
}
fn strict_error_for(body: &str, language: &str, fixture_id: &str, assertions: &[Assertion]) -> Option<String> {
let _ = take_skip_records();
fail_on_unavailable_field_markers(body, language, fixture_id, assertions);
strict_assertion_failure(&take_skip_records(), true).map(|error| format!("{error:#}"))
}
fn assertion_on(field: &str, skip: Option<AssertionSkip>) -> Assertion {
Assertion {
field: Some(field.to_string()),
skip,
..Assertion::default()
}
}
#[test]
fn non_strict_is_a_noop_even_on_a_marker_body() {
let _ = take_skip_records();
fail_on_unavailable_field_markers(
" # skipped: field 'chunks' not available on result type\n",
"python",
"widget_smoke",
&[],
);
assert!(strict_assertion_failure(&take_skip_records(), false).is_none());
}
#[test]
fn strict_fails_loudly_naming_fixture_and_field() {
let error = strict_error_for(
" # skipped: field 'chunks' not available on result type\n",
"python",
"widget_smoke",
&[],
)
.expect("an unresolved field must fail under strict");
assert!(
error.contains("[python] fixture `widget_smoke`: field `chunks`"),
"got: {error}"
);
}
#[test]
fn an_unmappable_field_is_fatal_by_default() {
let _ = take_skip_records();
let body = " // skipped: field 'strategy.crawl_order' not available on result type\n";
fail_on_unavailable_field_markers(
body,
"go",
"traversal_order",
&[assertion_on("strategy.crawl_order", None)],
);
let error = strict_assertion_failure(&take_skip_records(), super::strict_assertions_enabled())
.expect("an unmappable field must fail generation by default");
assert!(
format!("{error:#}").contains("field `strategy.crawl_order`"),
"got: {error:#}"
);
}
#[test]
fn strict_error_lists_every_offender() {
let _ = take_skip_records();
fail_on_unavailable_field_markers(
" // skipped: field 'alpha' not available on result type\n\
\x20 // skipped: field 'beta' not available on result type\n",
"go",
"smoke",
&[],
);
let error = strict_assertion_failure(&take_skip_records(), true).expect("two gaps must fail");
let rendered = format!("{error:#}");
assert!(rendered.contains("field `alpha`"), "got: {rendered}");
assert!(rendered.contains("field `beta`"), "got: {rendered}");
assert!(rendered.starts_with("2 e2e assertion(s)"), "got: {rendered}");
}
#[test]
fn an_explicitly_opted_out_field_skips_and_is_counted() {
let _ = take_skip_records();
let body = " // skipped: field 'strategy.crawl_order' not available on result type\n";
let assertions = [assertion_on(
"strategy.crawl_order",
Some(AssertionSkip::Scoped(AssertionSkipDirective {
languages: vec!["go".to_string()],
kind: AssertionSkipKind::LanguageLimitation,
reason: Some("traversal order is not exposed on the Go result".to_string()),
})),
)];
fail_on_unavailable_field_markers(body, "go", "traversal_order", &assertions);
let records = take_skip_records();
assert!(
strict_assertion_failure(&records, true).is_none(),
"an acknowledged skip must not fail generation"
);
assert_eq!(records.len(), 1, "the acknowledged skip must still be recorded");
assert_eq!(
records[0].verdict,
SkipVerdict::Acknowledged(AssertionSkipKind::LanguageLimitation)
);
assert_eq!(records[0].field, "strategy.crawl_order");
let summary = skip_summary(&records).expect("an acknowledged skip must still produce a summary");
assert_eq!(
summary,
"1 assertion(s) skipped across 1 fixture(s): 0 awaiting alef support, \
1 language/ABI limitation(s), 0 unresolved field path(s)"
);
}
#[test]
fn a_not_representable_opt_out_is_attributed_to_alef() {
let _ = take_skip_records();
let body = " // skipped: field 'is_error' not available on result type\n";
let assertions = [assertion_on(
"is_error",
Some(AssertionSkip::Scoped(AssertionSkipDirective {
languages: Vec::new(),
kind: AssertionSkipKind::NotRepresentable,
reason: Some("`is_error` is an assertion kind, not a field path".to_string()),
})),
)];
fail_on_unavailable_field_markers(body, "go", "error_smoke", &assertions);
let records = take_skip_records();
assert!(strict_assertion_failure(&records, true).is_none());
let summary = skip_summary(&records).expect("summary");
assert!(summary.contains("1 awaiting alef support"), "got: {summary}");
assert!(summary.contains("0 language/ABI limitation(s)"), "got: {summary}");
}
#[test]
fn a_bare_true_skip_covers_every_language() {
let body = " // skipped: field 'chunks' not available on result type\n";
let assertions = [assertion_on("chunks", Some(AssertionSkip::All(true)))];
let expected = [SkipVerdict::Acknowledged(AssertionSkipKind::NotRepresentable)];
assert_eq!(verdicts_for(body, "dart", &assertions), expected);
assert_eq!(verdicts_for(body, "ruby", &assertions), expected);
}
#[test]
fn a_scoped_skip_does_not_cover_other_languages() {
let body = " // skipped: field 'chunks' not available on result type\n";
let assertions = [assertion_on(
"chunks",
Some(AssertionSkip::Scoped(AssertionSkipDirective {
languages: vec!["dart".to_string()],
kind: AssertionSkipKind::LanguageLimitation,
reason: None,
})),
)];
assert_eq!(
verdicts_for(body, "dart", &assertions),
vec![SkipVerdict::Acknowledged(AssertionSkipKind::LanguageLimitation)]
);
assert_eq!(
verdicts_for(body, "go", &assertions),
vec![SkipVerdict::UnacknowledgedGap],
"an opt-out scoped to dart must leave go fatal"
);
}
#[test]
fn a_false_skip_does_not_opt_out() {
let body = " // skipped: field 'chunks' not available on result type\n";
let assertions = [assertion_on("chunks", Some(AssertionSkip::All(false)))];
assert_eq!(
verdicts_for(body, "go", &assertions),
vec![SkipVerdict::UnacknowledgedGap]
);
}
#[test]
fn a_skip_on_another_field_does_not_opt_this_one_out() {
let body = " // skipped: field 'chunks' not available on result type\n";
let assertions = [assertion_on("usage", Some(AssertionSkip::All(true)))];
assert_eq!(
verdicts_for(body, "go", &assertions),
vec![SkipVerdict::UnacknowledgedGap]
);
}
#[test]
fn a_body_with_no_marker_records_nothing() {
assert!(verdicts_for(" assert result.count == 1 # noqa: S101\n", "python", &[]).is_empty());
}
#[test]
fn unsupported_assertion_type_comments_are_not_recorded() {
assert!(
verdicts_for(
" // skipped: unsupported assertion type on synthetic field 'embeddings'\n",
"go",
&[]
)
.is_empty()
);
}
#[test]
fn language_suffixed_not_available_comments_stay_fatal() {
let error = strict_error_for(
"\t// skipped: field 'keywords' not available on Go ProcessingResult\n",
"go",
"smoke",
&[],
)
.expect("a binding-type resolution miss is a gap");
assert!(error.contains("field `keywords`"), "got: {error}");
}
#[test]
fn streaming_field_assertions_await_alef_support_rather_than_failing() {
let body = " // streaming assertion on unsupported field 'has_page_event'\n";
assert_eq!(
verdicts_for(body, "csharp", &[]),
vec![SkipVerdict::AwaitingGeneratorSupport]
);
assert!(
strict_error_for(body, "csharp", "stream_smoke", &[]).is_none(),
"a missing generator feature must not fail a consumer's build"
);
}
#[test]
fn every_streaming_wording_awaits_alef_support() {
for (body, language) in [
(
" # skipped: streaming field 'stream.items': no python accessor\n",
"python",
),
(
" // skipped: field 'stream.items' not available on streaming result type\n",
"go",
),
] {
assert_eq!(
verdicts_for(body, language, &[]),
vec![SkipVerdict::AwaitingGeneratorSupport],
"{language} streaming wording must be alef's debt, not the consumer's"
);
}
}
#[test]
fn summary_separates_alef_backlog_from_binding_limits() {
let _ = take_skip_records();
fail_on_unavailable_field_markers(
" // streaming assertion on unsupported field 'has_page_event'\n",
"csharp",
"stream_smoke",
&[],
);
fail_on_unavailable_field_markers(
" // skipped: field 'usage.tokens' references a field or type excluded from \
the Swift binding\n",
"swift",
"excluded_smoke",
&[],
);
let summary = skip_summary(&take_skip_records()).expect("summary");
assert_eq!(
summary,
"2 assertion(s) skipped across 2 fixture(s): 1 awaiting alef support, \
1 language/ABI limitation(s), 0 unresolved field path(s)"
);
}
#[test]
fn tagged_union_boundary_wordings_are_counted_not_fatal() {
let dart = " // skipped: field 'payload.tags' crosses a tagged-union variant boundary \
(not expressible in Dart)\n";
assert_eq!(verdicts_for(dart, "dart", &[]), vec![SkipVerdict::Limitation]);
let swift = " // skipped: field 'payload.tags' crosses a tagged-union variant boundary \
(not expressible in Swift)\n";
assert_eq!(verdicts_for(swift, "swift", &[]), vec![SkipVerdict::Limitation]);
}
#[test]
fn the_swift_json_bridged_count_wording_is_counted_not_fatal() {
let body = format!(
" // skipped: {}\n",
super::field_skip::FieldSkip::CountOnJsonBridgedLeafInSwift.message("metadata.headings.length")
);
assert_eq!(verdicts_for(&body, "swift", &[]), vec![SkipVerdict::Limitation]);
assert!(
strict_error_for(&body, "swift", "metadata_headings", &[]).is_none(),
"a resolvable field refused for an ABI reason must not fail a consumer's build"
);
}
#[test]
fn ruby_serialized_enum_accessor_wording_is_counted_not_fatal() {
let body = " # skipped: enum variant accessor 'metadata.format.excel' not available on Ruby \
(serialized to Hash)\n";
assert_eq!(verdicts_for(body, "ruby", &[]), vec![SkipVerdict::Limitation]);
}
#[test]
fn result_is_simple_template_wording_is_counted_not_fatal() {
let body = " // skipped: result_is_simple, field 'metadata.title' not on simple result type\n";
assert_eq!(verdicts_for(body, "php", &[]), vec![SkipVerdict::Limitation]);
}
#[test]
fn not_applicable_for_simple_result_wording_is_counted_not_fatal() {
let body = " # skipped: field 'structure.headings' not applicable for simple result type\n";
assert_eq!(verdicts_for(body, "python", &[]), vec![SkipVerdict::Limitation]);
}
#[test]
fn swift_binding_exclusion_wording_is_counted_not_fatal() {
let body = " // skipped: field 'usage.tokens' references a field or type excluded from \
the Swift binding\n";
assert_eq!(verdicts_for(body, "swift", &[]), vec![SkipVerdict::Limitation]);
}
#[test]
fn the_result_is_simple_resolver_wording_stays_a_gap() {
let body = " # skipped: result_is_simple for field 'metadata' not available on result type\n";
assert_eq!(verdicts_for(body, "ruby", &[]), vec![SkipVerdict::UnacknowledgedGap]);
}
#[test]
fn coarse_oracle_backends_downgrade_gaps_to_limitations() {
let body = " // skipped: field 'chunks' not available on result type\n";
assert_eq!(verdicts_for(body, "gleam", &[]), vec![SkipVerdict::Limitation]);
assert_eq!(verdicts_for(body, "brew", &[]), vec![SkipVerdict::Limitation]);
assert_eq!(
verdicts_for(body, "go", &[]),
vec![SkipVerdict::UnacknowledgedGap],
"the same wording must stay fatal on an IR-wired backend"
);
}
#[test]
fn summary_is_none_when_nothing_was_skipped() {
assert_eq!(skip_summary(&[]), None);
}
#[test]
fn summary_counts_distinct_fixtures_not_markers() {
let _ = take_skip_records();
let body = " // skipped: field 'usage.tokens' references a field or type excluded from \
the Swift binding\n";
fail_on_unavailable_field_markers(body, "swift", "alpha", &[]);
fail_on_unavailable_field_markers(body, "swift", "alpha", &[]);
fail_on_unavailable_field_markers(body, "swift", "beta", &[]);
let summary = skip_summary(&take_skip_records()).expect("three markers must summarise");
assert!(
summary.starts_with("3 assertion(s) skipped across 2 fixture(s):"),
"got: {summary}"
);
}
#[test]
fn is_falsy_flag_accepts_zero_and_case_insensitive_false_only() {
assert!(is_falsy_flag("0"));
assert!(is_falsy_flag("false"));
assert!(is_falsy_flag("FALSE"));
assert!(!is_falsy_flag("1"));
assert!(!is_falsy_flag("true"));
assert!(!is_falsy_flag(""));
assert!(!is_falsy_flag("no"));
}
#[test]
fn diagnostic_names_language_fixture_field_and_the_opt_in() {
let message = strict_error_for(
" # skipped: field 'usage' not available on result type\n",
"ruby",
"batch_smoke",
&[],
)
.expect("an unresolved field must fail under strict");
assert!(message.contains("[ruby]"), "got: {message}");
assert!(message.contains("`batch_smoke`"), "got: {message}");
assert!(message.contains("`usage`"), "got: {message}");
assert!(message.contains("\"skip\""), "must name the opt-in: {message}");
assert!(
message.contains(super::STRICT_ASSERTIONS_ENV),
"must name the escape hatch: {message}"
);
}
}
#[cfg(test)]
mod assertion_type_marker_tests {
use super::{SkipOrigin, SkipVerdict, fail_on_unsupported_assertion_type_markers, skip_summary, take_skip_records};
fn verdicts_for(body: &str, language: &str) -> Vec<SkipVerdict> {
let _ = take_skip_records();
fail_on_unsupported_assertion_type_markers(body, language, "smoke");
take_skip_records().into_iter().map(|r| r.verdict).collect()
}
#[test]
fn recorded_markers_are_tagged_with_the_assertion_type_origin() {
let _ = take_skip_records();
fail_on_unsupported_assertion_type_markers(
"\t// skipped: unsupported assertion type on synthetic field 'embeddings'\n",
"go",
"smoke",
);
let records = take_skip_records();
assert_eq!(records.len(), 1);
assert_eq!(records[0].origin, SkipOrigin::AssertionType);
}
#[test]
fn generator_gap_wordings_await_alef_support() {
let body = " // skipped: unsupported traversal assertion 'equals' on 'pages[].url'\n";
assert_eq!(verdicts_for(body, "go"), vec![SkipVerdict::AwaitingGeneratorSupport]);
}
#[test]
fn the_streaming_assertion_type_wording_awaits_alef_support() {
let line =
super::assertion_type_skip::streaming_assertion_type_skip_line(" ", "//", "chunks", "matches_regex");
let body = format!("{line}\n");
assert_eq!(verdicts_for(&body, "go"), vec![SkipVerdict::AwaitingGeneratorSupport]);
}
#[test]
fn the_streaming_assertion_value_wording_awaits_alef_support() {
let line = super::assertion_type_skip::streaming_assertion_value_skip_line(" ", "//", "chunks", "count_min");
let body = format!("{line}\n");
assert_eq!(verdicts_for(&body, "dart"), vec![SkipVerdict::AwaitingGeneratorSupport]);
}
#[test]
fn language_limitation_wordings_are_counted_as_limitations() {
let body = " // skipped: field 'content' is a scalar String without meaningful .count\n";
assert_eq!(verdicts_for(body, "swift"), vec![SkipVerdict::Limitation]);
}
#[test]
fn field_availability_markers_are_not_recorded_by_this_gate() {
let body = " // skipped: field 'chunks' not available on result type\n";
assert!(verdicts_for(body, "python").is_empty());
}
#[test]
fn a_body_with_no_marker_records_nothing() {
assert!(verdicts_for(" assert result.count == 1\n", "python").is_empty());
}
#[test]
fn summary_calls_out_assertion_type_skips_separately() {
let _ = take_skip_records();
fail_on_unsupported_assertion_type_markers(
" // skipped: unsupported traversal assertion 'equals' on 'pages[].url'\n",
"go",
"traversal_smoke",
);
let summary = skip_summary(&take_skip_records()).expect("summary");
assert!(
summary.contains("1 from an assertion type this backend cannot render"),
"got: {summary}"
);
}
}
pub trait E2eCodegen: Send + Sync {
#[allow(clippy::too_many_arguments)]
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>>;
fn supported_assertion_types(&self) -> Vec<&'static str> {
assertion_types::supported_assertion_types(self.language_name())
}
#[allow(clippy::too_many_arguments)]
fn generate_gated(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>> {
assertion_types::ensure_supported_assertion_types(
groups,
e2e_config,
self.language_name(),
&self.supported_assertion_types(),
)?;
self.generate(groups, e2e_config, config, type_defs, enums, functions, errors)
}
fn render_snippet_body(
&self,
_fixture: &Fixture,
_e2e_config: &E2eConfig,
_config: &ResolvedCrateConfig,
_type_defs: &[TypeDef],
_enums: &[EnumDef],
) -> Result<String> {
anyhow::bail!(
"e2e code generator `{}` does not support documentation snippets",
self.language_name()
)
}
fn render_snippet_body_with_functions(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
_functions: &[crate::core::ir::FunctionDef],
) -> Result<String> {
self.render_snippet_body(fixture, e2e_config, config, type_defs, enums)
}
fn language_name(&self) -> &'static str;
}
pub fn all_generators() -> Vec<Box<dyn E2eCodegen>> {
vec![
Box::new(rust::RustE2eCodegen),
Box::new(python::PythonE2eCodegen),
Box::new(typescript::TypeScriptCodegen),
Box::new(go::GoCodegen),
Box::new(java::JavaCodegen),
Box::new(kotlin::KotlinE2eCodegen),
Box::new(kotlin_android::KotlinAndroidE2eCodegen),
Box::new(csharp::CSharpCodegen),
Box::new(php::PhpCodegen),
Box::new(php_ext::PhpExtCodegen),
Box::new(ruby::RubyCodegen),
Box::new(elixir::ElixirCodegen),
Box::new(gleam::GleamE2eCodegen),
Box::new(r::RCodegen),
Box::new(wasm::WasmCodegen),
Box::new(c::CCodegen),
Box::new(zig::ZigE2eCodegen),
Box::new(dart::DartE2eCodegen),
Box::new(swift::SwiftE2eCodegen),
Box::new(brew::BrewCodegen),
Box::new(homebrew::HomebrewCodegen),
]
}
pub fn generators_for(languages: &[String]) -> Vec<Box<dyn E2eCodegen>> {
all_generators()
.into_iter()
.filter(|g| languages.iter().any(|l| l == g.language_name()))
.collect()
}
pub(crate) fn resolve_field<'a>(input: &'a serde_json::Value, field_path: &str) -> &'a serde_json::Value {
if field_path == "input" {
if let Some(inner) = input.get("extract_input") {
return inner;
}
return input;
}
let path = field_path.strip_prefix("input.").unwrap_or(field_path);
let mut current = input;
for part in path.split('.') {
current = current.get(part).unwrap_or(&serde_json::Value::Null);
}
current
}
pub(crate) fn select_best_matching_call<'a>(
initial_call: &'a crate::e2e::config::CallConfig,
e2e_config: &'a E2eConfig,
fixture: &Fixture,
) -> &'a crate::e2e::config::CallConfig {
let initial_satisfied = initial_call.args.iter().all(|arg| {
if arg.optional {
return true;
}
let field_value = if arg.arg_type == "mock_url_list" {
resolve_urls_field(&fixture.input, &arg.field)
} else {
resolve_field(&fixture.input, &arg.field)
};
field_value.as_null().is_none()
});
if initial_satisfied {
return initial_call;
}
for alt_call in e2e_config.calls.values() {
let all_satisfied = alt_call.args.iter().all(|arg| {
if arg.optional {
return true;
}
let field_value = if arg.arg_type == "mock_url_list" {
resolve_urls_field(&fixture.input, &arg.field)
} else {
resolve_field(&fixture.input, &arg.field)
};
field_value.as_null().is_none()
});
if all_satisfied {
return alt_call;
}
}
initial_call
}
pub(crate) fn resolve_urls_field<'a>(input: &'a serde_json::Value, field_path: &str) -> &'a serde_json::Value {
let result = resolve_field(input, field_path);
if !result.is_null() {
return result;
}
let aliases = [
("batch_urls", "urls"),
("urls", "batch_urls"),
("batch_urls", "url_list"),
("batch_urls", "urls_list"),
("urls", "url_list"),
("urls", "urls_list"),
];
for (orig, alias) in &aliases {
if field_path.ends_with(orig) {
let aliased_path = field_path.replace(orig, alias);
let result = resolve_field(input, &aliased_path);
if !result.is_null() {
return result;
}
}
}
&serde_json::Value::Null
}
#[derive(Debug, Clone)]
pub struct TestBackendEmission {
pub setup_block: String,
pub arg_expr: String,
pub type_imports: Vec<String>,
pub teardown_block: String,
}
pub fn emit_test_backend(
language: &str,
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&MethodDef],
fixture: &Fixture,
enums: &[crate::core::ir::EnumDef],
) -> TestBackendEmission {
match language {
"rust" => rust::emit_test_backend(trait_bridge, methods, fixture),
"python" => python::emit_test_backend(trait_bridge, methods, fixture),
"typescript" | "wasm" => typescript::emit_test_backend(trait_bridge, methods, fixture, enums),
"node" => typescript::emit_test_backend(trait_bridge, methods, fixture, enums), "go" => go::emit_test_backend(trait_bridge, methods, fixture),
"java" => java::emit_test_backend(trait_bridge, methods, fixture, ""),
"kotlin" => kotlin::emit_test_backend(trait_bridge, methods, fixture),
"kotlin_android" => kotlin_android::emit_test_backend(trait_bridge, methods, fixture),
"csharp" => csharp::emit_test_backend(trait_bridge, methods, fixture),
"php" => php::emit_test_backend(trait_bridge, methods, fixture),
"ruby" => ruby::emit_test_backend(trait_bridge, methods, fixture),
"elixir" => elixir::emit_test_backend(trait_bridge, methods, fixture, "", ""),
"gleam" => gleam::emit_test_backend(trait_bridge, methods, fixture),
"r" => r::emit_test_backend(trait_bridge, methods, fixture),
"c" => c::emit_test_backend(trait_bridge, methods, fixture),
"zig" => zig::emit_test_backend(trait_bridge, methods, fixture),
"dart" => dart::emit_test_backend(trait_bridge, methods, fixture, &[]),
"swift" => swift::emit_test_backend(trait_bridge, methods, fixture, &[]),
"brew" => brew::emit_test_backend(trait_bridge, methods, fixture),
"php_ext" => php_ext::emit_test_backend(trait_bridge, methods, fixture),
"homebrew" => homebrew::emit_test_backend(trait_bridge, methods, fixture),
_ => panic!(
"e2e codegen: no test_backend emitter registered for language `{language}`; \
cannot generate a test_backend stub for this target"
),
}
}
#[cfg(test)]
mod preserved_url_tests {
use super::{preserved_url_list, preserved_url_literal};
#[test]
fn scalar_url_is_preserved_only_when_requested() {
let value = serde_json::json!("http://127.0.0.1/private");
assert_eq!(preserved_url_literal(true, &value), Some("http://127.0.0.1/private"));
assert_eq!(preserved_url_literal(false, &value), None);
}
#[test]
fn url_list_is_preserved_atomically() {
let value = serde_json::json!(["http://host-a.test/", "file:///tmp/example"]);
assert_eq!(
preserved_url_list(true, &value),
Some(vec!["http://host-a.test/", "file:///tmp/example"])
);
assert_eq!(
preserved_url_list(true, &serde_json::json!(["https://host.test", 7])),
None
);
}
}
#[cfg(test)]
mod unimplemented_test_backend_tests {
use super::emit_test_backend;
use crate::core::config::TraitBridgeConfig;
use crate::e2e::fixture::Fixture;
fn registered_bridge() -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: "SampleBackend".into(),
..TraitBridgeConfig::default()
}
}
fn sample_fixture() -> Fixture {
Fixture {
id: "register_sample_backend".into(),
..Fixture::default()
}
}
#[test]
fn languages_without_a_real_emitter_panic_through_the_dispatcher() {
for language in ["gleam", "brew", "php_ext", "homebrew", "kotlin", "not-a-real-language"] {
let bridge = registered_bridge();
let fixture = sample_fixture();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
emit_test_backend(language, &bridge, &[], &fixture, &[])
}));
assert!(
result.is_err(),
"expected `{language}` to panic instead of returning a TestBackendEmission, but it returned a value"
);
}
}
}