tests.rs
24.3 kB · rust · 642 lines
1use crate::model::*;2use crate::parse::{self, Files};3use crate::report;4use crate::resolve::{self, Built};5use crate::{cli, js, py};6use std::path::{Path, PathBuf};78fn build(lib: &str) -> Built {9 let files: Files = [("lib.rs".to_string(), lib.to_string())]10 .into_iter()11 .collect();12 resolve::build(&files, "0.0.0").expect("the source builds")13}1415fn manifest(lib: &str) -> Manifest {16 build(lib).manifest17}1819fn function<'a>(m: &'a Manifest, path: &str) -> &'a Function {20 let paths: Vec<&str> = m.functions.iter().map(|f| f.path.as_str()).collect();21 m.functions22 .iter()23 .find(|f| f.path == path)24 .unwrap_or_else(|| panic!("no {path} in {paths:?}"))25}2627fn reason(m: &Manifest, path: &str) -> String {28 match &function(m, path).cross {29 Cross::Skip { reason } => reason.clone(),30 other => panic!("{path} is {other:?}"),31 }32}3334fn kind<'a>(m: &'a Manifest, path: &str) -> &'a TypeCross {35 let paths: Vec<&str> = m.types.iter().map(|t| t.path.as_str()).collect();36 &m.types37 .iter()38 .find(|t| t.path == path)39 .unwrap_or_else(|| panic!("no {path} in {paths:?}"))40 .cross41}4243fn scratch(name: &str, lib: &str, units: &str) -> PathBuf {44 let root = std::env::temp_dir().join(format!("bridge-{name}-{}", std::process::id()));45 let _ = std::fs::remove_dir_all(&root);46 std::fs::create_dir_all(root.join("bridge")).expect("a scratch root");47 std::fs::write(root.join("bridge/units.txt"), units).expect("units.txt");48 let m = manifest(lib);49 cli::write(&m, &root).expect("the cli writes");50 py::write(&m, &root).expect("the python bridge writes");51 js::write(&m, &root).expect("the js bridge writes");52 root53}5455fn read(root: &Path, file: &str) -> String {56 std::fs::read_to_string(root.join(file)).unwrap_or_else(|_| panic!("no {file}"))57}5859const ERROR: &str = "pub mod core { pub mod error { pub enum Error { Value(String) } pub type Result<T> = std::result::Result<T, Error>; } pub use error::{Error, Result}; }";6061#[test]62fn scalars_and_strings_cross() {63 let m = manifest(64 "pub fn f(a: bool, b: u8, c: f64, d: String, e: &str, g: char, h: f32, i: i8) -> u64 { 0 }",65 );66 let f = function(&m, "f");67 assert_eq!(f.cross, Cross::Ok);68 assert_eq!(f.params[3].ty, Ty::String);69 assert_eq!(f.params[4].ty, Ty::Str);70 assert_eq!(71 f.params[5].ty,72 Ty::Scalar {73 name: "char".into()74 }75 );76 assert_eq!(f.ret, Ty::Scalar { name: "u64".into() });77}7879#[test]80fn wide_integers_and_code_cross() {81 let m = manifest(82 "pub struct Code(pub(crate) u128); pub fn f(a: u128, b: i128, c: Code) -> Code { c }",83 );84 let f = function(&m, "f");85 assert_eq!(f.cross, Cross::Ok);86 assert_eq!(f.params[0].ty, Ty::U128);87 assert_eq!(f.params[1].ty, Ty::I128);88 assert_eq!(f.ret, Ty::Code);89}9091#[test]92fn containers_and_results_cross() {93 let src = format!("{ERROR} use core::Result; pub fn f(a: Vec<u8>, b: &[u64], c: Option<usize>, d: (u8, u8), e: [u8; 4]) -> Result<Vec<u8>> {{ Ok(a) }}");94 let m = manifest(&src);95 let f = function(&m, "f");96 assert_eq!(f.cross, Cross::Ok);97 let u8 = || Box::new(Ty::Scalar { name: "u8".into() });98 assert_eq!(f.params[0].ty, Ty::Vec { item: u8() });99 assert_eq!(100 f.params[1].ty,101 Ty::Slice {102 mutable: false,103 item: Box::new(Ty::Scalar { name: "u64".into() })104 }105 );106 assert_eq!(107 f.params[2].ty,108 Ty::Option {109 item: Box::new(Ty::Scalar {110 name: "usize".into()111 })112 }113 );114 assert_eq!(115 f.params[3].ty,116 Ty::Tuple {117 items: vec![*u8(), *u8()]118 }119 );120 assert_eq!(f.params[4].ty, Ty::Array { item: u8(), len: 4 });121 assert_eq!(122 f.ret,123 Ty::Result {124 item: Box::new(Ty::Vec { item: u8() })125 }126 );127}128129#[test]130fn json_maps_and_sets_cross() {131 let m = manifest("pub use serde_json::Value as Json; pub fn f(v: &Json, m: std::collections::HashMap<u8, Vec<u8>>) -> std::collections::BTreeSet<u8> { todo!() }");132 let f = function(&m, "f");133 assert_eq!(f.cross, Cross::Ok);134 assert_eq!(135 f.params[0].ty,136 Ty::Ref {137 mutable: false,138 lifetime: None,139 item: Box::new(Ty::Json)140 }141 );142 assert!(matches!(f.params[1].ty, Ty::Map { .. }));143 assert!(matches!(f.ret, Ty::Set { .. }));144}145146#[test]147fn hand_methods_export_as_free_functions_self_first() {148 let m = manifest("pub mod core { pub mod tensor { pub struct Tensor { pub shape: Vec<usize> } impl Tensor { pub fn new(shape: Vec<usize>) -> Tensor { Tensor { shape } } pub fn size(&self) -> usize { 0 } } } pub use tensor::Tensor; }");149 assert_eq!(150 kind(&m, "core::Tensor"),151 &TypeCross::Hand {152 name: "Tensor".into()153 }154 );155 let new = function(&m, "core::tensor::new");156 assert_eq!(157 (new.owner.as_deref(), new.self_kind, new.module.as_str()),158 (Some("core::Tensor"), None, "core::tensor")159 );160 let size = function(&m, "core::tensor::size");161 assert_eq!(162 (size.self_kind, &size.cross),163 (Some(SelfKind::Ref), &Cross::Ok)164 );165}166167#[test]168fn a_hand_type_in_a_private_module_exports_at_the_nearest_public_module() {169 let m = manifest("pub mod six { mod models { pub struct Cell6d { pub start: u8 } impl Cell6d { pub fn width(&self) -> usize { 0 } } } pub use models::Cell6d; }");170 let f = function(&m, "six::width");171 assert_eq!((f.module.as_str(), &f.cross), ("six", &Cross::Ok));172}173174#[test]175fn rng_crosses_by_hand_and_is_passed_mutably() {176 let m = manifest("pub struct Rng { s: u64 } pub fn draw(rng: &mut Rng) -> u64 { 0 }");177 assert_eq!(kind(&m, "Rng"), &TypeCross::Hand { name: "Rng".into() });178 let f = function(&m, "draw");179 assert_eq!(f.cross, Cross::Ok);180 assert_eq!(181 f.params[0].ty,182 Ty::Ref {183 mutable: true,184 lifetime: None,185 item: Box::new(Ty::Hand {186 name: "Rng".into(),187 dim: None188 })189 }190 );191}192193#[test]194fn a_serde_struct_without_self_methods_is_plain_and_keeps_its_constructors() {195 let m = manifest("pub mod gen { #[derive(Serialize, Deserialize)] pub struct Tile { pub side: usize } impl Tile { pub fn new(side: usize) -> Tile { Tile { side } } } }");196 assert_eq!(kind(&m, "gen::Tile"), &TypeCross::Plain);197 let f = function(&m, "gen::Tile::new");198 assert_eq!(199 (f.owner.as_deref(), f.module.as_str(), &f.cross),200 (Some("gen::Tile"), "gen", &Cross::Ok)201 );202}203204#[test]205fn a_serde_skipped_field_makes_a_class() {206 let m = manifest("pub mod gen { #[derive(Serialize, Deserialize)] pub struct File { pub width: usize, #[serde(default, skip_serializing_if = \"Vec::is_empty\")] pub tags: Vec<u8>, #[serde(skip)] pub png: Vec<u8> } #[derive(Serialize, Deserialize)] pub struct Tile { #[serde(default, skip_serializing_if = \"Option::is_none\")] pub side: Option<usize> } }");207 assert_eq!(kind(&m, "gen::File"), &TypeCross::Class);208 assert_eq!(kind(&m, "gen::Tile"), &TypeCross::Plain);209 let file = m.types.iter().find(|t| t.path == "gen::File").expect("File is listed");210 let skips: Vec<bool> = file.fields.iter().map(|f| f.serde_skip).collect();211 assert_eq!(skips, [false, false, true]);212}213214#[test]215fn a_struct_with_a_self_method_is_a_class() {216 let m = manifest("pub mod life { #[derive(Serialize, Deserialize)] pub struct Life { pub time: usize } impl Life { pub fn step(&mut self) {} } }");217 assert_eq!(kind(&m, "life::Life"), &TypeCross::Class);218 let f = function(&m, "life::Life::step");219 assert_eq!((f.self_kind, &f.cross), (Some(SelfKind::Mut), &Cross::Ok));220}221222#[test]223fn a_fieldless_serde_enum_is_a_string_even_with_methods() {224 let m = manifest("#[derive(Serialize, Deserialize)] pub enum Dtype { U8, U16 } impl Dtype { pub fn max(self) -> i64 { 0 } }");225 assert_eq!(226 kind(&m, "Dtype"),227 &TypeCross::Enum {228 named: false,229 words: vec![]230 }231 );232 let f = function(&m, "Dtype::max");233 assert_eq!((f.self_kind, &f.cross), (Some(SelfKind::Value), &Cross::Ok));234}235236#[test]237fn a_named_enum_records_its_words_all_and_name() {238 let m = manifest("pub mod life { named_enum! { #[derive(Serialize, Deserialize)] pub enum Fate { Dead => \"dead\", Alive => \"alive\", } } }");239 assert_eq!(240 kind(&m, "life::Fate"),241 &TypeCross::Enum {242 named: true,243 words: vec!["dead".into(), "alive".into()]244 }245 );246 let all = function(&m, "life::Fate::all");247 assert_eq!(248 (all.source, all.constant, &all.cross),249 (Source::NamedEnum, true, &Cross::Ok)250 );251 assert_eq!(252 all.ret,253 Ty::Array {254 item: Box::new(Ty::Enum {255 path: "life::Fate".into()256 }),257 len: 2258 }259 );260 let name = function(&m, "life::Fate::name");261 assert_eq!(262 (name.source, &name.cross),263 (264 Source::NamedEnum,265 &Cross::Skip {266 reason: "&'static return".into()267 }268 )269 );270}271272#[test]273fn const_generic_functions_dispatch_on_two_and_three() {274 let m = manifest("pub mod cell { pub struct CellNd<const N: usize> { pub n: u8 } pub type Cell2d = CellNd<2>; pub fn fills<const N: usize>(cell: &CellNd<N>) -> usize { 0 } impl CellNd<2> { pub fn rotate(self) -> Cell2d { self } } }");275 let fills = function(&m, "cell::fills");276 assert_eq!(277 (fills.dims.as_slice(), &fills.cross),278 ([2, 3].as_slice(), &Cross::Ok)279 );280 let rotate = function(&m, "cell::rotate");281 assert_eq!(rotate.dims, vec![2]);282 assert_eq!(283 rotate.ret,284 Ty::Hand {285 name: "CellNd".into(),286 dim: Some(2)287 }288 );289 let alias = m290 .types291 .iter()292 .find(|t| t.path == "cell::Cell2d")293 .expect("the alias");294 assert_eq!(295 (alias.kind, &alias.alias),296 (297 TypeKind::Alias,298 &Some(Ty::Hand {299 name: "CellNd".into(),300 dim: Some(2)301 })302 )303 );304}305306#[test]307fn a_trait_generic_is_skipped() {308 let m = manifest("pub fn uniform<T: PartialEq>(items: &[T]) -> bool { true }");309 assert_eq!(reason(&m, "uniform"), "generic over T");310}311312#[test]313fn a_closure_parameter_is_skipped() {314 let m = manifest("pub fn render<F>(rule: F) -> u8 where F: Fn(&[u8]) -> bool { 0 }");315 assert_eq!(reason(&m, "render"), "closure parameter F");316}317318#[test]319fn a_static_return_is_skipped() {320 let m = manifest("pub fn genus() -> &'static str { \"iso\" }");321 assert_eq!(reason(&m, "genus"), "&'static return");322}323324#[test]325fn a_lifetime_bearing_return_is_skipped() {326 let m = manifest("pub fn first<'a>(items: &'a [u8]) -> &'a u8 { &items[0] }");327 assert_eq!(reason(&m, "first"), "lifetime-bearing return");328}329330#[test]331fn an_elided_borrow_return_is_copied_out() {332 let m = manifest("pub struct Cell { pub types: Vec<u8> } impl Cell { pub fn shape(&self) -> &[u8] { &self.types } }");333 assert_eq!(function(&m, "shape").cross, Cross::Ok);334}335336#[test]337fn a_mutable_borrow_return_is_skipped() {338 let m = manifest("pub struct Tensor { pub data: Vec<u8> } impl Tensor { pub fn bytes_mut(&mut self) -> &mut [u8] { &mut self.data } }");339 assert_eq!(reason(&m, "bytes_mut"), "mutable borrow return");340}341342#[test]343fn a_serde_helper_in_a_private_module_is_private() {344 let m = manifest("mod counts { pub fn serialize<S: serde::Serializer>(counts: &u8, serializer: S) -> Result<S::Ok, S::Error> { todo!() } }");345 assert_eq!(function(&m, "counts::serialize").cross, Cross::Private);346}347348#[test]349fn an_error_constructor_is_skipped() {350 let src = format!("{ERROR} pub mod other {{}} impl core::error::Error {{}} pub mod core2 {{}}");351 let m = manifest(&format!("pub mod core {{ pub mod error {{ pub enum Error {{ Value(String) }} pub type Result<T> = std::result::Result<T, Error>; pub fn value_error<T>(message: impl Into<String>) -> Result<T> {{ todo!() }} }} }} {}", &src[src.len()..]));352 assert_eq!(reason(&m, "core::error::value_error"), "error constructor");353}354355#[test]356fn an_impl_trait_argument_is_skipped() {357 let m = manifest("pub fn new(birth: impl Into<u8>) -> u8 { 0 }");358 assert!(reason(&m, "new").starts_with("birth: impl Trait argument"));359}360361#[test]362fn an_iterator_return_is_skipped() {363 let m = manifest("pub fn each() -> impl Iterator<Item = u8> { std::iter::empty() }");364 assert_eq!(reason(&m, "each"), "returns iterator return");365}366367#[test]368fn a_private_type_in_a_signature_is_skipped() {369 let m = manifest("type Rotation = fn() -> u8; struct Point; pub fn create(rotation: Rotation) {} pub fn corners() -> Point { Point }");370 assert_eq!(reason(&m, "create"), "rotation: unknown type Rotation");371 assert_eq!(reason(&m, "corners"), "returns unknown type Point");372}373374#[test]375fn a_type_without_serde_or_methods_is_uncrossable() {376 let m = manifest("pub mod two { pub enum Shape { Square } pub fn png(shape: Shape) {} }");377 assert!(matches!(378 kind(&m, "two::Shape"),379 TypeCross::Uncrossable { .. }380 ));381 assert_eq!(reason(&m, "two::png"), "shape: uncrossable type two::Shape");382}383384#[test]385fn a_class_from_another_unit_is_skipped() {386 let m = manifest("pub mod core { pub struct Colorizer { pub n: u8 } impl Colorizer { pub fn color(&self) -> u8 { 0 } } } pub mod math { pub fn render(c: &crate::core::Colorizer) -> u8 { 0 } }");387 assert_eq!(388 reason(&m, "math::render"),389 "class core::Colorizer lives in unit core"390 );391}392393#[test]394fn mutating_plain_data_in_place_is_skipped() {395 let m = manifest("pub fn push(row: &mut String) {} pub fn fft(re: &mut [f64]) {}");396 assert_eq!(reason(&m, "push"), "row: mutates plain data in place");397 assert_eq!(reason(&m, "fft"), "re: mutable slice argument");398}399400#[test]401fn the_shortest_public_path_wins_and_ties_go_to_the_definition() {402 let m = manifest("pub mod a { pub mod b { pub fn f() {} } pub use b::f; } pub mod cell { pub fn edges() {} } pub mod two { pub use crate::cell::edges; }");403 assert_eq!(function(&m, "a::f").module, "a");404 assert_eq!(function(&m, "cell::edges").cross, Cross::Ok);405 assert!(m.functions.iter().all(|f| f.path != "two::edges"));406}407408#[test]409fn re_exports_resolve_groups_renames_and_globs() {410 let m = manifest("mod one { pub fn x() {} } mod two { pub fn y() {} } mod three { pub fn z() {} } pub mod g { pub use super::one::*; } pub mod h { pub use crate::two::y as ey; pub use crate::three::{z}; }");411 for path in ["g::x", "h::ey", "h::z"] {412 assert_eq!(function(&m, path).cross, Cross::Ok, "{path}");413 }414}415416#[test]417fn an_item_reachable_only_through_a_private_module_is_private() {418 let m = manifest(419 "mod hidden { pub fn f() {} pub struct S; } pub mod shown { mod deep { pub fn g() {} } }",420 );421 assert_eq!(function(&m, "hidden::f").cross, Cross::Private);422 assert_eq!(function(&m, "shown::deep::g").cross, Cross::Private);423 assert!(m.types.is_empty());424}425426#[test]427fn a_name_collision_fails_the_run() {428 let built = build("pub mod core { pub struct Tensor { pub n: u8 } impl Tensor { pub fn new() -> Tensor { todo!() } } pub struct Cell { pub n: u8 } impl Cell { pub fn new() -> Cell { todo!() } } }");429 assert_eq!(430 built.collisions,431 vec!["collision: core::new at lib.rs:1 and lib.rs:1"]432 );433 let built =434 build("pub mod census { pub fn census() {} pub fn extra() {} } pub use census::census;");435 assert_eq!(436 built.collisions,437 vec!["collision: module census and fn census at lib.rs:1"]438 );439}440441#[test]442fn skip_txt_must_name_every_skipped_function_and_nothing_else() {443 let m = manifest("pub fn genus() -> &'static str { \"iso\" } pub fn fine() {}");444 assert_eq!(report::skip_lines(&m), vec!["genus &'static return"]);445 assert_eq!(446 report::check_skip(&m, ""),447 vec!["skip.txt is missing genus"]448 );449 assert_eq!(450 report::check_skip(&m, "fine a stale line\n"),451 vec!["skip.txt is missing genus", "skip.txt names nothing: fine"]452 );453 assert!(report::check_skip(&m, "genus a decision\n").is_empty());454}455456#[test]457fn consts_are_listed_with_their_type() {458 let m = manifest("pub mod font { pub const FPS: usize = 25; pub type Pen = (char, &'static [&'static str]); pub const UPPERS: &[Pen] = &[]; }");459 let fps = m460 .consts461 .iter()462 .find(|c| c.path == "font::FPS")463 .expect("fps");464 assert_eq!(465 (&fps.ty, &fps.cross),466 (467 &Ty::Scalar {468 name: "usize".into()469 },470 &Cross::Ok471 )472 );473 let uppers = m474 .consts475 .iter()476 .find(|c| c.path == "font::UPPERS")477 .expect("uppers");478 assert_eq!(479 uppers.cross,480 Cross::Skip {481 reason: "&'static const".into()482 }483 );484}485486#[test]487fn a_public_trait_adds_its_methods_to_every_implementing_type() {488 let m = manifest("pub mod name { pub trait Named: Sized { const KIND: &'static str; #[doc = \" Folds.\"] fn checked(self) -> Result<Self, u8>; fn to_json(&self) -> String { String::new() } fn from_json(text: &str) -> Result<Self, u8> { todo!() } } pub mod word { #[derive(Serialize, Deserialize)] pub struct Word { pub n: u8 } impl super::Named for Word { const KIND: &'static str = \"word\"; fn checked(self) -> Result<Self, u8> { Ok(self) } } } }");489 assert_eq!(kind(&m, "name::word::Word"), &TypeCross::Class);490 let checked = function(&m, "name::word::Word::checked");491 assert_eq!(492 (checked.self_kind, checked.source, checked.via.as_deref()),493 (Some(SelfKind::Value), Source::Trait, Some("name::Named"))494 );495 assert_eq!(checked.docs, vec!["Folds."]);496 assert!(checked.defined_at.starts_with("lib.rs:"));497 let from = function(&m, "name::word::Word::from_json");498 assert_eq!(499 (from.self_kind, from.owner.as_deref()),500 (None, Some("name::word::Word"))501 );502 assert!(503 matches!(from.cross, Cross::Skip { .. }),504 "Result<Self, u8> is not the crate Result"505 );506 assert_eq!(function(&m, "name::word::Word::to_json").ret, Ty::String);507 assert!(m.functions.iter().all(|f| f.name != "KIND"));508}509510#[test]511fn a_public_class_field_gets_a_setter_unless_it_holds_a_borrow() {512 let root = scratch("setter", "pub mod life { #[derive(Clone, Copy, Serialize, Deserialize)] pub enum Boundary { Constant, Wrap } #[derive(Clone, Serialize, Deserialize)] pub struct Config { pub boundary: Boundary, pub padding: usize, pub name: &'static str } impl Config { pub fn budget(&self) -> usize { 0 } } }", "life\n");513 let rust = read(&root, "pkgs/mrlypy/src/gen.rs");514 assert!(rust.contains("pub fn set_boundary(&mut self, value: PySerde<mrlyrs::life::Boundary>) -> PyResult<()> {"));515 assert!(rust.contains("self.0.padding = value;"));516 assert!(!rust.contains("set_name"));517 assert!(read(&root, "pkgs/mrlypy/python/mrlypy/life/__init__.pyi").contains("@boundary.setter"));518 let wasm = read(&root, "pkgs/mrlyjs/units/life/src/lib.rs");519 assert!(wasm.contains("pub fn set_boundary(&mut self, value: JsValue) -> Result<(), JsValue> {"));520 assert!(wasm.contains("self.inner.padding = value;"));521 assert!(!wasm.contains("set_name"));522 let dts = read(&root, "pkgs/mrlyjs/life.d.ts");523 assert!(dts.contains("set boundary(value: Boundary);"));524 assert!(dts.contains("readonly name: string;"));525 std::fs::remove_dir_all(root).ok();526}527528#[test]529fn default_crosses_as_a_static_on_the_type_and_an_alias_fixes_n() {530 let lib = "pub mod gen { #[derive(Clone, Default, Serialize, Deserialize)] pub struct Paint { pub n: u8 } #[derive(Clone, Serialize, Deserialize)] pub struct Field { pub n: u8 } impl Field { pub fn size(&self) -> u8 { 0 } } impl Default for Field { fn default() -> Field { Field { n: 1 } } } #[derive(Clone, Serialize, Deserialize)] pub struct ConfigNd<const N: usize> { pub n: u8 } impl<const N: usize> Default for ConfigNd<N> { fn default() -> Self { ConfigNd { n: 0 } } } pub type Config2d = ConfigNd<2>; } pub mod core { #[derive(Clone, Default, Serialize, Deserialize)] pub struct Rng { s: u64 } }";531 let m = manifest(lib);532 let paint = function(&m, "gen::Paint::default");533 assert_eq!(534 (paint.source, paint.owner.as_deref(), paint.self_kind, &paint.cross),535 (Source::Default, Some("gen::Paint"), None, &Cross::Ok)536 );537 assert_eq!(paint.ret, Ty::Plain { path: "gen::Paint".into() });538 assert_eq!(539 function(&m, "gen::Field::default").ret,540 Ty::Class { path: "gen::Field".into(), dim: None }541 );542 assert_eq!(543 function(&m, "gen::Config2d::default").ret,544 Ty::Plain { path: "gen::ConfigNd".into() }545 );546 let defaults: Vec<&str> = m547 .functions548 .iter()549 .filter(|f| f.source == Source::Default)550 .map(|f| f.path.as_str())551 .collect();552 assert_eq!(defaults, ["gen::Config2d::default", "gen::Field::default", "gen::Paint::default"]);553 let root = scratch("default", lib, "gen\n");554 assert!(read(&root, "pkgs/mrlypy/python/mrlypy/gen/__init__.pyi").contains("class Config2d:\n @staticmethod\n def default() -> dict[str, Any]:"));555 assert!(read(&root, "pkgs/mrlyjs/gen.d.ts").contains("static default(): Field;"));556 assert!(read(&root, "pkgs/mrlyrs/src/bin/mrly.rs").contains("(\"gen.Config2d.default\", \"() -> gen.ConfigNd\""));557 std::fs::remove_dir_all(root).ok();558}559560#[test]561fn the_hand_rng_declares_choice_and_shuffle_in_both_bridges() {562 let root = scratch("rng", "pub mod core { pub struct Rng { s: u64 } impl Rng { pub fn new(seed: u64) -> Rng { Rng { s: seed } } pub fn choice<'a, T>(&mut self, items: &'a [T]) -> &'a T { &items[0] } pub fn shuffle<T>(&mut self, seq: &mut [T]) {} } }", "core\n");563 let stub = read(&root, "pkgs/mrlypy/python/mrlypy/core/__init__.pyi");564 assert!(stub.contains(" def choice(self, seq: Any) -> Any:"));565 assert!(stub.contains(" def shuffle(self, seq: list[Any]) -> None:"));566 let dts = read(&root, "pkgs/mrlyjs/core.d.ts");567 assert!(dts.contains(" choice<T>(items: ArrayLike<T>): T;"));568 assert!(dts.contains(" shuffle<T>(items: T[]): void;"));569 std::fs::remove_dir_all(root).ok();570}571572fn crate_files() -> Files {573 parse::load(Path::new(concat!(574 env!("CARGO_MANIFEST_DIR"),575 "/../pkgs/mrlyrs/src"576 )))577 .expect("the crate loads")578}579580#[test]581fn the_crate_parses_the_same_twice() {582 let files = crate_files();583 let a = resolve::build(&files, "0").expect("first").manifest;584 let b = resolve::build(&files, "0").expect("second").manifest;585 assert_eq!(a, b);586 assert_eq!(587 serde_json::to_string(&a).unwrap(),588 serde_json::to_string(&b).unwrap()589 );590}591592#[test]593fn function_entries_match_the_pub_fn_grep() {594 let files = crate_files();595 let built = resolve::build(&files, "0").expect("the crate builds");596 let grep: usize = files597 .values()598 .map(|t| t.lines().filter(|l| parse::is_pub_fn_line(l)).count())599 .sum();600 let fns = &built.manifest.functions;601 let written = fns602 .iter()603 .filter(|f| f.source == Source::Written && !f.constant)604 .count();605 let constant = fns606 .iter()607 .filter(|f| f.source == Source::Written && f.constant)608 .count();609 let generated = fns.iter().filter(|f| f.source == Source::NamedEnum).count();610 let traits = fns.iter().filter(|f| f.source == Source::Trait).count();611 let defaults = fns.iter().filter(|f| f.source == Source::Default).count();612 let text = &files["math/name/mod.rs"];613 let start = text.find("pub trait Named").expect("the trait");614 let end = start + text[start..].find("\n}\n").expect("its end");615 let trait_fns = text[start..end]616 .lines()617 .filter(|l| l.starts_with(" fn "))618 .count();619 let impls: usize = files620 .values()621 .map(|t| t.matches("impl Named for ").count())622 .sum();623 assert_eq!(624 traits,625 impls * trait_fns,626 "every impl Named carries every trait fn"627 );628 let named = built629 .manifest630 .types631 .iter()632 .filter(|t| matches!(t.cross, TypeCross::Enum { named: true, .. }))633 .count();634 assert_eq!(635 written + built.macro_body_fns,636 grep,637 "every grep line is a written pub fn or sits inside macro_rules"638 );639 assert_eq!(generated, 2 * named, "named_enum! writes all and name");640 assert_eq!(fns.len(), written + constant + generated + traits + defaults);641 assert!(built.collisions.is_empty(), "{:?}", built.collisions);642}