L'interface de l'exécuteur de tests


Les exécuteurs de tests ont pour unique responsabilité de prendre une solution, d'exécuter tous les tests et de renvoyer une sortie standardisée. Toutes les interactions avec le site d'Exercism sont gérées automatiquement et ne font pas partie de cette spécification.

Exécution

  • Un exécuteur de tests doit fournir un script exécutable. Tu trouveras plus d'informations dans le fichier docker.md.
  • Le script reçoit trois paramètres :
    • Le slug de l'exercice (par exemple two-fer).
    • Un chemin vers un répertoire d'entrée (avec un slash final) contenant le ou les fichiers de solution soumis ainsi que tout autre fichier de l'exercice. Ce répertoire doit être considéré comme en lecture seule. Il est techniquement possible d'y écrire, mais il vaut mieux utiliser /tmp pour les fichiers temporaires (par exemple pour compiler les sources).
    • Un chemin vers un répertoire de sortie (avec un slash final). Ce répertoire est accessible en écriture.
  • Le script doit écrire un fichier results.json dans le répertoire de sortie.
  • L'exécuteur doit se terminer avec un code de sortie de 0 s'il s'est exécuté correctement, quel que soit le statut des tests.

Temps d'exécution autorisé

L'exécuteur de tests dispose de 100 % du CPU et de 3 Go de mémoire pendant une fenêtre de 20 secondes par solution. Au bout de 20 secondes, le processus est arrêté et signale un dépassement de délai.

Note

Nous te recommandons vivement de suivre notre document sur les bonnes pratiques de performance pour réduire le risque de dépassement de délai.

Format de sortie

Les champs suivants sont pris en charge dans les fichiers results.json :

Niveau supérieur

Version

clé : version, type : number, présence : requise

version : 1, 2, 3

La version de la spécification à laquelle ce fichier se conforme :

  • 1 : pour les parcours dont l'exécuteur de tests ne peut pas fournir d'informations sur les tests individuels.
  • 2 : pour les parcours dont l'exécuteur de tests peut fournir des informations sur chaque test. Version minimale requise pour les parcours avec des exercices d'apprentissage.
  • 3 : pour les parcours dont l'exécuteur de tests peut associer des tests individuels à une tâche.

Statut

clé : status, type : string, présence : requise

version : 1, 2, 3

Les statuts globaux suivants sont valides :

  • pass : tous les tests ont réussi
  • fail : au moins un test a le statut fail ou error
  • error : aucun test n'a été exécuté (cela signifie généralement une erreur de compilation ou une erreur de syntaxe)

Le statut error ne doit être utilisé que si tous les tests ont produit une erreur. Pour les langages compilés, cela vient généralement du fait que le code ne peut pas être compilé. Pour les langages interprétés, il s'agit d'une erreur d'exécution, comme une erreur de syntaxe qui empêche le fichier d'être analysé.

Message

clé : message, type : string, présence : requise si status = error, ou quand status = fail et version = 1

version : 1, 2, 3

