<< Volver

Parte 1. Testing de efectos (2 ptos.)

En esta sección van a implementar un mecanismo para hacer tests sobre efectos secundarios, en particular, sobre la impresión de caracteres (printing). La necesidad de testear efectos secundarios es muy recurrente en proyectos reales, existiendo distintas técnicas para abordarlo (p.ej. usando mocks). En esta ocasión, implementarán una solución que utiliza alcance dinámico para redirigir la salida de impresión hacia una estructura de datos.

Asegúrense de haber estudiado la introducción a SL y CL antes de proceder, en particular, la definición de CL (no usamos SL en esta pregunta).

En comparación con lo visto en clases, CL cuenta con una nueva expresión {printn <CL>} que imprime el valor de la expresión en pantalla (usando println de Racket) y retorna el valor de la expresión. Por ejemplo, ejecutar {+ 1 {printn {+ 1 2}}} debe imprimir 3, y su valor es 4.

  • Escriba tests de printn y observe que no es posible chequear (con la función test) que efectivamente se imprima, ni que los valores impresos sean los esperados.

Para poder validar los valores impresos, van a utilizar una estrategia que consiste, en esencia, en redirigir la impresión desde la salida estándar hacia un log. Luego, los tests simplemente consisten en corroborar el estado del log.

Agregando logs: primer intento

En un primer intento, van a agregar logs a través de una nueva estructura de datos y manteniendo un registro global de impresiones. En seguida, actualizarán su función de interpretación para que utilice este nuevo mecanismo.

Para esta sección y la siguiente, consideren la siguiente estructura, donde se mantiene tanto el valor de ejecución como el log de impresiones.

(deftype Result 
    (result val log))

Por otro lado, para mantener un log les recomendamos utilizar el mecanismo de cajas de Racket (documentación). A continuación les proveemos una ilustración de la API de cajas, para hacer crecer una lista:

> (define log (box '())) ;; Crea una caja con valor inicial lista vacía
> (unbox log)  ;; Abre la caja y obtiene su valor guardado
'()
> (set-box! log (cons "hola" (unbox log)))  ;; Modifica el contenido de la caja
> (set-box! log (cons "chao" (unbox log)))  ;; Modifica el contenido de la caja
> (unbox log)
'("chao" "hola")

Con estos dos elementos, van a reemplazar la función println utilizada en la interpretación de CL, de tal manera de poder rescatar la información de los valores impresos. A modo de preparación para la siguiente etapa, realice los siguientes pasos:

  • Defina una nueva función de impresión println-g que, dado un número, lo agrega a un log global (es decir, usen define para agregar un identificador global).
  • Modifique interp para que use println-g, en vez de println.
  • Defina una función interp-g, que dada una expresión, retorna un valor de tipo Result (usando interp). La función debe reiniciar el log global en cada llamada.
  • Defina tests para verificar que efectivamente es capaz de testear la salida de las impresiones.

Llegados a este punto, ¡ya son capaces de testear las impresiones de caracteres! Sin embargo, este enfoque tiene dos problemas importantes:

  1. Ya no se imprime en pantalla m(.
  2. El uso de un valor global no es adecuado en un contexto concurrente (p.ej. si se ejecutasen tests en paralelo, el log resultante sería impredecible e incorrecto).

En lo que sigue verán cómo solucionar estos puntos.

Segundo intento: alcance dinámico

Una solución a los problemas introducidos en la parte anterior es utilizar alcance dinámico. En esta sección los guiaremos en el proceso, para el cual harán uso del mecanismo de parametrización provisto por Racket. Un parámetro en Racket es una “caja”, con un contenido inicial, cuyo contenido puede ser redefinido localmente, con alcance dinámico. Además, los parámetros de Racket son thread-safe.

Para comenzar, se utiliza la función make-parameter, que permite crear un parámetro. El argumento que se pasa a make-parameter representa el valor inicial del parámetro. Note que para obtener el valor asociado al parámetro, es necesario “aplicarlo”, como si fuera una función sin argumentos.

> (define location (make-parameter "here"))
> (location)  
"here"

Luego, es posible usar la expresión parameterize para redefinir el valor del parámetro con alcance dinámico. El nuevo valor es visible durante toda la ejecución del cuerpo del parametrize. En primer lugar, se pueden definir nuevos valores para parámetros definidos anteriormente y, en seguida, se define el cuerpo donde se van a ver reflejados estos cambios. La idea es que los nuevos valores quedan acotados a este espacio solamente. Fuera de la ejecución del cuerpo de parameterize, un parámetro mantiene su valor original.

> (define (where-am-I?)
    (string-append "I am " (location)))
> (where-am-I?) 
"I am here"    
> (parameterize ([location "there"])
    (where-am-I?))   ;; solo dentro de la evaluación de este cuerpo se ve el nuevo valor
"I am there"
> (where-am-I?)  ;; fuera de la ejecución del cuerpo del parameterize, location sigue teniendo su valor original
"I am here"
  • (0.5 ptos) Modifique su intérprete para que la interpretación de printn utilice un parámetro para imprimir. Es decir, el valor inicial del parámetro debe ser la función println de Racket.
  • (1 pto) Defina una nueva función de interpretación interp-p, que dada una expresión retorna un valor de tipo Result. La función debe hacer uso de interp, pero manteniendo un log local y redefiniendo el valor del parámetro. El nuevo valor del parámetro debe ser una función que registre impresiones en el log local.
  • (0.5 ptos) Provea tests para verificar que efectivamente es capaz de corroborar la salida de las impresiones.

Ahora sí, ya debiesen ser capaces de interpretar sus expresiones en dos modalidades distintas: el modo normal, donde imprimen en pantalla, y el modo de prueba, donde registran su información en un log, lo que les permite corroborar los valores impresos.