La interfaz del ejecutor de tests


Los test runners tienen la única responsabilidad de tomar una solución, ejecutar todas las pruebas y devolver una salida estandarizada. Todas las interacciones con el sitio web de Exercism se gestionan automáticamente y no forman parte de esta especificación.

Ejecución

  • Un test runner debe proporcionar un script ejecutable. Puedes encontrar más información en el archivo docker.md.
  • El script recibirá tres parámetros:
    • El slug del ejercicio (por ejemplo, two-fer).
    • Una ruta a un directorio de entrada (con una barra al final) que contiene el archivo o los archivos de la solución enviada y cualquier otro archivo del ejercicio. Este directorio debe considerarse de solo lectura. Técnicamente es posible escribir en él, pero es mejor usar /tmp para los archivos temporales (por ejemplo, para compilar el código fuente).
    • Una ruta a un directorio de salida (con una barra al final). En este directorio se puede escribir.
  • El script debe escribir un archivo results.json en el directorio de salida.
  • El runner debe terminar con un código de salida 0 si se ejecutó correctamente, sin importar el estado de las pruebas.

Tiempo de ejecución permitido

El test runner dispone del 100 % de la CPU y de 3 GB de memoria durante una ventana de 20 segundos por solución. Después de 20 segundos, el proceso se detiene y se informa de que se agotó el tiempo de espera.

Note

Te recomendamos encarecidamente seguir nuestro documento de buenas prácticas de rendimiento para reducir la probabilidad de que se agote el tiempo de espera.

Formato de salida

Los siguientes campos se admiten en los archivos results.json:

Nivel superior

Versión

clave: version, tipo: number, presencia: obligatoria

versión: 1, 2, 3

La versión de la especificación a la que se adhiere este archivo:

  • 1: para los tracks cuyo test runner no puede proporcionar información sobre pruebas individuales.
  • 2: para los tracks cuyo test runner puede generar información sobre pruebas individuales. Versión mínima requerida para los tracks con ejercicios de concepto.
  • 3: para los tracks cuyo test runner puede vincular pruebas individuales a una tarea.

Estado

clave: status, tipo: string, presencia: obligatoria

versión: 1, 2, 3

Los siguientes estados generales son válidos:

  • pass: todas las pruebas pasaron
  • fail: al menos una prueba tiene el estado fail o error
  • error: no se ejecutó ninguna prueba (normalmente esto significa un error de compilación o un error de sintaxis)

El estado error se debe usar solo si todas las pruebas dieron error. En los lenguajes compilados, esto suele ser el resultado de que el código no se puede compilar. En los lenguajes interpretados, es un error en tiempo de ejecución, como un error de sintaxis que impide que el archivo se analice.

Mensaje

clave: message, tipo: string, presencia: obligatoria si status = error, o cuando status = fail y version = 1

versión: 1, 2, 3

Cuando el estado es error (no se ejecutó correctamente ninguna prueba), se debe proporcionar la clave message de nivel superior. Debe mostrar al usuario el error que se produjo. Como es la única información que recibirá el usuario sobre cómo depurar su problema, debe ser lo más clara posible:

  • Simplifica las rutas para que sean algo como <solution-dir>/relative/path en lugar de /full/path/to, ya que eso incluiría datos específicos del ECR que no son de ayuda.
  • Cuando sea posible o corresponda, colapsa las pilas de llamadas que no pertenezcan al código del usuario.
  • Nunca muestres pilas de llamadas sin contexto (es decir, sin el mensaje de error).
  • No cambies el mensaje de error (si es posible), ya que así será más fácil buscar el error.

En Ruby, en el caso de un error de sintaxis, proporcionamos el error en tiempo de ejecución y la traza de la pila. En los lenguajes compilados, se debe proporcionar el error de compilación.

El valor message de nivel superior está limitado a 65535 caracteres. La longitud máxima efectiva es menor si el valor contiene caracteres de varios bytes.

