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}