L'interfaccia del test runner


I test runner hanno la sola responsabilità di prendere una soluzione, eseguire tutti i test e restituire un output standardizzato. Tutte le interazioni con il sito di Exercism sono gestite automaticamente e non fanno parte di questa specifica.

Esecuzione

  • Un test runner dovrebbe fornire uno script eseguibile. Trovi maggiori informazioni nel file docker.md.
  • Lo script riceverà tre parametri:
    • Lo slug dell'esercizio (ad esempio two-fer).
    • Un percorso verso una directory di input (con una barra finale) che contiene i file della soluzione inviata ed eventuali altri file dell'esercizio. Questa directory va considerata di sola lettura. Tecnicamente è possibile scriverci, ma è meglio usare /tmp per i file temporanei (ad esempio per compilare i sorgenti).
    • Un percorso verso una directory di output (con una barra finale). Questa directory è scrivibile.
  • Lo script deve scrivere un file results.json nella directory di output.
  • Il test runner deve terminare con un codice di uscita 0 se è stato eseguito con successo, indipendentemente dallo stato dei test.

Tempo di esecuzione consentito

Il test runner ha a disposizione il 100% della CPU e 3 GB di memoria per una finestra di 20 secondi per ogni soluzione. Dopo 20 secondi, il processo viene interrotto e viene segnalato un timeout.

Note

Ti consigliamo vivamente di seguire il nostro documento sulle best practice per le prestazioni per ridurre il rischio di timeout.

Formato dell'output

I seguenti campi sono supportati nei file results.json:

Livello superiore

Versione

chiave: version, tipo: number, presenza: obbligatoria

version: 1, 2, 3

La versione della specifica a cui questo file aderisce:

  • 1: per i track il cui test runner non può fornire informazioni sui singoli test.
  • 2: per i track il cui test runner può fornire informazioni sui singoli test. Versione minima richiesta per i track con esercizi sui concetti.
  • 3: per i track il cui test runner può collegare singoli test a un compito.

Stato

chiave: status, tipo: string, presenza: obbligatoria

version: 1, 2, 3

I seguenti stati complessivi sono validi:

  • pass: tutti i test sono passati
  • fail: almeno un test ha lo stato fail o error
  • error: nessun test è stato eseguito (di solito significa un errore di compilazione o un errore di sintassi)

Lo stato error va usato solo se tutti i test sono andati in errore. Per i linguaggi compilati, questo di solito deriva dal fatto che il codice non riesce a compilare. Per i linguaggi interpretati, si tratta di un errore a runtime, come un errore di sintassi che impedisce al file di essere analizzato.

Messaggio

chiave: message, tipo: string, presenza: obbligatoria se status = error, oppure quando status = fail e version = 1

version: 1, 2, 3

Quando lo stato è error (nessun test è stato eseguito correttamente), va fornita la chiave message di livello superiore. Deve mostrare all'utente l'errore che si è verificato. Dato che è l'unica informazione che l'utente riceverà su come fare debug del proprio problema, deve essere il più chiaro possibile:

  • Semplifica i percorsi in modo che siano qualcosa come <solution-dir>/relative/path invece di /full/path/to, perché quest'ultimo includerebbe dati specifici di ECR che non sono utili
  • Quando è possibile o applicabile, comprimi gli stack che non appartengono al codice dell'utente
  • Non mostrare mai gli stack di chiamate senza contesto (cioè il messaggio di errore)
  • Non modificare il messaggio di errore (se possibile), perché così sarà più facile cercare l'errore

In Ruby, nel caso di un errore di sintassi, forniamo l'errore a runtime e lo stack trace. Nei linguaggi compilati, va fornito l'errore di compilazione.

Il valore di message di livello superiore è limitato a 65535 caratteri. La lunghezza massima effettiva è inferiore se il valore contiene caratteri multibyte.

Quando lo stato non è error, imposta il valore su null oppure ometti del tutto la chiave.

Test

chiave: tests, tipo: array, presenza: obbligatoria se status = fail oppure status = pass

version: 2, 3

È un array dei risultati dei test, specificati nella sezione "Per singolo test" qui sotto.

I test DEVONO essere restituiti nell'ordine in cui sono specificati nel file dei test. Per i linguaggi che eseguono i test in ordine casuale, questo può significare riordinare i risultati secondo l'ordine specificato nel file dei test.

La ragione è che agli studenti viene mostrato solo il primo fallimento, quindi è importante che venga mostrato quello giusto. Dato che di solito i test sono ordinati nel file dei test seguendo la logica del TDD, e dato che per gli esercizi di pratica gli studenti vedono il file dei test nell'editor, allineare i risultati al file dei test è fondamentale.

Per singolo test

Nome

chiave: name, tipo: string, presenza: obbligatoria

version: 2, 3