Cuando el estado no es error, puedes establecer el valor en null u omitir la clave por completo.

Pruebas

clave: tests, tipo: array, presencia: obligatoria si status = fail o status = pass

versión: 2, 3

Este es un array con los resultados de las pruebas, especificado en la sección «Por prueba» que aparece a continuación.

Las pruebas DEBEN devolverse en el orden en que se especifican en el archivo de pruebas. En los lenguajes que ejecutan las pruebas en un orden aleatorio, esto puede implicar reordenar los resultados conforme al orden especificado en el archivo de pruebas.

La razón de esto es que a los estudiantes solo se les muestra el primer fallo, por lo que es importante que se muestre el fallo correcto. Como las pruebas suelen estar ordenadas en el archivo de pruebas siguiendo un enfoque TDD, y como en los ejercicios de práctica los estudiantes ven el archivo de pruebas en el editor, alinear los resultados con el archivo de pruebas es fundamental.

Por prueba

Nombre

clave: name, tipo: string, presencia: obligatoria

versión: 2, 3

Este es el nombre de la prueba en un formato legible para las personas.

Código de la prueba

clave: test_code, tipo: string, presencia: obligatoria si el ejercicio es un ejercicio de concepto

versión: 2, 3

Esto DEBE estar presente para los ejercicios de concepto y DEBERÍA estar presente para los ejercicios de práctica. La diferencia en este requisito se debe a que a los estudiantes no se les muestran las pruebas en los ejercicios de concepto, por lo que podría ser imposible resolverlos sin que se muestre test_code, mientras que en los ejercicios de práctica las pruebas sí se muestran.

Este es el cuerpo del comando que se está probando. Por ejemplo, la siguiente prueba de Ruby:

def test_duplicate_items_uniqs_list
  cart = ShoppingCart.new
  cart.add(:STARIC)
  cart.add(:MEDNEW)
  cart.add(:MEDNEW)
  assert_equal 'Newspaper, Rice', cart.items_list
end

debería devolver un valor test_code de:

"cart = ShoppingCart.new
cart.add(:STARIC)
cart.add(:MEDNEW)
cart.add(:MEDNEW)
assert_equal 'Newspaper, Rice', cart.items_list"

(con los saltos de línea reemplazados por \n para que el JSON sea válido).

Estado

clave: status, tipo: string, presencia: obligatoria

versión: 2, 3

Los siguientes estados por prueba son válidos:

  • pass: la prueba pasó
  • fail: la prueba falló
  • error: la prueba dio error, es decir, no devolvió un valor

Mensaje

clave: message, tipo: string, presencia: obligatoria si status es fail o error

versión: 2, 3

La clave message por prueba se usa para devolver los resultados de una prueba con un status de fail o error. Debe ser lo más legible para las personas posible. Todo lo que se escriba aquí se mostrará al estudiante cuando su prueba no pase. Si no hay ningún mensaje de fallo de prueba ni mensaje de error, puedes establecer el valor en null u omitir la clave por completo. También se permite mostrar aquí la salida del conjunto de pruebas. El valor message no tiene límite de longitud.

Salida

clave: output, tipo: string, presencia: opcional

versión: 2, 3

