Ingeniería 12 min de lectura

Cómo se prueba un CAD

En un CAD, el peor bug es el que mueve una cara 0,2 mm sin dar ningún error. Estas son las capas de pruebas que lo buscan.

SSergio16 sept 2026
Cómo se prueba un CAD

Primera entrega de la serie «Por dentro de KapyCAD 2». KapyCAD 2 es la próxima gran versión de KapyCAD: está en desarrollo, la tenemos prevista antes de que acabe el año y los usuarios del plan fundador tendrán acceso anticipado a su beta. En esta serie contamos cómo lo estamos reconstruyendo por dentro, y empezamos por lo menos vistoso, que es también lo que hace posible todo lo demás: cómo sabemos que no hemos roto nada, en un tipo de programa donde romper algo rara vez hace ruido.

El bug que no se ve

En una web normal, un bug suele ser escandaloso. Un botón que no responde, una página en blanco, un error en rojo en la consola. Lo ves y lo arreglas.

En un CAD paramétrico los peores bugs son silenciosos. Cambias una cota arriba del árbol y el redondeo que pusiste hace un mes salta a otra arista. Abres una pieza y una medida que escribiste como 5,3 ahora vale 5,29999…: en pantalla no se nota, pero el número ya no es el que escribiste. O una cara se desplaza dos décimas. Nada se cuelga, nada se pone rojo y el modelo se ve casi igual, hasta que imprimes la pieza y el eje no entra.

Lo que escribiste5,3 mmLo que salió5,29999… mmAmpliado: un peloEn pantalla son iguales. Un test que solo mira si se cuelga da las dos por buenas.
Dos segmentos que en pantalla son idénticos. Solo uno mide lo que escribiste.

Por eso los tests del tipo «abre el archivo y comprueba que no explota» sirven de poco aquí. Un CAD que no explota pero mueve tu geometría es peor que uno que explota, porque el segundo al menos te avisa. Necesitamos tests que miren números, y muchos, sobre piezas reales.

Hemos montado varias capas de pruebas, cada una pensada para cazar un tipo distinto de bug invisible. En el diagrama van de la más barata, abajo, a la más cara, arriba.

diagram
Las capas de pruebas de un CAD: muchas y rápidas abajo, pocas y realistas arriba

De los tests unitarios hablaremos poco: son los de toda la vida, una función, una entrada, una salida esperada. Tenemos miles y son imprescindibles, pero lo interesante empieza un piso más arriba.

Documentos de referencia

La primera idea es muy sencilla: guarda piezas reales y vigila que no cambien.

Tenemos un conjunto de documentos de referencia que se abren y se regeneran enteros en Node con el mismo kernel de geometría que corre en tu navegador. De cada uno anotamos sus invariantes:

  • De cada cuerpo: volumen, área, caja envolvente, cuántas caras, aristas y vértices tiene, y si el kernel lo da por un sólido válido.
  • De cada croquis: cuántas regiones cerradas detecta y el veredicto del solver (resuelto o no, cuántos grados de libertad le quedan, qué restricciones chocan).
  • De cada operación: si se evaluó bien y, si falló, con qué código de error.

En cada cambio, el corpus se regenera y se compara con lo anotado. Volumen y área, con una tolerancia de una milésima relativa. La caja envolvente, a la millonésima de milímetro. Y todo lo que es un recuento o un estado, exacto: si una pieza tenía 26 caras y ahora tiene 27, alguien tiene que explicarlo.

Igual de importante es lo que dejamos sin anotar. Nunca guardamos las coordenadas exactas que devuelve el solver de croquis. Dónde acaba un punto en un croquis a medio definir es un detalle de implementación; cuántos grados de libertad le quedan es un hecho del documento. Si anotáramos coordenadas, cualquier mejora del solver pondría el corpus en rojo sin que nada estuviera mal.

Antes de medir nada, además, el comprobador anota la huella (un hash SHA-256) de los binarios del kernel que ha cargado, de modo que cada número queda ligado al kernel con el que se midió.

Jueces por propiedades

Los documentos de referencia tienen un límite: comparan contra una respuesta concreta, y hay preguntas que no tienen una sola.

Pongamos el solver de croquis (si no sabes qué es, lo explicamos en Cómo resolvemos tus croquis). Si cambias una cota de 20 a 35 mm en un croquis a medio definir, hay infinitas formas válidas de reacomodar los puntos. No podemos escribir «el resultado debe ser exactamente este», pero sí sabemos cosas que cualquier respuesta correcta tiene que cumplir. A esas cosas las llamamos propiedades, y a los tests que las comprueban, jueces.

Con los croquis usamos cuatro. El primero, «abrir no mueve nada», vuelve a resolver tal cual cada croquis guardado y comprueba que ningún punto se desplaza más de diez nanómetros (una cienmilésima de milímetro). Es una tolerancia muy por debajo de lo que ve una pantalla o una impresora y muy por encima del ruido de un cálculo bien convergido; lo que caza es un solver que vuelve a decidir una geometría que ya tenías. La regla que hay detrás la contamos en la segunda entrega.

El segundo, «ninguna cota da la vuelta a nada», sube y baja cada cota por una escalera de 81 valores, de muy pequeños a muy grandes, y comprueba que ningún segmento se invierte, ninguna tangencia cambia de lado, ningún arco se vuelve del revés y ningún ángulo cambia de signo.

