tests.rs
32.8 kB · rust · 879 lines
1use crate::model::*;2use crate::parse::{self, Files};3use crate::report;4use crate::resolve::{self, Built};5use crate::{cli, disk, js, py, version};6use serde_json::{json, Value};7use std::path::{Path, PathBuf};89fn build(lib: &str) -> Built {10 let files: Files = [("lib.rs".to_string(), lib.to_string())]11 .into_iter()12 .collect();13 resolve::build(&files, "0.0.0").expect("the source builds")14}1516fn manifest(lib: &str) -> Manifest {17 build(lib).manifest18}1920fn function<'a>(m: &'a Manifest, path: &str) -> &'a Function {21 let paths: Vec<&str> = m.functions.iter().map(|f| f.path.as_str()).collect();22 m.functions23 .iter()24 .find(|f| f.path == path)25 .unwrap_or_else(|| panic!("no {path} in {paths:?}"))26}2728fn reason(m: &Manifest, path: &str) -> String {29 match &function(m, path).cross {30 Cross::Skip { reason } => reason.clone(),31 other => panic!("{path} is {other:?}"),32 }33}3435fn kind<'a>(m: &'a Manifest, path: &str) -> &'a TypeCross {36 let paths: Vec<&str> = m.types.iter().map(|t| t.path.as_str()).collect();37 &m.types38 .iter()39 .find(|t| t.path == path)40 .unwrap_or_else(|| panic!("no {path} in {paths:?}"))41 .cross42}4344fn scratch(name: &str, lib: &str, units: &str) -> PathBuf {45 let root = std::env::temp_dir().join(format!("bridge-{name}-{}", std::process::id()));46 let _ = std::fs::remove_dir_all(&root);47 std::fs::create_dir_all(root.join("pkgs/bridge")).expect("a scratch root");48 std::fs::write(root.join("pkgs/bridge/units.txt"), units).expect("units.txt");49 let m = manifest(lib);50 cli::write(&m, &root).expect("the cli writes");51 py::write(&m, &root).expect("the python bridge writes");52 js::write(&m, &root).expect("the js bridge writes");53 root54}5556fn read(root: &Path, file: &str) -> String {57 std::fs::read_to_string(root.join(file)).unwrap_or_else(|_| panic!("no {file}"))58}5960const 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}; }";6162#[test]63fn scalars_and_strings_cross() {64 let m = manifest(65 "pub fn f(a: bool, b: u8, c: f64, d: String, e: &str, g: char, h: f32, i: i8) -> u64 { 0 }",66 );67 let f = function(&m, "f");68 assert_eq!(f.cross, Cross::Ok);69 assert_eq!(f.params[3].ty, Ty::String);70 assert_eq!(f.params[4].ty, Ty::Str);71 assert_eq!(72 f.params[5].ty,73 Ty::Scalar {74 name: "char".into()75 }76 );77 assert_eq!(f.ret, Ty::Scalar { name: "u64".into() });78}7980#[test]81fn wide_integers_and_code_cross() {82 let m = manifest(83 "pub struct Code(pub(crate) u128); pub fn f(a: u128, b: i128, c: Code) -> Code { c }",84 );85 let f = function(&m, "f");86 assert_eq!(f.cross, Cross::Ok);87 assert_eq!(f.params[0].ty, Ty::U128);88 assert_eq!(f.params[1].ty, Ty::I128);89 assert_eq!(f.ret, Ty::Code);90}9192#[test]93fn containers_and_results_cross() {94 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) }}");95 let m = manifest(&src);96 let f = function(&m, "f");97 assert_eq!(f.cross, Cross::Ok);98 let u8 = || Box::new(Ty::Scalar { name: "u8".into() });99 assert_eq!(f.params[0].ty, Ty::Vec { item: u8() });100 assert_eq!(101 f.params[1].ty,102 Ty::Slice {103 mutable: false,104 item: Box::new(Ty::Scalar { name: "u64".into() })105 }106 );107 assert_eq!(108 f.params[2].ty,109 Ty::Option {110 item: Box::new(Ty::Scalar {111 name: "usize".into()112 })113 }114 );115 assert_eq!(116 f.params[3].ty,117 Ty::Tuple {118 items: vec![*u8(), *u8()]119 }120 );121 assert_eq!(f.params[4].ty, Ty::Array { item: u8(), len: 4 });122 assert_eq!(123 f.ret,124 Ty::Result {125 item: Box::new(Ty::Vec { item: u8() })126 }127 );128}129130#[test]131fn json_maps_and_sets_cross() {132 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!() }");133 let f = function(&m, "f");134 assert_eq!(f.cross, Cross::Ok);135 assert_eq!(136 f.params[0].ty,137 Ty::Ref {138 mutable: false,139 lifetime: None,140 item: Box::new(Ty::Json)141 }142 );143 assert!(matches!(f.params[1].ty, Ty::Map { .. }));144 assert!(matches!(f.ret, Ty::Set { .. }));145}146147#[test]148fn hand_methods_export_as_free_functions_self_first() {149 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; }");150 assert_eq!(151 kind(&m, "core::Tensor"),152 &TypeCross::Hand {153 name: "Tensor".into()154 }155 );156 let new = function(&m, "core::tensor::new");157 assert_eq!(158 (new.owner.as_deref(), new.self_kind, new.module.as_str()),159 (Some("core::Tensor"), None, "core::tensor")160 );161 let size = function(&m, "core::tensor::size");162 assert_eq!(163 (size.self_kind, &size.cross),164 (Some(SelfKind::Ref), &Cross::Ok)165 );166}167168#[test]169fn a_hand_type_in_a_private_module_exports_at_the_nearest_public_module() {170 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; }");171 let f = function(&m, "six::width");172 assert_eq!((f.module.as_str(), &f.cross), ("six", &Cross::Ok));173}174175#[test]176fn rng_crosses_by_hand_and_is_passed_mutably() {177 let m = manifest("pub struct Rng { s: u64 } pub fn draw(rng: &mut Rng) -> u64 { 0 }");178 assert_eq!(kind(&m, "Rng"), &TypeCross::Hand { name: "Rng".into() });179 let f = function(&m, "draw");180 assert_eq!(f.cross, Cross::Ok);181 assert_eq!(182 f.params[0].ty,183 Ty::Ref {184 mutable: true,185 lifetime: None,186 item: Box::new(Ty::Hand {187 name: "Rng".into(),188 dim: None189 })190 }191 );192}193194#[test]195fn an_image_crosses_by_hand_as_one_shaped_array_and_a_list_of_them() {196 let lib = "pub mod core { pub mod image { #[derive(Clone, Serialize, Deserialize)] pub struct Image { pub width: usize, pub height: usize, pub colors: Vec<[u8; 4]> } impl Image { pub fn scale(&self, scale: usize) -> Image { self.clone() } } } pub use image::Image; } pub mod life { pub fn frame(scale: usize) -> crate::core::Image { todo!() } pub fn frames(scale: usize) -> Vec<crate::core::Image> { vec![] } }";197 let m = manifest(lib);198 assert_eq!(199 kind(&m, "core::Image"),200 &TypeCross::Hand {201 name: "Image".into()202 }203 );204 let image = || Ty::Hand {205 name: "Image".into(),206 dim: None,207 };208 assert_eq!(function(&m, "life::frame").ret, image());209 assert_eq!(210 function(&m, "life::frames").ret,211 Ty::Vec {212 item: Box::new(image())213 }214 );215 let scale = function(&m, "core::image::scale");216 assert_eq!(217 (scale.self_kind, &scale.cross),218 (Some(SelfKind::Ref), &Cross::Ok)219 );220 let root = scratch("image", lib, "core\nlife\n");221 let rust = read(&root, "pkgs/mrlypy/src/gen.rs");222 assert!(rust.contains("image: PyImage,"));223 assert!(rust.contains("(PyImage(out)).into_bound_py_any(py)"));224 assert!(rust.contains(".map(PyImage)"));225 let stub = read(&root, "pkgs/mrlypy/python/mrlypy/life/__init__.pyi");226 assert!(stub.contains("def frame(scale: int) -> NDArray[Any]:"));227 assert!(stub.contains("def frames(scale: int) -> list[NDArray[Any]]:"));228 let wasm = read(&root, "pkgs/mrlyjs/units/life/src/lib.rs");229 assert!(wasm.contains("hand::image_to_js(&value)"));230 assert!(wasm.contains("hand::list_to_js(&value, hand::image_to_js)"));231 let core = read(&root, "pkgs/mrlyjs/units/core/src/lib.rs");232 assert!(core.contains("let image = hand::image_from_js(&image)?;"));233 let dts = read(&root, "pkgs/mrlyjs/life.d.ts");234 assert!(dts.contains(235 "export interface Image {\n shape: [number, number];\n colors: Uint8Array;\n}"236 ));237 assert!(dts.contains("export function frame(scale: number): Image;"));238 assert!(dts.contains("export function frames(scale: number): Image[];"));239 let cli: String = read(&root, "pkgs/mrlyrs/src/bin/mrly.rs")240 .split_whitespace()241 .collect();242 assert!(cli.contains("(\"life.frames\",\"(scale:usize)->[Image]\""));243 std::fs::remove_dir_all(root).ok();244}245246#[test]247fn a_serde_struct_without_self_methods_is_plain_and_keeps_its_constructors() {248 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 } } } }");249 assert_eq!(kind(&m, "gen::Tile"), &TypeCross::Plain);250 let f = function(&m, "gen::Tile::new");251 assert_eq!(252 (f.owner.as_deref(), f.module.as_str(), &f.cross),253 (Some("gen::Tile"), "gen", &Cross::Ok)254 );255}256257#[test]258fn a_serde_skipped_field_makes_a_class() {259 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 image: Vec<u8> } #[derive(Serialize, Deserialize)] pub struct Tile { #[serde(default, skip_serializing_if = \"Option::is_none\")] pub side: Option<usize> } }");260 assert_eq!(kind(&m, "gen::File"), &TypeCross::Class);261 assert_eq!(kind(&m, "gen::Tile"), &TypeCross::Plain);262 let file = m263 .types264 .iter()265 .find(|t| t.path == "gen::File")266 .expect("File is listed");267 let skips: Vec<bool> = file.fields.iter().map(|f| f.serde_skip).collect();268 assert_eq!(skips, [false, false, true]);269}270271#[test]272fn a_struct_with_a_self_method_is_a_class() {273 let m = manifest("pub mod life { #[derive(Serialize, Deserialize)] pub struct Life { pub time: usize } impl Life { pub fn step(&mut self) {} } }");274 assert_eq!(kind(&m, "life::Life"), &TypeCross::Class);275 let f = function(&m, "life::Life::step");276 assert_eq!((f.self_kind, &f.cross), (Some(SelfKind::Mut), &Cross::Ok));277}278279#[test]280fn a_fieldless_serde_enum_is_a_string_even_with_methods() {281 let m = manifest("#[derive(Serialize, Deserialize)] pub enum Dtype { U8, U16 } impl Dtype { pub fn max(self) -> i64 { 0 } }");282 assert_eq!(283 kind(&m, "Dtype"),284 &TypeCross::Enum {285 named: false,286 words: vec![]287 }288 );289 let f = function(&m, "Dtype::max");290 assert_eq!((f.self_kind, &f.cross), (Some(SelfKind::Value), &Cross::Ok));291}292293#[test]294fn a_named_enum_records_its_words_all_and_name() {295 let m = manifest("pub mod life { named_enum! { #[derive(Serialize, Deserialize)] pub enum Fate { Dead => \"dead\", Alive => \"alive\", } } }");296 assert_eq!(297 kind(&m, "life::Fate"),298 &TypeCross::Enum {299 named: true,300 words: vec!["dead".into(), "alive".into()]301 }302 );303 let all = function(&m, "life::Fate::all");304 assert_eq!(305 (all.source, all.constant, &all.cross),306 (Source::NamedEnum, true, &Cross::Ok)307 );308 assert_eq!(309 all.ret,310 Ty::Array {311 item: Box::new(Ty::Enum {312 path: "life::Fate".into()313 }),314 len: 2315 }316 );317 let name = function(&m, "life::Fate::name");318 assert_eq!(319 (name.source, &name.cross),320 (321 Source::NamedEnum,322 &Cross::Skip {323 reason: "&'static return".into()324 }325 )326 );327}328329#[test]330fn const_generic_functions_dispatch_on_two_and_three() {331 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 } } }");332 let fills = function(&m, "cell::fills");333 assert_eq!(334 (fills.dims.as_slice(), &fills.cross),335 ([2, 3].as_slice(), &Cross::Ok)336 );337 let rotate = function(&m, "cell::rotate");338 assert_eq!(rotate.dims, vec![2]);339 assert_eq!(340 rotate.ret,341 Ty::Hand {342 name: "CellNd".into(),343 dim: Some(2)344 }345 );346 let alias = m347 .types348 .iter()349 .find(|t| t.path == "cell::Cell2d")350 .expect("the alias");351 assert_eq!(352 (alias.kind, &alias.alias),353 (354 TypeKind::Alias,355 &Some(Ty::Hand {356 name: "CellNd".into(),357 dim: Some(2)358 })359 )360 );361}362363#[test]364fn a_trait_generic_is_skipped() {365 let m = manifest("pub fn uniform<T: PartialEq>(items: &[T]) -> bool { true }");366 assert_eq!(reason(&m, "uniform"), "generic over T");367}368369#[test]370fn a_closure_parameter_is_skipped() {371 let m = manifest("pub fn render<F>(rule: F) -> u8 where F: Fn(&[u8]) -> bool { 0 }");372 assert_eq!(reason(&m, "render"), "closure parameter F");373}374375#[test]376fn a_static_return_is_skipped() {377 let m = manifest("pub fn table() -> &'static [u8] { &[] }");378 assert_eq!(reason(&m, "table"), "&'static return");379}380381#[test]382fn a_static_str_return_is_copied_out() {383 let m = manifest("pub fn genus() -> &'static str { \"iso\" }");384 assert_eq!(function(&m, "genus").cross, Cross::Ok);385}386387#[test]388fn a_lifetime_bearing_return_is_skipped() {389 let m = manifest("pub fn first<'a>(items: &'a [u8]) -> &'a u8 { &items[0] }");390 assert_eq!(reason(&m, "first"), "lifetime-bearing return");391}392393#[test]394fn an_elided_borrow_return_is_copied_out() {395 let m = manifest("pub struct Cell { pub types: Vec<u8> } impl Cell { pub fn shape(&self) -> &[u8] { &self.types } }");396 assert_eq!(function(&m, "shape").cross, Cross::Ok);397}398399#[test]400fn a_mutable_borrow_return_is_skipped() {401 let m = manifest("pub struct Tensor { pub data: Vec<u8> } impl Tensor { pub fn bytes_mut(&mut self) -> &mut [u8] { &mut self.data } }");402 assert_eq!(reason(&m, "bytes_mut"), "mutable borrow return");403}404405#[test]406fn a_serde_helper_in_a_private_module_is_private() {407 let m = manifest("mod counts { pub fn serialize<S: serde::Serializer>(counts: &u8, serializer: S) -> Result<S::Ok, S::Error> { todo!() } }");408 assert_eq!(function(&m, "counts::serialize").cross, Cross::Private);409}410411#[test]412fn an_error_constructor_is_skipped() {413 let src = format!("{ERROR} pub mod other {{}} impl core::error::Error {{}} pub mod core2 {{}}");414 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()..]));415 assert_eq!(reason(&m, "core::error::value_error"), "error constructor");416}417418#[test]419fn an_impl_trait_argument_is_skipped() {420 let m = manifest("pub fn new(birth: impl Into<u8>) -> u8 { 0 }");421 assert!(reason(&m, "new").starts_with("birth: impl Trait argument"));422}423424#[test]425fn an_iterator_return_is_skipped() {426 let m = manifest("pub fn each() -> impl Iterator<Item = u8> { std::iter::empty() }");427 assert_eq!(reason(&m, "each"), "returns iterator return");428}429430#[test]431fn a_private_type_in_a_signature_is_skipped() {432 let m = manifest("type Rotation = fn() -> u8; struct Point; pub fn create(rotation: Rotation) {} pub fn corners() -> Point { Point }");433 assert_eq!(reason(&m, "create"), "rotation: unknown type Rotation");434 assert_eq!(reason(&m, "corners"), "returns unknown type Point");435}436437#[test]438fn a_type_without_serde_or_methods_is_uncrossable() {439 let m = manifest("pub mod two { pub enum Shape { Square } pub fn image(shape: Shape) {} }");440 assert!(matches!(441 kind(&m, "two::Shape"),442 TypeCross::Uncrossable { .. }443 ));444 assert_eq!(445 reason(&m, "two::image"),446 "shape: uncrossable type two::Shape"447 );448}449450#[test]451fn a_class_from_another_unit_is_skipped() {452 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 } }");453 assert_eq!(454 reason(&m, "math::render"),455 "class core::Colorizer lives in unit core"456 );457}458459#[test]460fn mutating_plain_data_in_place_is_skipped() {461 let m = manifest("pub fn push(row: &mut String) {} pub fn fft(re: &mut [f64]) {}");462 assert_eq!(reason(&m, "push"), "row: mutates plain data in place");463 assert_eq!(reason(&m, "fft"), "re: mutable slice argument");464}465466#[test]467fn the_shortest_public_path_wins_and_ties_go_to_the_definition() {468 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; }");469 assert_eq!(function(&m, "a::f").module, "a");470 assert_eq!(function(&m, "cell::edges").cross, Cross::Ok);471 assert!(m.functions.iter().all(|f| f.path != "two::edges"));472}473474#[test]475fn re_exports_resolve_groups_renames_and_globs() {476 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}; }");477 for path in ["g::x", "h::ey", "h::z"] {478 assert_eq!(function(&m, path).cross, Cross::Ok, "{path}");479 }480}481482#[test]483fn an_item_reachable_only_through_a_private_module_is_private() {484 let m = manifest(485 "mod hidden { pub fn f() {} pub struct S; } pub mod shown { mod deep { pub fn g() {} } }",486 );487 assert_eq!(function(&m, "hidden::f").cross, Cross::Private);488 assert_eq!(function(&m, "shown::deep::g").cross, Cross::Private);489 assert!(m.types.is_empty());490}491492#[test]493fn a_name_collision_fails_the_run() {494 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!() } } }");495 assert_eq!(496 built.collisions,497 vec!["collision: core::new at lib.rs:1 and lib.rs:1"]498 );499 let built =500 build("pub mod census { pub fn census() {} pub fn extra() {} } pub use census::census;");501 assert_eq!(502 built.collisions,503 vec!["collision: module census and fn census at lib.rs:1"]504 );505}506507#[test]508fn skip_txt_must_name_every_skipped_function_and_nothing_else() {509 let m = manifest("pub fn genus() -> &'static [u8] { &[] } pub fn fine() {}");510 assert_eq!(report::skip_lines(&m), vec!["genus &'static return"]);511 assert_eq!(512 report::check_skip(&m, ""),513 vec!["skip.txt is missing genus"]514 );515 assert_eq!(516 report::check_skip(&m, "fine a stale line\n"),517 vec!["skip.txt is missing genus", "skip.txt names nothing: fine"]518 );519 assert!(report::check_skip(&m, "genus a decision\n").is_empty());520}521522#[test]523fn consts_are_listed_with_their_type() {524 let m = manifest("pub mod font { pub const FPS: usize = 25; pub type Pen = (char, &'static [&'static str]); pub const UPPERS: &[Pen] = &[]; }");525 let fps = m526 .consts527 .iter()528 .find(|c| c.path == "font::FPS")529 .expect("fps");530 assert_eq!(531 (&fps.ty, &fps.cross),532 (533 &Ty::Scalar {534 name: "usize".into()535 },536 &Cross::Ok537 )538 );539 let uppers = m540 .consts541 .iter()542 .find(|c| c.path == "font::UPPERS")543 .expect("uppers");544 assert_eq!(545 uppers.cross,546 Cross::Skip {547 reason: "&'static const".into()548 }549 );550}551552#[test]553fn a_public_trait_adds_its_methods_to_every_implementing_type() {554 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) } } } }");555 assert_eq!(kind(&m, "name::word::Word"), &TypeCross::Class);556 let checked = function(&m, "name::word::Word::checked");557 assert_eq!(558 (checked.self_kind, checked.source, checked.via.as_deref()),559 (Some(SelfKind::Value), Source::Trait, Some("name::Named"))560 );561 assert_eq!(checked.docs, vec!["Folds."]);562 assert!(checked.defined_at.starts_with("lib.rs:"));563 let from = function(&m, "name::word::Word::from_json");564 assert_eq!(565 (from.self_kind, from.owner.as_deref()),566 (None, Some("name::word::Word"))567 );568 assert!(569 matches!(from.cross, Cross::Skip { .. }),570 "Result<Self, u8> is not the crate Result"571 );572 assert_eq!(function(&m, "name::word::Word::to_json").ret, Ty::String);573 assert!(m.functions.iter().all(|f| f.name != "KIND"));574}575576#[test]577fn a_public_class_field_gets_a_setter_unless_it_holds_a_borrow() {578 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");579 let rust = read(&root, "pkgs/mrlypy/src/gen.rs");580 assert!(rust.contains(581 "pub fn set_boundary(&mut self, value: PySerde<mrlyrs::life::Boundary>) -> PyResult<()> {"582 ));583 assert!(rust.contains("self.0.padding = value;"));584 assert!(!rust.contains("set_name"));585 assert!(read(&root, "pkgs/mrlypy/python/mrlypy/life/__init__.pyi").contains("@boundary.setter"));586 let wasm = read(&root, "pkgs/mrlyjs/units/life/src/lib.rs");587 assert!(588 wasm.contains("pub fn set_boundary(&mut self, value: JsValue) -> Result<(), JsValue> {")589 );590 assert!(wasm.contains("self.inner.padding = value;"));591 assert!(!wasm.contains("set_name"));592 let dts = read(&root, "pkgs/mrlyjs/life.d.ts");593 assert!(dts.contains("set boundary(value: Boundary);"));594 assert!(dts.contains("readonly name: string;"));595 std::fs::remove_dir_all(root).ok();596}597598#[test]599fn default_crosses_as_a_static_on_the_type_and_an_alias_fixes_n() {600 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 } }";601 let m = manifest(lib);602 let paint = function(&m, "gen::Paint::default");603 assert_eq!(604 (605 paint.source,606 paint.owner.as_deref(),607 paint.self_kind,608 &paint.cross609 ),610 (Source::Default, Some("gen::Paint"), None, &Cross::Ok)611 );612 assert_eq!(613 paint.ret,614 Ty::Plain {615 path: "gen::Paint".into()616 }617 );618 assert_eq!(619 function(&m, "gen::Field::default").ret,620 Ty::Class {621 path: "gen::Field".into(),622 dim: None623 }624 );625 assert_eq!(626 function(&m, "gen::Config2d::default").ret,627 Ty::Plain {628 path: "gen::ConfigNd".into()629 }630 );631 let defaults: Vec<&str> = m632 .functions633 .iter()634 .filter(|f| f.source == Source::Default)635 .map(|f| f.path.as_str())636 .collect();637 assert_eq!(638 defaults,639 [640 "gen::Config2d::default",641 "gen::Field::default",642 "gen::Paint::default"643 ]644 );645 let root = scratch("default", lib, "gen\n");646 assert!(read(&root, "pkgs/mrlypy/python/mrlypy/gen/__init__.pyi")647 .contains("class Config2d:\n @staticmethod\n def default() -> dict[str, Any]:"));648 assert!(read(&root, "pkgs/mrlyjs/gen.d.ts").contains("static default(): Field;"));649 let cli: String = read(&root, "pkgs/mrlyrs/src/bin/mrly.rs")650 .split_whitespace()651 .collect();652 assert!(cli.contains("(\"gen.Config2d.default\",\"()->gen.ConfigNd\""));653 std::fs::remove_dir_all(root).ok();654}655656#[test]657fn the_hand_rng_declares_choice_and_shuffle_in_both_bridges() {658 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");659 let stub = read(&root, "pkgs/mrlypy/python/mrlypy/core/__init__.pyi");660 assert!(stub.contains(" def choice(self, seq: Any) -> Any:"));661 assert!(stub.contains(" def shuffle(self, seq: list[Any]) -> None:"));662 let dts = read(&root, "pkgs/mrlyjs/core.d.ts");663 assert!(dts.contains(" choice<T>(items: ArrayLike<T>): T;"));664 assert!(dts.contains(" shuffle<T>(items: T[]): void;"));665 std::fs::remove_dir_all(root).ok();666}667668#[test]669fn a_nested_type_without_serde_shuts_its_cli_door() {670 let lib = "pub mod num { #[derive(Clone)] pub struct Pair { pub n: u8 } impl Pair { pub fn size(&self) -> u8 { 0 } } pub fn pairs() -> Vec<Pair> { vec![] } }";671 let root = scratch("nested", lib, "num\n");672 let cli = read(&root, "pkgs/mrlyrs/src/bin/mrly.rs");673 assert!(cli.contains("\"() -> [num.Pair] # uncallable: num::Pair has no Serialize\""));674 std::fs::remove_dir_all(root).ok();675}676677#[test]678fn a_watched_run_names_the_stale_files_and_writes_nothing() {679 let lib = "pub mod core { pub fn add(a: u8, b: u8) -> u8 { a + b } }";680 let root = scratch("check", lib, "core\n");681 let m = manifest(lib);682 let (old, gone) = ("pkgs/mrlypy/src/gen.rs", "pkgs/mrlyjs/core.js");683 std::fs::write(root.join(old), "stale").expect("a stale file");684 std::fs::remove_file(root.join(gone)).expect("a missing file");685 disk::watch();686 py::write(&m, &root).expect("the python bridge checks");687 js::write(&m, &root).expect("the js bridge checks");688 assert_eq!(disk::stale(), [root.join(old), root.join(gone)]);689 assert_eq!(read(&root, old), "stale");690 assert!(!root.join(gone).exists());691 std::fs::remove_dir_all(root).ok();692}693694#[test]695fn generated_rust_is_saved_formatted() {696 let lib = "pub mod core { pub fn add(a: u8, b: u8) -> u8 { a + b } }";697 let root = scratch("fmt", lib, "core\n");698 for file in [699 "pkgs/mrlyrs/src/bin/mrly.rs",700 "pkgs/mrlypy/src/gen.rs",701 "pkgs/mrlyjs/units/core/src/lib.rs",702 ] {703 let text = read(&root, file);704 assert_eq!(disk::rustfmt(&text).expect("rustfmt runs"), text, "{file}");705 }706 std::fs::remove_dir_all(root).ok();707}708709#[test]710fn every_copy_takes_the_manifest_version() {711 let lib = "pub mod core { pub fn f() {} }";712 let root = scratch("version", lib, "core\n");713 let py = "[package]\nname = \"mrlypy\"\nversion = \"0.1.0\"\n\n[dependencies]\npyo3 = { version = \"0.29\" }\n";714 let js = "{\n \"name\": \"mrlyjs\",\n \"version\": \"0.1.0\",\n \"type\": \"module\"\n}\n";715 std::fs::write(root.join("pkgs/mrlypy/Cargo.toml"), py).expect("a crate");716 std::fs::write(root.join("pkgs/mrlyjs/package.json"), js).expect("a package");717 let mut m = manifest(lib);718 m.version = "1.2.3".into();719 js::write(&m, &root).expect("the js bridge writes");720 version::write(&m, &root).expect("the stamps write");721 assert!(read(&root, "pkgs/mrlyjs/units/core/Cargo.toml").contains("\nversion = \"1.2.3\"\n"));722 assert_eq!(723 read(&root, "pkgs/mrlypy/Cargo.toml"),724 py.replace("0.1.0", "1.2.3")725 );726 assert_eq!(727 read(&root, "pkgs/mrlyjs/package.json"),728 js.replace("0.1.0", "1.2.3")729 );730 std::fs::remove_dir_all(root).ok();731}732733fn entry(path: &str, ret: &str, status: &str) -> Value {734 json!({"path": path, "self_kind": null, "params": [], "ret": {"t": "scalar", "name": ret}, "dims": [], "cross": {"status": status}})735}736737#[test]738fn a_removed_or_changed_path_breaks_and_an_addition_does_not() {739 let old = [740 entry("a", "u8", "ok"),741 entry("b", "u8", "ok"),742 entry("c", "u8", "ok"),743 entry("d", "u8", "ok"),744 entry("p", "u8", "private"),745 ];746 let new = [747 entry("a", "u8", "ok"),748 entry("b", "u16", "ok"),749 entry("d", "u8", "skip"),750 entry("e", "u8", "ok"),751 ];752 let diff = version::diff(&json!({"functions": old}), &json!({"functions": new}));753 assert_eq!(diff.removed, ["c", "d"]);754 assert_eq!(diff.changed, ["b"]);755 assert_eq!(diff.added, 1);756 let grown = [&old[..4], &[entry("e", "u8", "ok")]].concat();757 let diff = version::diff(&json!({"functions": old}), &json!({"functions": grown}));758 assert!(diff.removed.is_empty() && diff.changed.is_empty());759 assert_eq!(diff.added, 1);760}761762#[test]763fn a_break_needs_the_minor_under_one_and_the_major_from_one() {764 assert_eq!(version::need((0, 2, 3), true), (0, 3, 0));765 assert_eq!(version::need((0, 2, 3), false), (0, 2, 4));766 assert_eq!(version::need((1, 2, 3), true), (2, 0, 0));767 assert!(version::semver("0.10.0") > version::semver("0.9.9"));768 assert_eq!(version::semver("0.2"), None);769}770771#[test]772fn the_bump_reads_the_tagged_manifest_where_the_bridge_lives() {773 let root = std::env::temp_dir().join(format!("bridge-bump-{}", std::process::id()));774 let _ = std::fs::remove_dir_all(&root);775 let file = root.join("pkgs/bridge/manifest.json");776 std::fs::create_dir_all(root.join("pkgs/bridge")).expect("a scratch root");777 let git = |args: &[&str]| {778 let done = std::process::Command::new("git")779 .arg("-C")780 .arg(&root)781 .args(["-c", "user.name=bridge"])782 .args(["-c", "user.email=bridge@localhost"])783 .args(["-c", "commit.gpgsign=false"])784 .args(args)785 .output()786 .expect("git runs");787 assert!(done.status.success(), "git {args:?}");788 };789 let write = |version: &str, ret: &str| {790 let text = json!({"version": version, "functions": [entry("a", ret, "ok")]});791 std::fs::write(&file, text.to_string()).expect("a manifest");792 };793 git(&["init", "-q"]);794 write("0.1.0", "u8");795 git(&["add", "-A"]);796 git(&["commit", "-q", "-m", "one"]);797 git(&["tag", "v0.1.0"]);798 write("0.1.1", "u8");799 assert_eq!(version::bump(&root), Ok(()));800 write("0.1.1", "u16");801 let refused = version::bump(&root).expect_err("a break under a patch");802 assert!(refused.contains("needs 0.2.0"), "{refused}");803 std::fs::remove_dir_all(root).ok();804}805806fn crate_files() -> Files {807 parse::load(Path::new(concat!(808 env!("CARGO_MANIFEST_DIR"),809 "/../mrlyrs/src"810 )))811 .expect("the crate loads")812}813814#[test]815fn the_crate_parses_the_same_twice() {816 let files = crate_files();817 let a = resolve::build(&files, "0").expect("first").manifest;818 let b = resolve::build(&files, "0").expect("second").manifest;819 assert_eq!(a, b);820 assert_eq!(821 serde_json::to_string(&a).unwrap(),822 serde_json::to_string(&b).unwrap()823 );824}825826#[test]827fn function_entries_match_the_pub_fn_grep() {828 let files = crate_files();829 let built = resolve::build(&files, "0").expect("the crate builds");830 let grep: usize = files831 .values()832 .map(|t| t.lines().filter(|l| parse::is_pub_fn_line(l)).count())833 .sum();834 let fns = &built.manifest.functions;835 let written = fns836 .iter()837 .filter(|f| f.source == Source::Written && !f.constant)838 .count();839 let constant = fns840 .iter()841 .filter(|f| f.source == Source::Written && f.constant)842 .count();843 let generated = fns.iter().filter(|f| f.source == Source::NamedEnum).count();844 let traits = fns.iter().filter(|f| f.source == Source::Trait).count();845 let defaults = fns.iter().filter(|f| f.source == Source::Default).count();846 let text = &files["math/name/mod.rs"];847 let start = text.find("pub trait Named").expect("the trait");848 let end = start + text[start..].find("\n}\n").expect("its end");849 let trait_fns = text[start..end]850 .lines()851 .filter(|l| l.starts_with(" fn "))852 .count();853 let impls: usize = files854 .values()855 .map(|t| t.matches("impl Named for ").count())856 .sum();857 assert_eq!(858 traits,859 impls * trait_fns,860 "every impl Named carries every trait fn"861 );862 let named = built863 .manifest864 .types865 .iter()866 .filter(|t| matches!(t.cross, TypeCross::Enum { named: true, .. }))867 .count();868 assert_eq!(869 written + built.macro_body_fns,870 grep,871 "every grep line is a written pub fn or sits inside macro_rules"872 );873 assert_eq!(generated, 2 * named, "named_enum! writes all and name");874 assert_eq!(875 fns.len(),876 written + constant + generated + traits + defaults877 );878 assert!(built.collisions.is_empty(), "{:?}", built.collisions);879}