La clave output por prueba se debe usar para almacenar y mostrar cualquier cosa que un usuario genere deliberadamente para una prueba.

  • Se debe adjuntar a todos los resultados de pruebas que produzcan salida del usuario.
  • Solo se debe mostrar el contenido que un usuario genera manualmente, no la salida automática del test runner.
  • Puedes capturar el contenido que se genera por medios normales (por ejemplo, puts en Ruby, print en Python o Debug.WriteLine en C#), o puedes proporcionar un método que el usuario pueda usar (por ejemplo, el test runner de Ruby le proporciona al usuario un método debug disponible globalmente que puede usar, el cual tiene las mismas características que el método puts estándar).
  • La salida debe estar limitada a 500 caracteres. Se acepta tanto truncar con un mensaje de «Output was truncated. Please limit to 500 chars» como devolver un error en esta situación.

ID de la tarea

clave: task_id, tipo: number, presencia: opcional

versión: 3

Vincula una prueba a una tarea específica mediante el ID de la tarea, que es el número que aparece al inicio del encabezado de la tarea. Vincula una prueba a una tarea solo si se puede vincular con precisión a una sola tarea.

Por el momento, solo los ejercicios de concepto tienen tareas bien definidas a las que puedes vincular pruebas, pero esto podría cambiar en el futuro.

Por ejemplo, considera el siguiente archivo instructions.md:

# Instructions

You're going to write some code to help Lucian cook an exquisite lasagna from his favorite cook book.

## 1. Define the expected oven time in minutes

...

## 2. Calculate the remaining oven time in minutes

...

Estas instrucciones definen dos tareas:

  1. Define el tiempo esperado en el horno, en minutos
  2. Calcula el tiempo restante en el horno, en minutos

El archivo results.json podría entonces tener una entrada como esta:

{
  "name": "Expected oven time in minutes",
  "status": "pass",
  "task_id": 1,
  "test_code": "Assert.Equal(40, Lasagna.ExpectedMinutesInOven());"
}

Esta prueba ahora está vinculada a la primera tarea: «Define el tiempo esperado en el horno, en minutos». Ten en cuenta que el nombre no tiene que coincidir con la descripción de la tarea.

Hay varias formas en que los tracks podrían implementar esto:

  • Agregar metadatos a las pruebas dentro del archivo de pruebas (por ejemplo, usando atributos, anotaciones o comentarios) y hacer que el test runner lea estos metadatos al ejecutar las pruebas.
  • Almacenar la asignación entre el nombre de la prueba y el ID de la tarea en un archivo aparte (como el archivo .meta/config.json del ejercicio) y combinar esta información en el archivo results.json generado.

Ejemplos

Estos son ejemplos de cómo puede verse un archivo results.json válido para las distintas versiones:

Ejemplo de la v1

{
  "version": 1,
  "status": "fail",
  "message": "Failed: test_answer\nExpected: 42, actual: 3"
}

Ejemplo de la v2

{
  "version": 2,
  "status": "fail",
  "message": null,
  "tests": [
    {
      "name": "Test that the thing works",
      "status": "fail",
      "message": "Expected 42 but got 123123",
      "output": "Debugging information output by the user",
      "test_code": "assert_equal 42, answerToTheUltimateQuestion()"
    }
  ]
}

Ejemplo de la v3

{
  "version": 3,
  "status": "fail",
  "message": null,
  "tests": [
    {
      "name": "Test that the thing works",
      "status": "fail",
      "message": "Expected 42 but got 123123",
      "output": "Debugging information output by the user",
      "test_code": "assert_equal 42, answerToTheUltimateQuestion()",
      "task_id": 1
    }
  ]
}

Consideraciones sobre la interfaz y la experiencia de usuario

Cuando una prueba falla

Cuando la solución de un estudiante no pasa una prueba, se debe mostrar algo como:

Test Code:
  <test_code>

Test Result:
  <message>

Cuando una prueba pasa

Cuando la solución pasa una prueba, se debe mostrar algo como:

Test Code:
  <test_code>

Cómo agregar metadatos para el conjunto de pruebas de tu lenguaje

Todos los caminos llevan a Roma y no hay un patrón prescrito para lograrlo. Hasta ahora se han adoptado varios enfoques:

  • Archivos JSON auxiliares compilados manualmente y combinados con los resultados de las pruebas durante la ejecución.
  • Análisis estático automatizado del conjunto de pruebas, combinado con los resultados de las pruebas durante la ejecución.
    • Esto se puede lograr mediante análisis de AST o análisis de texto.