È il nome del test in un formato leggibile da una persona.

Codice del test

chiave: test_code, tipo: string, presenza: obbligatoria se l'esercizio è un esercizio sui concetti

version: 2, 3

Questo campo DEVE essere presente per gli esercizi sui concetti e DOVREBBE esserlo per gli esercizi di pratica. La differenza in questo requisito deriva dal fatto che negli esercizi sui concetti i test non vengono mostrati agli studenti, quindi risolvere l'esercizio potrebbe essere impossibile senza che venga mostrato test_code, mentre negli esercizi di pratica i test vengono mostrati.

È il corpo del comando che viene testato. Ad esempio, il seguente test 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

dovrebbe restituire un valore test_code pari a:

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

(con le interruzioni di riga sostituite da \n per rendere valido il JSON).

Stato

chiave: status, tipo: string, presenza: obbligatoria

version: 2, 3

I seguenti stati per singolo test sono validi:

  • pass: il test è passato
  • fail: il test è fallito
  • error: il test è andato in errore, cioè non ha restituito un valore

Messaggio

chiave: message, tipo: string, presenza: obbligatoria se status è fail o error

version: 2, 3

La chiave message per singolo test serve a restituire i risultati di un test il cui status è fail o error. Deve essere il più possibile leggibile da una persona. Qualunque cosa venga scritta qui sarà mostrata allo studente quando il suo test non passa. Se non c'è alcun messaggio di fallimento o di errore del test, imposta il valore su null oppure ometti del tutto la chiave. È anche consentito mostrare qui l'output della suite di test. Il valore di message non ha limiti di lunghezza.

Output

chiave: output, tipo: string, presenza: opzionale

version: 2, 3

La chiave output per singolo test dovrebbe essere usata per memorizzare e mostrare tutto ciò che un utente produce intenzionalmente per un test.

  • Dovrebbe essere allegata a tutti i risultati dei test che producono output dell'utente.
  • Dovrebbe comparire solo il contenuto prodotto manualmente dall'utente, non l'output automatico del test runner.
  • Puoi sia catturare il contenuto prodotto con i mezzi normali (ad esempio puts in Ruby, print in Python o Debug.WriteLine in C#), sia fornire un metodo che l'utente può usare (ad esempio, il test runner di Ruby mette a disposizione dell'utente un metodo debug globale che può usare, con le stesse caratteristiche del metodo puts standard).
  • L'output deve essere limitato a 500 caratteri. In questo caso sono accettabili sia troncare con un messaggio «Output troncato. Limita a 500 caratteri», sia restituire un errore.

ID del compito

chiave: task_id, tipo: number, presenza: opzionale

version: 3

Collega un test a un compito specifico tramite l'ID del compito, cioè il numero usato all'inizio dell'intestazione del compito. Collega un test a un compito solo se può essere collegato esattamente a un compito.

Al momento, solo gli esercizi sui concetti hanno compiti ben definiti a cui collegare i test, ma in futuro le cose potrebbero cambiare.

Ad esempio, considera il seguente file 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

...

Queste istruzioni definivano due compiti:

  1. Definire il tempo di cottura previsto in minuti
  2. Calcolare il tempo di cottura rimanente in minuti

Il file results.json potrebbe quindi avere una voce come questa:

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

Questo test ora è collegato al primo compito: «Definire il tempo di cottura previsto in minuti». Nota che il nome non deve necessariamente corrispondere alla descrizione del compito.

Ci sono vari modi in cui i track potrebbero implementarlo:

  • Aggiungere metadati ai test all'interno del file dei test (ad esempio usando attributi, annotazioni o commenti) e far leggere questi metadati al test runner quando esegue i test.
  • Memorizzare la corrispondenza tra nome del test e ID del compito in un file separato (come il file .meta/config.json dell'esercizio) e unire queste informazioni nel file results.json generato.

Esempi

Questi sono esempi di come può apparire un file results.json valido per le diverse versioni:

Esempio v1

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

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

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

Considerazioni su UI/UX

In caso di test fallito

Quando la soluzione di uno studente non supera un test, dovrebbe mostrare qualcosa come:

Test Code:
  <test_code>

Test Result:
  <message>

In caso di test superato

Quando la soluzione supera un test, dovrebbe mostrare qualcosa come:

Test Code:
  <test_code>

Come aggiungere metadati alla suite di test del tuo linguaggio

Tutte le strade portano a Roma e non esiste un modello prestabilito per arrivarci. Finora sono stati adottati diversi approcci:

  • File JSON ausiliari compilati manualmente, uniti ai risultati dei test durante l'esecuzione dei test.
  • Analisi statica automatizzata della suite di test, unita ai risultati dei test durante l'esecuzione dei test.
    • Si può fare tramite analisi dell'AST o analisi del testo.