«simi» es boca/lengua en quechua. Es el sh que el frente C necesitaba: busybox está en la raíz de confianza del arranque (STAGE1_COMPONENTS, ATTEST_PATHS) y en el exec de cada card, y el candidato ajeno que se midió —brush 0.4.0— pesa 6,9 MB, arranca 4x más lento que el ash y trae dos divergencias medidas (#1394, #1396). Alcance MEDIDO, no supuesto: las 23 cards con shell (8 builtins, 13 construcciones), los 21 /init empotrados de las imágenes (+ if, funciones, case, aquí-documentos) y el POSIX de takana-live-install.sh. Resultado del banco diferencial, 159 casos contra el ash de busybox 1.36.1 (10 fixtures de regresión, 107 de tortura de comillas, 23 fragmentos de cards, 19 /init empotrados): simi DIVERGE=0 bash DIVERGE=8 brush DIVERGE=35 Incluye el bucle REAL de config.status que genera cualquier configure de autotools — el que tumbó a brush. 499 KB release, 1,4x la latencia del ash (brush: 4,1x). 47 pruebas propias en verde. Seis bugs que el banco destapó, y ninguno se veía sin un control al lado: 1. El lexer fundía lo entrecomillado con lo desnudo, así que x="a b" se ejecutaba como ORDEN y el error decía «no encontrado» — mandando a buscar al lugar equivocado. Ahora el comillado es por carácter. 2. Usaba la barra invertida como marca interna y la quitaba al final, así que una barra PRODUCIDA por una sustitución se perdía. La quita de comillas es sobre las comillas de la palabra, nunca sobre el resultado de una expansión: hace falta una máscara de protección, no un escape dentro del texto. 3. set -e no se suspendía dentro del cuerpo de una función llamada desde una condición. Ahora es un contador dinámico, que entra solo donde corresponde. 4. Los delimitadores de la sustitución de orden y de ${ } no respetaban la barra invertida: un paréntesis escapado cerraba la sustitución. 5. Las clases entre corchetes no entendían la barra invertida — y la del config.status de libtool es literalmente una clase con cuatro escapes. 6. Dentro de comillas dobles el cuerpo de un backtick sigue entre comillas, así que la barra también se elimina ante la comilla doble. Y uno que el banco NO vio y sí vio la prueba de regresión: el trap EXIT HEREDADO se disparaba en el subshell. POSIX §2.12 manda resetearlo — es la mitad complementaria del #1396 de brush, y al revés. Los dos oráculos hacen falta; el caso quedó agregado al fixture 08.
199 lines
7.7 KiB
Rust
199 lines
7.7 KiB
Rust
//! 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 <<FIN
|
|
literal $d
|
|
FIN
|
|
cat <<'CRUDO'
|
|
sin expandir $d
|
|
CRUDO"#;
|
|
assert_eq!(
|
|
salida(g),
|
|
"==> 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, "");
|
|
}
|