La interfaz del ejecutor de tests


Los ejecutores de tests tienen la única responsabilidad de tomar una solución, ejecutar todos los tests 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 ejecutor de tests debe proporcionar un script ejecutable. Encontrarás 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 contenga 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 ejecutor debe terminar con un código de salida 0 si se ha ejecutado correctamente, independientemente del estado de los tests.

Tiempo de ejecución permitido

El ejecutor de tests dispone del 100 % de la CPU y 3 GB de memoria durante una ventana de 20 segundos por solución. Pasados los 20 segundos, el proceso se detiene y se informa de un tiempo de espera agotado.

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 son compatibles con los archivos results.json:

Nivel superior

Versión

clave: version, tipo: number, presencia: obligatorio

version: 1, 2, 3

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

  • 1: Para los tracks cuyo ejecutor de tests no puede proporcionar información sobre tests individuales.
  • 2: Para los tracks cuyo ejecutor de tests puede mostrar información sobre tests individuales. Versión mínima obligatoria para los tracks con ejercicios de concepto.
  • 3: Para los tracks cuyo ejecutor de tests puede vincular tests individuales a una tarea.

Estado

clave: status, tipo: string, presencia: obligatorio

version: 1, 2, 3

Los siguientes estados globales son válidos:

  • pass: Todos los tests han pasado
  • fail: Al menos un test tiene el estado fail o error
  • error: No se ha ejecutado ningún test (normalmente esto significa un error de compilación o un error de sintaxis)

El estado error solo debe usarse si todos los tests han dado error. En los lenguajes compilados, esto suele deberse a que el código no se puede compilar. En los lenguajes interpretados, se trata de 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: obligatorio si status = error, o cuando status = fail y version = 1

version: 1, 2, 3

Cuando el estado es error (no se ha ejecutado correctamente ningún test), debe proporcionarse la clave message de nivel superior. Debe proporcionar al usuario el error que se ha producido. 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 este último incluiría datos específicos de ECR que no resultan útiles
  • Siempre que sea posible o aplicable, contrae las pilas de llamadas que no formen parte del código del usuario
  • No muestres nunca pilas de llamadas sin contexto (es decir, sin el mensaje de error)
  • No modifiques el mensaje de error (si es posible), ya que así será más fácil buscar el error

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

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

Cuando el estado no sea error, asigna al valor null u omite la clave por completo.

Tests

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

version: 2, 3

Este es un array con los resultados de los tests, especificados en la sección «Por test» que aparece a continuación.

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

La razón es que a los estudiantes solo se les muestra el primer fallo y, por tanto, es importante que se muestre el fallo correcto. Dado que los tests suelen estar ordenados en el archivo de tests siguiendo el enfoque TDD, y que en los ejercicios de práctica los estudiantes ven el archivo de tests en el editor, es fundamental alinear los resultados con el archivo de tests.

Por test

Nombre

clave: name, tipo: string, presencia: obligatorio

version: 2, 3

Es el nombre del test en un formato legible para las personas.

Código del test

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

version: 2, 3

Esto DEBE estar presente en los ejercicios de concepto y DEBERÍA estar presente en los ejercicios de práctica. La diferencia en este requisito se debe a que en los ejercicios de concepto no se muestran los tests a los estudiantes, por lo que resolver el ejercicio puede ser imposible sin mostrar el test_code, mientras que en los ejercicios de práctica sí se muestran los tests.

Este es el cuerpo del comando que se está probando. Por ejemplo, el siguiente test 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 sustituidos por \n para que el JSON sea válido).

Estado

clave: status, tipo: string, presencia: obligatorio

version: 2, 3

Los siguientes estados por test son válidos:

  • pass: El test ha pasado
  • fail: El test ha fallado
  • error: El test ha dado error, es decir, no ha devuelto un valor

Mensaje

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

version: 2, 3

La clave message por test se usa para devolver los resultados de un test con un status de fail o error. Debe ser lo más legible posible para las personas. Todo lo que se escriba aquí se mostrará al estudiante cuando su test no pase. Si no hay ningún mensaje de fallo ni de error del test, asigna al valor null u omite la clave por completo. También está permitido mostrar aquí la salida de la suite de tests. El valor de message no tiene límite de longitud.

Salida

clave: output, tipo: string, presencia: opcional

version: 2, 3

La clave output por test debe usarse para almacenar y mostrar cualquier cosa que un usuario imprima deliberadamente para un test.

  • Debe adjuntarse a todos los resultados de test que produzcan salida del usuario.
  • Solo debe mostrarse el contenido que el usuario haya imprimido manualmente, no la salida automática del ejecutor de tests.
  • Puedes capturar el contenido que se imprime por los medios habituales (por ejemplo, puts en Ruby, print en Python o Debug.WriteLine en C#), o bien proporcionar un método que el usuario pueda usar (por ejemplo, el ejecutor de tests de Ruby pone a disposición del usuario un método debug global que puede usar y que tiene las mismas características que el método puts estándar).
  • La salida debe estar limitada a 500 caracteres. Se acepta tanto truncarla con un mensaje de «La salida se ha truncado. Limítala a 500 caracteres» como devolver un error en esta situación.

ID de la tarea

clave: task_id, tipo: number, presencia: opcional

version: 3

Vincula un test a una tarea concreta mediante el ID de la tarea, que es el número que aparece al principio del encabezado de la tarea. Vincula un test a una tarea solo si puede vincularse a una única tarea.

Por el momento, solo los ejercicios de concepto tienen tareas bien definidas a las que puedes vincular tests, 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 de horno esperado en minutos
  2. Calcula el tiempo de horno restante en minutos

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

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

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

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

  • Añadir metadatos a los tests dentro del archivo de tests (por ejemplo, usando atributos, anotaciones o comentarios) y hacer que el ejecutor de tests lea estos metadatos al ejecutar los tests.
  • Guardar la correspondencia entre el nombre del test y el ID de la tarea en un archivo aparte (como el archivo .meta/config.json del ejercicio) e integrar esta información en el archivo results.json generado.

Ejemplos

Estos son ejemplos de cómo puede ser 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 UI/UX

Cuando un test falla

Cuando la solución de un estudiante falla un test, debería mostrar algo como:

Test Code:
  <test_code>

Test Result:
  <message>

Cuando un test pasa

Cuando la solución pasa un test, debería mostrar algo como:

Test Code:
  <test_code>

Cómo añadir metadatos a la suite de tests de tu lenguaje

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

  • Archivos JSON auxiliares compilados manualmente, integrados con los resultados de los tests durante la ejecución de estos.
  • Análisis estático automatizado de la suite de tests, integrado con los resultados de los tests durante la ejecución de estos.
    • Esto puede lograrse mediante análisis de AST o analizando el texto