El tercero pide un diagnóstico exacto. Cuando el solver dice «te quedan 3 grados de libertad» o «estas dos restricciones chocan», lo contrastamos con dos cálculos independientes, hechos con algoritmos distintos; si no coinciden, alguno se equivoca.

El cuarto mide la rapidez, y lo hace en dos momentos. Mientras arrastras un punto, el 95 % de los fotogramas tienen que resolverse en menos de 16 ms, y al soltar se guarda el último fotograma dibujado, sin que ningún punto se aparte de él más de diez nanómetros. Al confirmar un cambio, la resolución completa de cada croquis no puede tardar más que con el solver actual (PlaneGCS), cuyos tiempos tenemos congelados croquis a croquis.

Oráculos congelados

Reescribir código que ya funciona es el caso más delicado. Cuando portas una pieza importante de un lenguaje a otro (en nuestro caso, de TypeScript a Rust), el código nuevo tiene que hacer lo mismo que el viejo, con todas sus manías, aunque alguna no sea «correcta» en abstracto, porque hay documentos de usuarios que dependen de esas manías.

Para eso usamos el oráculo congelado:

  1. Antes de tocar nada, le pasamos al código viejo un montón de entradas reales y grabamos todo lo que responde.
  2. Escribimos el código nuevo.
  3. Le pasamos las mismas entradas y exigimos que responda lo mismo bit a bit, sin margen.
  4. Cuando todo cuadra, borramos el código viejo. La grabación se queda.
diagram
Un oráculo congelado: el código viejo responde una vez, y la grabación juzga al nuevo para siempre

En el paso 4, la grabación sobrevive al código que la produjo, y desde ese día cualquier cambio sigue comparándose con lo que hacía la versión original, aunque ya no exista.

Exigir los mismos bits tiene un motivo concreto. Uno de los pasos que actualizan documentos antiguos decide hacia dónde apunta una línea con Math.hypot, la función de JavaScript que calcula la longitud de un vector. La versión de Rust de esa función redondea distinto en el último bit, y en un croquis degenerado ese bit basta para cambiar un signo y que una línea apunte al revés. El oráculo lo vio, y ahora el código de Rust reproduce el mismo cálculo que hace JavaScript en el navegador. Sin el oráculo, ese bug habría pasado desapercibido en algún archivo raro que nadie abre, hasta que alguien lo abriera.

El control rojo

Lo que más nos ha costado aprender es que un juez que nunca falla no demuestra nada.

Imagina que escribes un test, lo ejecutas y sale verde. Eso no te dice si tu código está bien o si el test no está mirando lo que crees. Un comparador que por error compara un archivo consigo mismo siempre sale verde, y un juez que recorre una lista vacía, también. Un juez verde por accidente te da una confianza que no te has ganado.

Así que cada juez importante lleva su control rojo: un test que rompe algo a propósito y comprueba que el juez lo ve. Al oráculo de migraciones le cambiamos un campo en un único paso, y tiene que ponerse en rojo señalando ese paso. Al juez que mide el tiempo de regeneración le damos un kernel que hace un 20 % más de trabajo, y tiene que salir rojo; «parece algo más lento» no basta. Y cuando portamos una familia de operaciones, saboteamos uno a uno los sitios donde se lee un valor (lo fijamos a una constante) y comprobamos que algún juez se queja. Después deshacemos cada sabotaje y verificamos que todo vuelve a verde.

Arriba del todo, un navegador

Todo lo anterior corre en Node, sin pantalla, y es rápido. Pero tú no usas KapyCAD desde Node: lo usas en un navegador, con ratón, con prisas y a veces haciendo cosas que no esperábamos.

Por eso la punta de la pirámide son pruebas en un navegador real: gestos grabados (dibujar un rectángulo, arrastrar un punto, acotar) que se reproducen en el editor, y los tutoriales guiados, que se recorren paso a paso para comprobar que cada casilla se pone en verde. Son lentos y hay pocos, pero son los únicos que pasan por la interfaz que usas tú.

Dos lecciones

La comparación bit a bit es implacable, y eso tiene un límite: cuando un cambio es deliberado, el oráculo no lo distingue de un error. Lo vimos de lleno con el solver de croquis. Un solver nuevo, por definición, no puede dar los mismos bits que el actual, y pedírselo no tenía sentido. Así que retiramos esos oráculos y los sustituimos por los jueces por propiedades, que preguntan «¿es correcto?» en lugar de «¿es idéntico?».

La otra lección es sobre regrabar. Es tentador: el corpus sale rojo, miras por encima, «ah, es por mi cambio», regrabas y a otra cosa, y si había un bug, queda grabado como bueno. Nuestra regla es que solo se regraba cuando un cambio está pensado para mover un número, y hay que escribir qué número se movió y por qué, caso a caso.

La grabación de migraciones, la de los 6256 documentos, es el tema de la segunda entrega: cómo conseguimos que un archivo guardado con una versión anterior se abra igual, aunque el formato haya cambiado unas cincuenta veces en estos meses de desarrollo.

S

Escrito por

Sergio

Building Kapy CAD — parametric 3D modelling for 3D printing, in the browser.

Sigue leyendo

Discord