Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
La integración continua (CI) nace de una observación incómoda: cuanto más tiempo pasa un cambio sin fundirse con el trabajo de los demás, más caro y doloroso se vuelve integrarlo. Hunt y Thomas, en The Pragmatic Programmer, insisten en integrar temprano y a menudo precisamente para que los conflictos se descubran cuando aún son pequeños; McConnell, en Code Complete, defiende la misma idea bajo el nombre de "integración incremental": añadir una pieza, comprobar que todo sigue funcionando, y solo entonces añadir la siguiente. La CI automatiza ese ritual. Cada vez que alguien empuja un cambio, un servidor reconstruye el proyecto, corre las pruebas y pasa el linter. Si todo pasa, el pipeline queda 'verde'; si algo falla, queda 'rojo' y el cambio no debería integrarse hasta arreglarlo.
El corazón de esta clase es una idea lógica sencilla pero exigente: el pipeline está verde solo si todos los pasos pasan. Es un AND lógico sobre los resultados. Basta que un paso —compilar, un test, una regla de estilo— falle para que el conjunto entero sea rojo. No hay "verde con excepciones". El programa de juguete de casos.json captura exactamente ese AND: recibe una lista de ceros y unos (el resultado de cada paso, 1 = pasó) y responde ci=verde únicamente cuando no hay ningún cero. Detrás de esa reducción minúscula está la disciplina que mantiene sano un repositorio real, donde cada paso puede ser un pytest, un cargo build o un go vet.
Entender esto te da algo más que un truco: te da el modelo mental para razonar sobre por qué un equipo que respeta el rojo entrega software más estable, y por qué ignorarlo —"ya lo arreglo luego"— erosiona la confianza en la rama principal hasta que nadie sabe si está sana.
Al finalizar, podrás:
fail-fast y de la caché de dependencias.| # | Tema | Por qué importa |
|---|---|---|
| 1 | CI e integración frecuente | Descubre conflictos cuando aún son baratos |
| 2 | Pipeline de pasos | Encadena build, test y lint como un contrato |
| 3 | Verde/rojo como AND | Todo pasa, o el conjunto falla |
| 4 | Matriz y fail-fast | Verifica varios entornos y aborta pronto |
casos.json en Python, Go, Rust, etc., en paralelo, y cada celda aporta su propio verde o rojo al veredicto final.fail-fast aborta el resto de la matriz en cuanto una celda falla, para no gastar minutos en un resultado ya condenado; la caché de dependencias guarda entre ejecuciones lo descargado (paquetes de pip, ~/.cargo, node_modules) para que el pipeline sea rápido y el equipo no lo evite por lento.Trabajas en un equipo que mantiene este mismo repositorio políglota. Alguien abre un pull request que toca la implementación de una clase. GitHub Actions dispara el workflow: en paralelo, una celda de la matriz compila y prueba la versión en Java, otra corre cargo test sobre la de Rust, otra pasa ruff sobre la de Python. Dos celdas quedan verdes, pero la de Rust falla porque un unwrap reventó con una entrada vacía. El pipeline entero se pone rojo. La regla del equipo es tajante: no se fusiona en rojo. El autor ve el fallo en minutos, corrige el caso borde, empuja de nuevo, y ahora las tres celdas pasan. La rama principal nunca llegó a contener el código roto. Ese pequeño programa que decide verde o rojo es, en esencia, lo que acabas de vivir a escala.
ci=<verde|rojo>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
1 1 1 |
ci=verde |
1 0 1 |
ci=rojo |
1 1 |
ci=verde |
El algoritmo es la traducción directa del AND lógico: leer los pasos y comprobar que todos valen 1.
LEER pasos ; verde <- todos == 1
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyLa versión de Python es la más transparente sobre lo que realmente ocurre. Lee todo stdin, lo parte por espacios y convierte cada token a entero mediante una comprensión de lista. La decisión vive en all(p == 1 for p in pasos): all es la encarnación exacta del AND lógico —devuelve True solo si cada elemento cumple la condición— y, además, es perezoso, así que en cuanto encuentra el primer paso distinto de 1 deja de mirar. Ese cortocircuito es el mismo espíritu del fail-fast de un pipeline real. Ramalho, en Fluent Python, destaca all/any sobre expresiones generadoras como la manera pitónica de expresar cuantificadores sin escribir un bucle explícito.
import sys
pasos = [int(x) for x in sys.stdin.read().split()]
print(f"ci={'verde' if all(p == 1 for p in pasos) else 'rojo'}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
Para 1 1 1, all recorre los tres unos y devuelve True, así que imprime ci=verde. Para 1 0 1, se detiene en el 0 y produce ci=rojo. Para 1 1, dos unos, verde. Exactamente los tres casos del contrato.
javascript/main.mjs · node main.mjsJavaScript expresa el mismo cuantificador con Array.prototype.every, que también cortocircuita en el primer elemento que no cumple. La diferencia idiomática está en la lectura: readFileSync(0, ...) lee el descriptor 0 (stdin) de una vez, y split(/\s+/) separa por cualquier bloque de espacios. Haverbeke, en Eloquent JavaScript, presenta every/some como los métodos de orden superior que capturan "para todos" y "existe" sin bucles manuales.
import { readFileSync } from "node:fs";
const pasos = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`ci=${pasos.every((p) => p === 1) ? "verde" : "rojo"}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsTypeScript es, aquí, JavaScript con la red del tipado estático: la única diferencia visible es la anotación pasos: number[], que documenta y hace comprobable en tiempo de compilación que trabajamos con un arreglo de números. Cherny, en Programming TypeScript, defiende justo este valor: los tipos son una forma de prueba que se ejecuta antes de correr el programa, otra capa de verificación temprana muy en el espíritu de la CI.
import { readFileSync } from "node:fs";
const pasos: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`ci=${pasos.every((p) => p === 1) ? "verde" : "rojo"}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
rust/main.rs · rustc main.rs -o main && ./mainRust muestra un contraste instructivo: su iterador .all(...) también es perezoso y cortocircuita, pero el parse::<i64>() devuelve un Result que aquí se resuelve con unwrap(). En un pipeline real, ese unwrap sobre una entrada malformada sería justo el tipo de fallo que pone la celda de Rust en rojo —una lección viva sobre por qué la CI existe—. Klabnik y Nichols, en The Rust Programming Language, insisten en que el sistema de tipos te obliga a decidir qué hacer con el error, aunque unwrap sea el atajo de "confío en la entrada".
use std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let verde = s.split_whitespace().all(|x| x.parse::<i64>().unwrap() == 1);
println!("ci={}", if verde { "verde" } else { "rojo" });
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
java/Main.java · java Main.javaJava resuelve el AND de la forma más elemental y quizá más didáctica: un booleano verde que empieza en true y se apaga irrevocablemente si algún paso no es 1. No cortocircuita (recorre todos los tokens), pero el resultado es idéntico. Es la traducción literal de "todos deben pasar": una sola falla contamina el conjunto. Bloch, en Effective Java, recomendaría en código de producción Arrays.stream(...).allMatch(...) para expresar la intención con un cuantificador, pero la versión con bucle deja el AND completamente a la vista.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] p = br.readLine().trim().split("\\s+");
boolean verde = true;
for (String s : p) if (Integer.parseInt(s) != 1) verde = false;
System.out.println("ci=" + (verde ? "verde" : "rojo"));
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
using System.Linq;
bool verde = Console.In.ReadToEnd()
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.All(x => int.Parse(x) == 1);
Console.WriteLine($"ci={(verde ? "verde" : "rojo")}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
verde := true
for _, s := range strings.Fields(line) {
n, _ := strconv.Atoi(s)
if n != 1 {
verde = false
}
}
res := "rojo"
if verde {
res = "verde"
}
fmt.Printf("ci=%s\n", res)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
int main(void) {
long x;
int verde = 1;
while (scanf("%ld", &x) == 1) {
if (x != 1) verde = 0;
}
printf("ci=%s\n", verde ? "verde" : "rojo");
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: verde si el mínimo de los pasos es 1.
WITH pasos(x) AS (VALUES (1), (1), (1))
SELECT printf('ci=%s', CASE WHEN min(x) = 1 THEN 'verde' ELSE 'rojo' END) AS resultado FROM pasos;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
SQL merece una nota aparte: como es declarativo, no piensa en "recorrer y apagar un flag", sino en agregar. min(x) sobre una columna de ceros y unos vale 1 solo si no hay ningún cero; en cuanto aparece un cero, el mínimo cae a 0. Es una forma algebraica y elegante de expresar el mismo AND: verde ⇔ mínimo = 1. Date, en SQL and Relational Theory, subraya justo esto: en el modelo relacional razonas sobre conjuntos y agregados, no sobre iteraciones paso a paso.
php/main.php · php main.php<?php
$pasos = array_map('intval', preg_split('/\s+/', trim(fgets(STDIN))));
$verde = !in_array(0, $pasos, true);
echo "ci=" . ($verde ? "verde" : "rojo") . "\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
Más allá de este ejercicio, cada lenguaje del núcleo trae su propio ecosistema de CI: el "paso de test" concreto que corre el pipeline cambia radicalmente de uno a otro, y conocerlos es parte de trabajar en un repo políglota.
| Lenguaje | Runner de test | Gestor / paquetes | Qué se cachea en CI |
|---|---|---|---|
| Python | pytest |
pip / Poetry (poetry.lock) |
~/.cache/pip, venv |
| JavaScript/TS | Jest o Vitest | npm / pnpm (*-lock) |
node_modules, store de pnpm |
| Java | JUnit | Maven / Gradle | ~/.m2, caché de Gradle |
| C# | xUnit | NuGet | ~/.nuget/packages |
| Go | go test |
go modules (go.sum) |
caché de módulos y build |
| Rust | cargo test |
Cargo (Cargo.lock) |
~/.cargo, target/ |
| C | CTest / a mano | Make / CMake | objetos compilados |
| SQL | asserts en scripts | — | — |
| PHP | PHPUnit | Composer (composer.lock) |
caché de Composer |
| Clase de diferencia | Observación entre lenguajes |
|---|---|
| Sintáctica | all/every/allMatch frente a bucle con flag booleano. |
| Semántica | Basta un paso en rojo para que el pipeline falle (AND). |
| Paradigmática | SQL usa min como agregado en lugar de recorrer. |
El AND que decide verde/rojo aparece idéntico en cada servidor de CI. GitHub Actions expresa la matriz con strategy.matrix y el fail-fast con strategy.fail-fast; GitLab CI encadena stages; Jenkins modela pipeline { stages { ... } }. En todos, el veredicto global es la conjunción de los pasos, y en todos existe la misma tentación humana —ignorar el rojo— y la misma cura: tratar la rama principal como sagrada.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 147
Detalle en reto.md.
Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 146 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 148 ⏭️