Una herramienta para escribir artículos Markdown acerca de Clojure implementada en Clojure (1 / N)
En Agilogy somos pragmáticos y nos gusta serlo. Por esa razón, automatizamos cuanto podemos tareas repetitivas y buscamos soluciones ágiles y creativas a problemas conocidos. Esa es la razón de que nuestra web sea estática y generada automáticamente a partir de páginas y artículos de blog escritos en Markdown.
En esta serie de artículos me propongo desarrollar y explicar a la vez una herramienta para escribir artículos acerca de Clojure. Mi objetivo es, en primer lugar, practicar con Clojure, poniéndome un reto de suficiente dificultad. En segundo lugar, espero que dicha herramienta pueda servir para publicar otros artículos acerca de Clojure.
Hay que tener en cuenta que no tengo experiencia en Clojure y, por ello, es probable que el código resultante sea muy mejorable. ¡Estaré encantado de leer sugerencias al respecto en los comentarios del blog!
Este primer artículo tan solo describe el sistema que quiero desarrollar y empieza explorando cómo leer, interpretar y ejecutar código fuente Clojure y cómo ejecutar pruebas unitarias y capturar sus resultados. No voy a escribir todavía una sola línea de código de la herramienta si no a averiguar si es posible hacerlo y cómo hacerlo.
Requisitos
El objetivo de la herramienta, si se resume al máximo, sería interpretar un artículo escrito en Markdown y con fragmentos de código Clojure. Cuando encuentre uno de dichos fragmentos de código, la herramienta debería ejecutarlo y añadir al artículo el resultado de evaluar el código.
Queremos, por ejemplo, añadir al artículo cualquier salida generada por el código en la salida estándar:
1
(println "Hello world!")
Output:
Hello world!
También se debería mostrar el resultado de evaluar la expresión:
1
(+ 2 3)
Result:
5
Necesitaremos poder usar otras librerías y que, más adelante, los fragmentos de código que las usan no fallen:
1
(use 'clojure.test)
En el caso de que el código defina una prueba unitaria, además, debería ejecutarla y añadir al artículo el resultado de dicha ejecución: Número de pruebas ejecutadas, número de errores y fallos y salida generada por las pruebas. Para ello asumiré que usamos clojure.test.
1
2
(deftest prueba-de-ejemplo
(is (= 5 (+ 2 3))))
1 tests run. 0 failures and 0 errors.
Además, en todo caso, si se produce una excepción, querremos que se notifique de la excepción producida:
1
(+ 2 (/ 3 0))
An exception of class java.lang.ArithmeticException has been thrown!
java.lang.ArithmeticException: Divide by zero
En cuanto al proceso de desarrollo, quiero usar TDD. Así que nada de escribir una sola forma de Clojure sin tener una prueba fallando antes.
En primer lugar, ¿es posible?
Cuando empecé a escribir este artículo, al que añado este apartado a posteriori, no tenía claro si sería posible y factible con los recursos de que disponía, hacer lo que intentaba hacer. Bien, la respuesta, ahora lo sé, es sí. Y como muestra, un botón: Este artículo que estás leyendo ya se ha anotado con la herramienta de la que hablo. Así que los resultados de ejecución y salida estándar que verás capturados después de cada fragmento de código Clojure los ha añadido la herramienta.
Tardaré unos cuantos artículos en completar la explicación de cómo lo logré. Espero que te resulten interesantes.
Primeras pruebas de viabilidad: Evaluar código Clojure
Leer los contenidos de un fichero y buscar fragmentos de código no parece demasiado complicado. En cambio, de lo que no tengo ni idea es de como coger un fragmento de código que tengo en un String y acabarlo evaluando, al estilo del eval de Javascript.
1
2
(deftest eval-clojure
(is (= 4 (eval "(+ 2 2)"))))
1 tests run. 1 failures and 0 errors.
FAIL in (eval-clojure) (:2) expected: (= 4 (eval "(+ 2 2)")) actual: (not (= 4 "(+ 2 2)"))
Wow! Aunque no hace exactamente lo que el de Javascript, existe un eval en Clojure. Ok, un poco de Google por ahí y tenemos que read-string nos permite leer formas de Clojure desde un string. Pero no las evalua. Para eso, nuestro amigo eval:
1
2
(deftest eval-clojure
(is (= 4 (eval (read-string "(+ 2 2)")))))
1 tests run. 0 failures and 0 errors.
¡Perfecto!
Un poco más de investigación y descubro que load-string hace exactamente lo mismo que (eval (read-string %)):
1
2
(deftest load-string-test
(is (= 4 (load-string "(+ 2 2)"))))
1 tests run. 0 failures and 0 errors.
Detección de errores de sintaxis y de ejecución
Pero, ¿y si el código en el string no es correcto? Voy a hacer algunas pruebas en el REPL. Es de esperar que lance una excepción…
1
(read-string "( no cierro parentesis")
An exception of class java.lang.RuntimeException has been thrown!
java.lang.RuntimeException: EOF while reading
¡Exacto! ¿Y si el código es sintácticamente correcto pero hay un error durante su ejecución? Probemos:
1
(eval (read-string "(a)"))
An exception of class clojure.lang.Compiler$CompilerException has been thrown!
java.lang.RuntimeException: Unable to resolve symbol: a in this context, compiling:(null:1)
Bien. Pero, aunque no haya programado ni una sola línea de código de producción no me gusta la idea de tener pruebas manuales de las que no voy a dejar constancia… Así que me escribo un par de pruebas. No son para probar el eval y el read-string de Clojure sino para documentarme a mí mismo (y a ti, querido lector) cómo funcionan.
1
2
3
(deftest eval-clojure-bad-syntax
(is (thrown-with-msg? RuntimeException #"EOF while reading"
(read-string "(sin cerrar parentesis"))))
1 tests run. 0 failures and 0 errors.
1
2
3
(deftest eval-clojure-bad-semantics
(is (thrown-with-msg? clojure.lang.Compiler$CompilerException #"Unable to resolve symbol: a in this context"
(eval (read-string "(a)")))))
1 tests run. 0 failures and 0 errors.
Ejecutar Clojure y capturar la salida en un String
Clojure escribe en la salida estándar, un Writer llamado *out* en Clojure. Necesito capturar lo que se escribe allí en un String, lo que significa, hacer un bind de ese Writer a un StringWriter. Ahora mismo no tengo presente como hacerlo. Pero buscando por ahí (Google clojure StringWriter) me encuentro que with-out-str es una macro que viene a hacer esto mismo. Prefiero no usarla (más adelante tendré que hacer lo mismo con otro canal de escritura y la macro ya no me servirá), pero examinando su código fuente, puedo ver cómo funciona:
1
2
(use 'clojure.repl)
(source with-out-str)
Output:
(defmacro with-out-str
"Evaluates exprs in a context in which *out* is bound to a fresh
StringWriter. Returns the string created by any nested printing
calls."
{:added "1.0"}
[& body]
`(let [s# (new java.io.StringWriter)]
(binding [*out* s#]
~@body
(str s#))))
Ok. La estructura es la siguiente:
1
2
3
4
(let [s (new java.io.StringWriter)]
(binding [*out* s]
(println "Hola mundo")
(str s)))
Result:
Hola mundo
¡Ops! Perdón. Dije que iba a ser 100% TDD. Escribamos una prueba de esto:
1
2
3
4
5
6
(deftest out-binding
(is (= "Hello World!"
(let [s (new java.io.StringWriter)]
(binding [*out* s]
(print "Hello World!")
(str s))))))
1 tests run. 0 failures and 0 errors.
¡Woo hoo!
Ejecutar pruebas: Ejecutar una sola prueba
Para simplificar, supondré que un fragmento de código Clojure de un artículo contendrá una única prueba o bien algo que no lo es (una expresión a evaluar). Si se trata de una prueba la querremos ejecutar y mostrar el informe típico de fallo, si es que falla. Estudiando el código fuente de cojure.test me encuentro que una prueba es una función que, si se ejecuta, escribe en *test-out* cualquier fallo de aserción que se produzca. Así que si capturo *test-out* podré ver los informes de fallos de ejecución:
1
2
3
4
5
6
(let [s (new java.io.StringWriter)]
(deftest failing-test
(is (= 2 3)))
(binding [*test-out* s]
(failing-test)
(str s)))
Result:
FAIL in (failing-test) (:3) expected: (= 2 3) actual: (not (= 2 3))
De nuevo, escribamos una prueba para documentarnos a nosotros mismos que esta idea funciona:
1
2
3
4
5
6
7
(deftest test-test
(.contains
(let [s (new java.io.StringWriter)]
(binding [*test-out* s]
(failing-test)
(str s)))
"actual: (not (= 2 3))"))
2 tests run. 1 failures and 0 errors.
Nota: Como comenté, en la versión del artículo que estás leyendo, el sistema que propongo ya está en funcionamiento y los resultados de ejecución que ves son generados por él. En particular, se genera un informe que cuenta los tests ejecutados. Aún no he explicado como lo hago. El caso es que, en este caso, da un fallo de test porque el sistema cuenta también los tests ejecutados como parte del test (en este caso, el failing-test). Por ello, aunque no hay ningún fallo en el test-test, el único test definido en este fragmento, nos indica que se han ejecutado dos tests y que ha habido un fallo pero no nos da ningún resultado de fallo. Esto solo sucede cuando se escribe un test consistente en ejecutar un test, algo poco habitual.
Informe de número de tests ejecutados, errores y fallos
Si ejecutáis pruebas unitarias en cualquier lenguaje (en Clojure, un lein test es habitual), el sistema os suele indicar el número de pruebas ejecutadas y el número de fallos (pruebas cuyas aserciones no se cumplen) y de errores (pruebas que lanzan una excepción en algun momento). ¿Cómo capturar dicha información?
De nuevo examino el código fuente de clojure.test y descubro que, además de escribir en *test-out*, las pruebas utilizan un binding a una referencia clojure.test/*report-counters*. Por ello, para capturar esos contadores, necesito hacer mi propio binding antes y ejecutar la prueba allí dentro:
1
2
3
4
5
6
7
8
(let [to (new java.io.StringWriter)]
(binding [clojure.test/*test-out* to
clojure.test/*report-counters* (ref clojure.test/*initial-report-counters*)]
(failing-test)
(println "Tests: " (:test @clojure.test/*report-counters*))
(println "Failures: " (:fail @clojure.test/*report-counters*))
(println "Errors: " (:error @clojure.test/*report-counters*))
(println "Report: " (str to))))
Output:
Tests: 1 Failures: 1 Errors: 0 Report: FAIL in (failing-test) (:3) expected: (= 2 3) actual: (not (= 2 3))
Conclusiones
En este artículo hemos planteado la posibilidad de construir una herramienta para anotar un post Markdown sobre Clojure añadiendo, tras cada fragmento de código Clojure, el resultado de su ejecución. Como primer paso, hemos averiguado la forma de compilar y ejecutar código Clojure de forma dinámica a partir del código fuente (disponible en un string). También hemos visto como capturar la salida estándar. En el caso de que el código sea una prueba, hemos visto cómo ejecutar dicha prueba y capturar los contadores de pruebas, fallos y errores y el informe de fallo de la prueba en caso de que exista.
En futuros artículos empezaremos a construir la herramienta usando TDD a partir de los resultados de la pequeña investigación en este artículo. Editaré este artículo para añadir los enlaces a las siguientes partes en cuanto las publique. Hasta entonces… disfrutad y, por favor, comentad fallos, dudas y críticas constructivas en la sección de comentarios.






