sábado, 2 de marzo de 2013

La importancia del testing

Cualquiera que trabaja en un ambiente profesional sabe que en un proyecto de software se dedican tiempo y recursos planificados para testing y control de calidad en general (QA). Se requiere gente capacitada y con un sexto sentido para averiguar como reventar un sistema en dos simples pasos (mi novia trabaja como tester y creanme que es increíble la facilidad que tiene para encontrar errores que jamás se me habría ocurrido siquiera buscar). Pero también, cualquier programador que desarrolla un proyecto artesanalmente por hobbie, fuera de toda formalidad como sucede a muchísimos proyectos de software libre, sabe lo tedioso que puede ser lograr eso. Ya lo dije muchas veces, en ZinjaI, en PSeInt, y en MotoGT desarrollo a mi ritmo, lo que creo necesario, y soy accidentalmente mi propio tester cuando uso las dos primeras herramientas en mi trabajo, o cuando juego un rato con el tercero.

Pero como buen seguidor del modelo de bazar de Raymond, trato de liberar seguido y esto convierte a mis usuarios en mi mayor recurso de testing. El proceso suele ser: cambio algo, lo pruebo en mi notebook mientras lo desarrollo (una prueba para nada general), lo publico creyendo ingénuamente que les va a funcionar a todos, y luego recibo unos cuantos reportes de errores. A veces son solo detalles, otras veces burradas importantes que no debieron publicarse nunca. Estos errores pueden hacerme perder muchos usuarios, ya que se pueden llevar una muy mala primera imágen y no volver. Pero más allá de eso creo que tengo que hacer una consideración especial, principalmente para con PSeInt: los usuarios son estudiantes que recién empiezan, y el software promete facilitarles el aprendizaje, pero el estudiante por su inexperiencia podría no distinguir un error en la interpretación de un error en su algoritmo. Esto ocurre, y atenta directamente contra el objetivo del proyecto, confunde al estudiante, va en una dirección perfectamente opuesta. Y entonces es doblemente preocupante. Por eso, hace un tiempo empecé a construir de a poco un sistema muy muy básico de testing automático para el núcleo del intérprete, y de eso habla este artículo.