Lorsque le statut est error (aucun test n'a été exécuté correctement), la clé message de niveau supérieur doit être fournie. Elle doit présenter à l'apprenant l'erreur qui s'est produite. Comme c'est la seule information que l'apprenant recevra pour déboguer son problème, elle doit être aussi claire que possible :

  • Simplifie les chemins pour obtenir quelque chose comme <solution-dir>/relative/path plutôt que /full/path/to, car cela inclurait des données propres à l'ECR qui ne sont pas utiles
  • Quand c'est possible ou pertinent, condense les piles d'appels qui ne proviennent pas du code de l'apprenant
  • Ne montre jamais de piles d'appels sans contexte (c'est-à-dire le message d'erreur)
  • Ne modifie pas le message d'erreur (si possible), car cela facilitera la recherche de l'erreur

En Ruby, en cas d'erreur de syntaxe, on fournit l'erreur d'exécution et la trace de pile. Dans les langages compilés, on doit fournir l'erreur de compilation.

La valeur message de niveau supérieur est limitée à 65535 caractères. La longueur maximale effective est inférieure si la valeur contient des caractères multioctets.

Lorsque le statut n'est pas error, définis la valeur à null ou omets complètement la clé.

Tests

clé : tests, type : array, présence : requise si status = fail ou status = pass

version : 2, 3

Il s'agit d'un tableau des résultats de tests, décrits dans la section « Par test » ci-dessous.

Les tests DOIVENT être renvoyés dans l'ordre où ils sont spécifiés dans le fichier de tests. Pour les langages qui exécutent les tests dans un ordre aléatoire, cela peut impliquer de réordonner les résultats selon l'ordre spécifié dans le fichier de tests.

La raison est que seul le premier échec est montré aux apprenants, et il est donc important que le bon échec soit affiché. Comme les tests sont généralement ordonnés dans le fichier de tests selon une logique de TDD, et comme, pour les exercices d'entraînement, les apprenants voient le fichier de tests dans l'éditeur, il est essentiel d'aligner les résultats sur le fichier de tests.

Par test

Nom

clé : name, type : string, présence : requise

version : 2, 3

C'est le nom du test dans un format lisible par un humain.

Code du test

clé : test_code, type : string, présence : requise si l'exercice est un exercice d'apprentissage

version : 2, 3

Ce champ DOIT être présent pour les exercices d'apprentissage et DEVRAIT l'être pour les exercices d'entraînement. Cette différence de contrainte vient du fait que les tests ne sont pas montrés aux apprenants dans les exercices d'apprentissage, si bien que résoudre l'exercice peut être impossible sans que le test_code soit affiché, alors que les tests sont affichés pour les exercices d'entraînement.

C'est le corps de la commande qui est testée. Par exemple, le test Ruby suivant :

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

doit renvoyer une valeur test_code de :

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

(avec les sauts de ligne remplacés par \n pour que le JSON soit valide).

Statut

clé : status, type : string, présence : requise

version : 2, 3

Les statuts par test suivants sont valides :

  • pass : le test a réussi
  • fail : le test a échoué
  • error : le test a produit une erreur, c'est-à-dire qu'il n'a pas renvoyé de valeur

Message

clé : message, type : string, présence : requise si status est fail ou error

version : 2, 3

La clé message par test sert à renvoyer les résultats d'un test dont le status est fail ou error. Elle doit être aussi lisible que possible par un humain. Tout ce qui est écrit ici sera affiché à l'apprenant lorsque son test ne passe pas. S'il n'y a pas de message d'échec ni de message d'erreur pour le test, définis la valeur à null ou omets complètement la clé. Il est également permis d'afficher ici la sortie de la suite de tests. La valeur message n'est pas limitée en longueur.

Sortie

clé : output, type : string, présence : optionnelle

version : 2, 3

La clé output par test doit servir à stocker et à afficher tout ce qu'un apprenant produit délibérément pour un test.

  • Elle doit être attachée à tous les résultats de tests qui produisent une sortie de l'apprenant.
  • Seul le contenu affiché manuellement par un apprenant doit apparaître, et non la sortie automatique de l'exécuteur de tests.
  • Tu peux soit capturer le contenu produit par les moyens habituels (par exemple puts en Ruby, print en Python ou Debug.WriteLine en C#), soit fournir une méthode que l'apprenant peut utiliser (par exemple, l'exécuteur de tests Ruby met à disposition de l'apprenant une méthode debug globale qu'il peut utiliser, et qui a les mêmes caractéristiques que la méthode puts standard).
  • La sortie doit être limitée à 500 caractères. Tu peux soit la tronquer avec un message du type « Output was truncated. Please limit to 500 chars », soit renvoyer une erreur dans cette situation.

ID de tâche

clé : task_id, type : number, présence : optionnelle

version : 3

Associe un test à une tâche précise via l'ID de cette tâche, qui est le numéro utilisé au début du titre de la tâche. N'associe un test à une tâche que s'il peut être associé précisément à une seule tâche.

Pour l'instant, seuls les exercices d'apprentissage ont des tâches bien définies auxquelles tu peux associer des tests, mais cela pourrait changer à l'avenir.

Par exemple, considère le fichier instructions.md suivant :

# 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

...

Ces instructions définissent deux tâches :

  1. Define the expected oven time in minutes
  2. Calculate the remaining oven time in minutes

Le fichier results.json pourrait alors contenir une entrée comme celle-ci :

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

Ce test est désormais associé à la première tâche : « Define the expected oven time in minutes ». Note que le nom n'a pas besoin de correspondre à la description de la tâche.

Il existe plusieurs façons dont les parcours pourraient implémenter cela :

  • Ajouter des métadonnées aux tests dans le fichier de tests (par exemple à l'aide d'attributs, d'annotations ou de commentaires) et faire en sorte que l'exécuteur de tests lise ces métadonnées au moment de lancer les tests.
  • Stocker la correspondance entre le nom du test et l'ID de la tâche dans un fichier séparé (comme le fichier .meta/config.json de l'exercice) et fusionner ces informations dans le fichier results.json généré.

Exemples

Voici des exemples de ce à quoi peut ressembler un fichier results.json valide pour les différentes versions :

Exemple v1

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

Exemple 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()"
    }
  ]
}

Exemple 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
    }
  ]
}

Considérations UI/UX

En cas d'échec d'un test

Lorsqu'un test échoue sur la solution d'un apprenant, il convient d'afficher quelque chose comme :

Test Code:
  <test_code>

Test Result:
  <message>

En cas de réussite d'un test

Lorsqu'un test réussit sur la solution, il convient d'afficher quelque chose comme :

Test Code:
  <test_code>

Comment ajouter des métadonnées pour la suite de tests de ton langage

Tous les chemins mènent à Rome et il n'y a pas de méthode imposée pour y parvenir. Plusieurs approches ont été adoptées jusqu'à présent :

  • Des fichiers JSON auxiliaires compilés manuellement, fusionnés avec les résultats de tests au moment de l'exécution.
  • Une analyse statique automatisée de la suite de tests, fusionnée avec les résultats de tests au moment de l'exécution.
    • Cela peut être réalisé par une analyse de l'AST ou par l'analyse du texte