use super::*;
use crate::core::config::NewAlefConfig;
fn resolve_config(toml_text: &str) -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(toml_text).expect("valid config");
cfg.resolve().expect("resolve").remove(0)
}
fn minimal_config() -> ResolvedCrateConfig {
resolve_config(
r#"
[workspace]
languages = ["ruby"]
[[crates]]
name = "my-lib"
sources = []
"#,
)
}
fn zero_arg_function(name: &str, return_type: TypeRef) -> FunctionDef {
FunctionDef {
name: name.to_string(),
return_type,
..Default::default()
}
}
fn simple_field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..Default::default()
}
}
fn dto(name: &str, fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
fields,
..Default::default()
}
}
#[test]
fn calls_a_visible_zero_argument_function() {
let api = ApiSurface {
functions: vec![zero_arg_function("ping", TypeRef::Primitive(PrimitiveType::Bool))],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(out.starts_with("# frozen_string_literal: true\n"), "got:\n{out}");
assert!(out.contains("require_relative \"../lib/my_lib\"\n"), "got:\n{out}");
assert!(out.contains("RSpec.describe MyLib do\n"), "got:\n{out}");
assert!(
out.contains(" expect(described_class.ping).to(be(true).or(be(false)))\n"),
"got:\n{out}"
);
}
#[test]
fn matches_the_returned_ruby_type_for_each_return_kind() {
let cases = [
(TypeRef::String, "expect(described_class.probe).to(be_a(String))"),
(
TypeRef::Primitive(PrimitiveType::U64),
"expect(described_class.probe).to(be_a(Integer))",
),
(
TypeRef::Primitive(PrimitiveType::F64),
"expect(described_class.probe).to(be_a(Float))",
),
];
for (return_type, expected) in cases {
let api = ApiSurface {
functions: vec![zero_arg_function("probe", return_type)],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(out.contains(expected), "expected `{expected}`, got:\n{out}");
}
}
#[test]
fn skips_functions_that_take_parameters() {
let api = ApiSurface {
functions: vec![FunctionDef {
params: vec![crate::core::ir::ParamDef {
name: "input".to_string(),
ty: TypeRef::String,
..Default::default()
}],
..zero_arg_function("greet", TypeRef::String)
}],
types: vec![dto("Widget", vec![simple_field("label", TypeRef::String)])],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains("greet"), "got:\n{out}");
assert!(
out.contains("described_class::Widget.new(label: \"alef-scaffold\")"),
"got:\n{out}"
);
}
#[test]
fn skips_async_functions() {
let api = ApiSurface {
functions: vec![FunctionDef {
is_async: true,
..zero_arg_function("fetch", TypeRef::String)
}],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains("fetch"), "got:\n{out}");
assert!(out.contains("described_class::VERSION"), "got:\n{out}");
}
#[test]
fn skips_cfg_gated_functions() {
let api = ApiSurface {
functions: vec![FunctionDef {
cfg: Some("feature = \"extra\"".to_string()),
..zero_arg_function("extra_ping", TypeRef::String)
}],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains("extra_ping"), "got:\n{out}");
}
#[test]
fn skips_functions_whose_generated_body_only_raises() {
let api = ApiSurface {
functions: vec![FunctionDef {
sanitized: true,
error_type: Some("Error".to_string()),
..zero_arg_function("not_delegatable", TypeRef::String)
}],
types: vec![dto("Widget", vec![simple_field("label", TypeRef::String)])],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains("not_delegatable"), "got:\n{out}");
assert!(
out.contains("described_class::Widget.new(label: \"alef-scaffold\")"),
"got:\n{out}"
);
}
#[test]
fn prefers_an_infallible_function_over_a_fallible_one() {
let api = ApiSurface {
functions: vec![
FunctionDef {
error_type: Some("Error".to_string()),
..zero_arg_function("might_fail", TypeRef::String)
},
zero_arg_function("always_works", TypeRef::String),
],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(
out.contains(" expect(described_class.always_works).to(be_a(String))\n"),
"got:\n{out}"
);
assert!(!out.contains("might_fail"), "got:\n{out}");
}
#[test]
fn still_calls_a_fallible_function_when_it_is_the_only_candidate() {
let api = ApiSurface {
functions: vec![FunctionDef {
error_type: Some("Error".to_string()),
..zero_arg_function("might_fail", TypeRef::String)
}],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(
out.contains(" expect(described_class.might_fail).to(be_a(String))\n"),
"got:\n{out}"
);
}
#[test]
fn skips_binding_excluded_functions() {
let api = ApiSurface {
functions: vec![
FunctionDef {
binding_excluded: true,
..zero_arg_function("hidden", TypeRef::String)
},
zero_arg_function("visible", TypeRef::String),
],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(out.contains("described_class.visible"), "got:\n{out}");
assert!(!out.contains("hidden"), "got:\n{out}");
}
#[test]
fn skips_functions_excluded_via_ruby_config() {
let config = resolve_config(
r#"
[workspace]
languages = ["ruby"]
[[crates]]
name = "my-lib"
sources = []
[crates.ruby]
exclude_functions = ["ping"]
"#,
);
let api = ApiSurface {
functions: vec![zero_arg_function("ping", TypeRef::String)],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &config, "my_lib");
assert!(
!out.contains("ping"),
"excluded function must not be referenced, got:\n{out}"
);
assert!(out.contains("described_class::VERSION"), "got:\n{out}");
}
#[test]
fn constructs_a_simple_dto_and_asserts_every_field() {
let api = ApiSurface {
types: vec![dto(
"Widget",
vec![
simple_field("label", TypeRef::String),
simple_field("count", TypeRef::Primitive(PrimitiveType::U32)),
],
)],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(
out.contains(" instance = described_class::Widget.new(label: \"alef-scaffold\", count: 1)\n"),
"got:\n{out}"
);
assert!(
out.contains(" expect([instance.label, instance.count]).to(eq([\"alef-scaffold\", 1]))\n"),
"got:\n{out}"
);
}
#[test]
fn constructs_an_all_string_dto_without_a_bracketed_word_array() {
let api = ApiSurface {
types: vec![dto(
"Widget",
vec![
simple_field("label", TypeRef::String),
simple_field("note", TypeRef::String),
],
)],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(
out.contains(" expect([instance.label, instance.note]).to(eq(%w[alef-scaffold alef-scaffold]))\n"),
"got:\n{out}"
);
assert!(
!out.contains("[\"alef-scaffold\", \"alef-scaffold\"]"),
"must not emit a bracketed all-String literal array: got:\n{out}"
);
}
#[test]
fn asserts_a_single_field_dto_without_an_array() {
let api = ApiSurface {
types: vec![dto("Widget", vec![simple_field("label", TypeRef::String)])],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(
out.contains(" expect(instance.label).to(eq(\"alef-scaffold\"))\n"),
"got:\n{out}"
);
}
#[test]
fn falls_back_to_a_constant_reference_for_a_dto_with_a_named_field() {
let api = ApiSurface {
types: vec![dto(
"Widget",
vec![
simple_field("label", TypeRef::String),
simple_field("nested", TypeRef::Named("Other".to_string())),
],
)],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains(".new("), "got:\n{out}");
assert!(
out.contains(" expect(described_class.const_get(:Widget)).to(be_a(Module))\n"),
"got:\n{out}"
);
}
#[test]
fn falls_back_to_a_constant_reference_for_a_dto_with_an_optional_field() {
let api = ApiSurface {
types: vec![dto(
"Widget",
vec![FieldDef {
optional: true,
..simple_field("label", TypeRef::String)
}],
)],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains(".new("), "got:\n{out}");
assert!(out.contains("const_get(:Widget)"), "got:\n{out}");
}
#[test]
fn skips_types_excluded_by_config_or_binding_exclusion() {
let config = resolve_config(
r#"
[workspace]
languages = ["ruby"]
[[crates]]
name = "my-lib"
sources = []
[crates.ruby]
exclude_types = ["Excluded"]
"#,
);
let api = ApiSurface {
types: vec![
dto("Excluded", vec![simple_field("label", TypeRef::String)]),
TypeDef {
binding_excluded: true,
..dto("Hidden", vec![simple_field("label", TypeRef::String)])
},
dto("Visible", vec![simple_field("label", TypeRef::String)]),
],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &config, "my_lib");
assert!(!out.contains("Excluded"), "got:\n{out}");
assert!(!out.contains("Hidden"), "got:\n{out}");
assert!(out.contains("described_class::Visible.new("), "got:\n{out}");
}
#[test]
fn skips_update_and_builder_types() {
let api = ApiSurface {
types: vec![
dto("WidgetUpdate", vec![simple_field("label", TypeRef::String)]),
dto("WidgetBuilder", vec![simple_field("label", TypeRef::String)]),
],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains("WidgetUpdate"), "got:\n{out}");
assert!(!out.contains("WidgetBuilder"), "got:\n{out}");
assert!(out.contains("described_class::VERSION"), "got:\n{out}");
}
#[test]
fn never_names_an_enum_because_magnus_registers_none_as_constants() {
let api = ApiSurface {
enums: vec![crate::core::ir::EnumDef {
name: "Colour".to_string(),
..Default::default()
}],
..Default::default()
};
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(!out.contains("Colour"), "got:\n{out}");
assert!(out.contains("described_class::VERSION"), "got:\n{out}");
}
#[test]
fn falls_back_to_the_version_assertion_when_the_api_surface_is_empty() {
let out = scaffold_ruby_spec(&ApiSurface::default(), &minimal_config(), "my_lib");
assert!(
out.contains(" expect(described_class::VERSION).to match(/\\A\\d+\\.\\d+\\.\\d+/)\n"),
"got:\n{out}"
);
}
#[test]
fn no_tier_emits_a_vacuous_or_unlinked_example() {
let surfaces = [
ApiSurface {
functions: vec![zero_arg_function("ping", TypeRef::String)],
..Default::default()
},
ApiSurface {
types: vec![dto("Widget", vec![simple_field("label", TypeRef::String)])],
..Default::default()
},
ApiSurface {
types: vec![dto(
"Widget",
vec![simple_field("nested", TypeRef::Named("Other".to_string()))],
)],
..Default::default()
},
ApiSurface::default(),
];
for api in surfaces {
let out = scaffold_ruby_spec(&api, &minimal_config(), "my_lib");
assert!(
out.contains("require_relative \"../lib/my_lib\""),
"every tier must load the gem, got:\n{out}"
);
assert_eq!(out.matches(" it \"").count(), 1, "exactly one example, got:\n{out}");
for tautology in ["expect(1)", "eq(1 + 1)", "to be_truthy", "to be_falsey"] {
assert!(!out.contains(tautology), "vacuous assertion `{tautology}` in:\n{out}");
}
assert!(
out.contains("described_class"),
"the example must assert against the generated module, got:\n{out}"
);
}
}
#[test]
fn seed_content_carries_no_alef_marker() {
let out = scaffold_ruby_spec(&ApiSurface::default(), &minimal_config(), "my_lib");
assert!(
!crate::core::hash::content_has_alef_marker(&out),
"seed must stay unmarked so it is never reclaimed by an overwrite run, got:\n{out}"
);
}
#[test]
fn seed_is_emitted_create_only_at_the_rspec_default_path() {
let config = minimal_config();
let api = ApiSurface {
version: "1.2.3".to_string(),
..Default::default()
};
let files = scaffold_ruby(&api, &config).expect("scaffold");
let spec = files
.iter()
.find(|f| f.path.to_string_lossy().contains("/spec/"))
.expect("a spec seed must be emitted");
assert_eq!(spec.path.to_string_lossy(), "packages/ruby/spec/my_lib_spec.rb");
assert!(!spec.generated_header, "the seed must stay create-only");
}
#[test]
fn every_seed_tier_keeps_its_uncomment_pass_marker() {
let config = minimal_config();
let tiers = [
(
"call",
ApiSurface {
functions: vec![zero_arg_function("ping", TypeRef::Primitive(PrimitiveType::Bool))],
..Default::default()
},
),
(
"construct",
ApiSurface {
types: vec![dto("Widget", vec![simple_field("label", TypeRef::String)])],
..Default::default()
},
),
(
"constant",
ApiSurface {
types: vec![dto(
"Widget",
vec![
simple_field("label", TypeRef::String),
simple_field("nested", TypeRef::Named("Other".to_string())),
],
)],
..Default::default()
},
),
("version", ApiSurface::default()),
];
for (tier, api) in tiers {
let out = scaffold_ruby_spec(&api, &config, "my_lib");
assert!(
out.contains("replace it with a real suite. ~keep") || out.contains("break on the next release. ~keep"),
"the {tier} tier lost its uncomment-pass marker, got:\n{out}"
);
}
}
#[test]
fn gemspec_files_filter_never_reintroduces_the_select_by_regexp_anti_pattern() {
let files = scaffold_ruby(&ApiSurface::default(), &minimal_config()).expect("scaffold");
let gemspec = files
.iter()
.find(|f| f.path.to_string_lossy().ends_with(".gemspec"))
.expect("a gemspec must be emitted");
let select_by_regexp_shape = regex_lite_contains_match_predicate(&gemspec.content);
assert!(
!select_by_regexp_shape,
"gemspec re-introduces the Style/SelectByRegexp anti-pattern \
(`.select`/`.reject` with a `.match?(%r{{...}})` predicate), got:\n{}",
gemspec.content
);
assert!(
gemspec.content.contains(".grep_v(%r{"),
"gemspec must filter spec.files via grep_v, RuboCop's own autocorrect target, got:\n{}",
gemspec.content
);
}
fn regex_lite_contains_match_predicate(content: &str) -> bool {
for method in [".select {", ".reject {", ".select{", ".reject{"] {
let mut search_from = 0;
while let Some(offset) = content[search_from..].find(method) {
let start = search_from + offset;
let block_end = content[start..].find('}').map_or(content.len(), |end| start + end + 1);
if content[start..block_end].contains(".match?(%r{") {
return true;
}
search_from = start + method.len();
}
}
false
}
#[test]
fn rubocop_config_excludes_the_alef_owned_gemspec_and_rakefile() {
let files = scaffold_ruby(&ApiSurface::default(), &minimal_config()).expect("scaffold");
let rubocop_yml = files
.iter()
.find(|f| f.path.to_string_lossy().ends_with(".rubocop.yml"))
.expect("a .rubocop.yml must be emitted");
assert!(
rubocop_yml.content.contains("\"*.gemspec\""),
"AllCops.Exclude must cover the alef-owned gemspec, got:\n{}",
rubocop_yml.content
);
assert!(
rubocop_yml.content.contains("\"Rakefile\""),
"AllCops.Exclude must cover the alef-owned Rakefile, got:\n{}",
rubocop_yml.content
);
}