(imágen tomada de http://www.jamulblog.com/2012/01/coding-motto.html)

Siempre dije que la implementación incial del intérprete, esa que hice hace diez años cuando recién empezaba mi carrera, era horrible. Que andaba más o menos bien, pero que su diseño e implementación eran muy bizarros y poco flexibles. Esto fue un problema grande cada vez que quise cambiar algo. Particularmente el agregar subprocesos me obligó a reescribir una buena parte (la evaluación de expresiones y el manejo de la memoria). En el cambio, como todo código nuevo, trajo brillantes y relucientes bugs también nuevos, que hubo que detectar y corregir con el tiempo. Otros cambios los apliqué de una forma no del todo recomendable, ya que no buscaba el mejor diseño o la mejor implementación, sino lo que me obligara a cambiar lo menos posible el código existente para evitar daños colaterales. Estos "daños colaterales" (cuando cambio algo pensando en una cosa y como efecto sin querer rompo otra), son los más frecuentes en el núcleo del intérprete. Un diseño más prolijo y un código más documentado definitivamente ayudan a minimizarlos, pero no siempre alcanzan. Además, mi diseño, si bien ha mejorado mucho en los últimos años, todavía arrastra unas cuantas desiciones originales. Y por otro lado, tampoco soy un experto en el tema como para decir cual sería la mejor desición, simplemente voy aprendiendo lentamente de mis errores.

 (imágen tomada de http://dotnetslackers.com/Community/blogs/xun/archive/2011/10/07/comics-time-how-to-detect-a-geek.aspx)

Entonces, más o menos desde que empecé a pensar en serio lo de los subprocesos, empecé también a recopilar ejemplos para probar antes de publicar una nueva versión. Los ejemplos incluyen casos muy simples de cada una de las instrucciones y estructuras de control con sus variantes, y sus posibles errores que el interprete debería detectar, la evaluación de una importante variedad de expresiones correctas e incorrectas, y los pseudocódigos que los usuarios fueron enviando en sus reportes de errores. Todos ellos se encuentran en el directorio test dentro de los fuentes de PSeInt. Por cada ejemplo hay un archivo con el pseudocódigo de entrada, un archivo con las opciones que hay que pasarle al intérprete (perfiles y entradas por teclado), y otro con la salida esperada. Además, hay dos scripts de bash que uso para ejecutar todo el lote de pruebas automáticamente y detectar aquellos casos donde la salida que se obtenga no sea la correcta. De esta forma me aseguro que lo básico funcione, y que los errores ya corregidos no vuelvan a aparecer.

Lo bueno de esto es que ahora, después de un buen tiempo guardando casos de prueba, puedo animarme a hacer cambios más grandes en el intérprete sin miedo a encontrar tantos efectos colaterales. Supongo que los errores más gruesos que podrían aparecer están cubiertos por esos casi 100 ejemplos, mientras que los que puedan saltarse las pruebas seguramente serán casos poco frecuentes o muy rebuscados. Por eso en la nueva versión, reescribí la verificación de sintaxis de las expresiones (no confundir con lo que reescribí antes, verificación es ver si está bien y sino marcar los errores, evaluación es determinar cuanto da como resultado suponiendo que ya estaba bien). Esto me permite mayor flexibilidad en los operadores (ahora por ejemplo se pueden aceptar variantes tipo C++, además de los originales, como != o == para comparaciones), y un mejor diagnóstico de los errores (los mensajes originales eran muy genéricos e inspirados en mi experiencia con Basic). Veremos cómo resulta, y tal vez si esta nueva versión está llena de errores deba escribir "La importancia del testing (parte 2)".

Lo que sí queda pendiente y no se resuelve de forma tan trivial es el testing de la interfaz. Es decir, estos scripts verifican que el intérprete interprete correctamente, lo cual creo es el punto más sensible. Pero nada verifica que la interfaz (editor de pseudocódigo, editor de diagramas de flujo, ejecución paso a paso, nueva terminal) funcione siempre como se espera. Para esto, las pruebas siguen siendo manuales y tediosas. Usar VirtualBox (máquinas virtuales) me ayuda muchísimo, pero no tengo el tiempo y la paciencia para probar todo lo que debería en todos los entornos más comunes. Ayer, sin ir más lejos encontré que en un Windows Vista la fuente de la explicación detallada de la ejecución paso a paso era muy distinta a la que veo en mi máquina virtual con un Windows XP, que el tamaño del panel también era muy distinto, y que en los checkbox no puedo poner saltos de linea. La programación multiplataforma tiene esas cosas, bibliotecas portables como wxWidgets u OpenGL me aislan de los problemas más grandes, pero hay un sin número de detalles que uno no imagina hasta que no se los encuentra. Es bueno entender qué hay detrás de esas bibliotecas para entender mejor de donde vienen esos problemas y cómo solucionarlos, pero siempre estoy corriendo desde atrás en este sentido. Seguramente a quien use un Slackware 13 de 64bits PSeInt y ZinjaI les funcionarán fantásticamente bien como a mí. Pero los que usan otras distribuciones u otros sistemas operativos a menudo se llevan una idea distinta.

La misma ventana en un Windows 7 (izquierda) en mi Slackware 13 (derecha).

Este año espero trabajar más en esos detalles. Ya dije que no iba a incluir en PSeInt grandes funcionalidades nuevas más que las que presenta esta última versión, así que el resto del tiempo será para mejorar en los detalles. En otro post hablaré particularmente sobre el reporte de errores, para que el feedback que recibo sea más útil y las correcciones se aceleren. Mientras tanto, y como siempre, les pido colaboración y paciencia a los usuarios, ya que así funciona para mi este mundillo del software libre.

1 comentario: