//! Las divergencias que el banco diferencial MIDIÓ, convertidas en pruebas. //! //! Cada una está acá porque se rompió de verdad —en brush 0.4.0, o en la primera versión de simi— //! y ninguna se veía sin comparar contra un shell de control. La referencia de todas es el ash de //! busybox 1.36.1, medido en la misma corrida (`docs/plan-botar-busybox.md`). //! //! Corren el binario de verdad, no la librería: lo que importa es el comportamiento observable. use std::process::Command; fn correr(guion: &str) -> (i32, String, String) { let salida = Command::new(env!("CARGO_BIN_EXE_simi")) .arg("-c") .arg(guion) .output() .expect("no pude ejecutar simi"); ( salida.status.code().unwrap_or(-1), String::from_utf8_lossy(&salida.stdout).into_owned(), String::from_utf8_lossy(&salida.stderr).into_owned(), ) } fn salida(guion: &str) -> String { correr(guion).1 } /// reubeno/brush#1394 · POSIX.1-2024 §2.6.3. La barra invertida dentro de `` ` ` `` es literal /// salvo ante `$`, `` ` `` y `\`. Es el bug que hacía que el `libtool` generado saliera con un /// nivel de escapado de menos y que 0 de 3 recetas autotools construyeran. #[test] fn barra_invertida_en_comillas_invertidas() { let g = r#"V=hola echo "a: [`echo \$V`]" echo "b: [`echo \\`]" echo "c: [`echo \\$V`]" echo "d: [$(echo \\$V)]""#; assert_eq!(salida(g), "a: [hola]\nb: [\\]\nc: [$V]\nd: [\\hola]\n"); } /// reubeno/brush#1396 · el `trap EXIT` de un subshell CORRE. brush no lo ejecuta nunca —ni con /// `exit` explícito— y falla en silencio: `rc=0` y la limpieza sin hacer. #[test] fn trap_exit_de_subshell() { assert_eq!( salida(r#"trap 'echo "[padre]"' EXIT; ( trap 'echo "[sub]"' EXIT; echo dentro ); echo fuera"#), "dentro\n[sub]\nfuera\n[padre]\n" ); // con `exit` explícito y con el estado preservado let (rc, out, _) = correr(r#"( trap 'echo T' EXIT; exit 3 ); echo rc=$?"#); assert_eq!((rc, out.as_str()), (0, "T\nrc=3\n")); // y en una sustitución de orden, que también es un subshell assert_eq!(salida(r#"x=$( trap 'echo T' EXIT; echo v ); echo "[$x]""#), "[v\nT]\n"); } /// El trap heredado NO se dispara en el subshell (POSIX §2.12): se resetea al entrar. Es el caso /// complementario del anterior, y confundirlos es fácil. #[test] fn trap_heredado_no_se_dispara_en_el_subshell() { assert_eq!( salida(r#"trap 'echo "[exit]"' EXIT; echo padre; ( echo sub ); echo vuelta"#), "padre\nsub\nvuelta\n[exit]\n" ); } /// `shift` más allá de `$#`: estado 1 y los posicionales intactos. brush devolvía 2. #[test] fn shift_mas_alla_de_argc() { assert_eq!(salida("set --; shift 2>/dev/null; echo rc=$?"), "rc=1\n"); assert_eq!(salida("set -- a b; shift 5 2>/dev/null; echo rc=$?-$#"), "rc=1-2\n"); assert_eq!(salida("set -- a b c; shift 2; echo $1"), "c\n"); } /// El bucle REAL del `config.status` que genera cualquier `configure` de autotools. Es el /// consumidor que decide, y el que ningún banco sintético reproduce: la primera versión de simi /// devolvía las tres variables VACÍAS, igual que brush. #[test] fn bucle_de_config_status_de_libtool() { let g = r#"ECHO="printf %s\\n" SED=sed GREP=grep sed_quote_subst='s/\([`"$\\]\)/\\\1/g' for var in ECHO SED GREP; do case `eval \\$ECHO \\""\\$$var"\\"` in *[\\\`\"\$]*) eval "lt_$var=\\\"\`\$ECHO \"\$$var\" | \$SED \"\$sed_quote_subst\"\`\\\"" ;; *) eval "lt_$var=\\\"\$$var\\\"" ;; esac done echo "ECHO=[$lt_ECHO]" echo "SED=[$lt_SED]" echo "GREP=[$lt_GREP]""#; // La expectativa es lo que da el ash de busybox, medido en la misma corrida — no lo que uno // cree que debería dar: la primera versión de esta prueba escribió `%s\n` de memoria y el // control dice `%s\\n`. assert_eq!( salida(g), "ECHO=[\"printf %s\\\\n\"]\nSED=[\"sed\"]\nGREP=[\"grep\"]\n" ); } /// Una asignación cuyo valor trae comillas o escapes sigue siendo una ASIGNACIÓN. Si el lexer /// funde el prefijo desnudo con el valor entrecomillado, `x="a b"` se ejecuta como si fuera una /// orden — y el fallo se lee como «no encontrado», que manda a buscar al lugar equivocado. #[test] fn asignaciones_con_comillas_y_escapes() { assert_eq!(salida(r#"x="a b"; echo "[$x]"; set -- $x; echo $#"#), "[a b]\n2\n"); assert_eq!(salida(r#"eval "y=\\\"hola\\\""; echo "[$y]""#), "[\"hola\"]\n"); assert_eq!(salida(r#"z=a'b'c; echo "[$z]""#), "[abc]\n"); } /// `set -e` se SUSPENDE dentro de una condición, y la suspensión entra en el cuerpo de la función /// llamada desde ahí. Es la semántica más divergente entre shells y la que rompe los scripts de /// limpieza cuando está mal. #[test] fn set_e_se_suspende_en_condiciones() { let g = r#"set -e ( false ) || echo or-salva if false; then echo no; fi f() { false; echo en-funcion-tras-false; } f || echo funcion-abortada echo fin"#; assert_eq!(salida(g), "or-salva\nen-funcion-tras-false\nfin\n"); // …y sigue abortando cuando toca let (rc, out, _) = correr("set -e\necho antes\nfalse\necho despues"); assert_eq!((rc, out.as_str()), (1, "antes\n")); } /// El orden de las expansiones (§2.6): lo entrecomillado no se divide ni se globea, y una barra /// invertida que SALE de una expansión es un carácter literal, no un escape. #[test] fn orden_de_expansiones() { assert_eq!(salida(r#"echo "/dev/nul*""#), "/dev/nul*\n"); assert_eq!(salida("echo /dev/nul*"), "/dev/null\n"); assert_eq!(salida("echo /no-existe-jamas*"), "/no-existe-jamas*\n"); assert_eq!(salida(r#"V=hola; echo [$(echo \\$V)]"#), "[\\hola]\n"); assert_eq!(salida("IFS=:; x=a:b:c; set -- $x; echo $#"), "3\n"); assert_eq!(salida(r#"v=arch.tar.gz; echo ${v%%.*} ${v##*.} ${#v}"#), "arch gz 11\n"); } /// El idioma de las 23 cards de arje, entero: `test … || { echo >&2; exit 78; }; exec …`. #[test] fn el_idioma_de_las_cards_de_arje() { let (rc, out, err) = correr( "test -f /no-existe || { echo 'falta la config' >&2; exit 78; }; exec /bin/true", ); assert_eq!(rc, 78); assert_eq!(out, ""); assert_eq!(err, "falta la config\n"); // el camino bueno: el `exec` REEMPLAZA el proceso let g = "echo $$ > /dev/null; exec /bin/echo reemplazado; echo NO-DEBERIA-LLEGAR"; assert_eq!(salida(g), "reemplazado\n"); } /// El bucle de espera de las cards de compatibilidad (`arje-polkit-compat`, `arje-logind-compat`). #[test] fn bucle_de_espera_con_aritmetica() { let g = "i=0; while [ ! -S /no-existe ] && [ $i -lt 5 ]; do i=$((i+1)); done; echo $i"; assert_eq!(salida(g), "5\n"); } /// Los `/init` empotrados de las imágenes: `for`, `case`, funciones, aquí-documentos. #[test] fn lo_que_usan_los_init_empotrados() { let g = r#"say() { echo "==> $*"; } say hola for d in proc sys dev raro; do case $d in proc|sys|dev) echo "salto $d" ;; *) echo "monto $d" ;; esac done cat < hola\nsalto proc\nsalto sys\nsalto dev\nmonto raro\nliteral raro\nsin expandir $d\n" ); } /// Un error de expansión en un shell no interactivo ABORTA con 2, no sigue con 1. #[test] fn errores_de_expansion_abortan_con_2() { let (rc, out, _) = correr("echo antes; echo ${nada:?falta}; echo despues"); assert_eq!((rc, out.as_str()), (2, "antes\n")); let (rc, _, _) = correr("set -u; echo $NODEFINIDA"); assert_eq!(rc, 2); let (rc, _, _) = correr("echo [`echo \"`]"); assert_eq!(rc, 2, "una comilla sin cerrar en una sustitución es error de sintaxis"); } /// `$(( ))`: la división por cero es un error, no un cero silencioso. #[test] fn division_por_cero_no_es_cero() { let (rc, out, _) = correr("echo $((1/0)); echo despues"); assert_eq!(rc, 2); assert_eq!(out, ""); }