A interface do executor de testes


Os Test Runners têm a única responsabilidade de receber uma solução, correr todos os testes e devolver uma saída normalizada. Todas as interações com o site do Exercism são tratadas automaticamente e não fazem parte desta especificação.

Execução

  • Um Test Runner deve fornecer um script executável. Podes encontrar mais informações no ficheiro docker.md.
  • O script recebe três parâmetros:
    • O slug do exercício (por exemplo, two-fer).
    • Um caminho para um diretório de entrada (com barra final) que contém o(s) ficheiro(s) da solução submetida e quaisquer outros ficheiros do exercício. Este diretório deve ser considerado apenas de leitura. Tecnicamente, é possível escrever nele, mas é melhor usar /tmp para ficheiros temporários (por exemplo, para compilar código-fonte).
    • Um caminho para um diretório de saída (com barra final). Este diretório permite escrita.
  • O script tem de escrever um ficheiro results.json no diretório de saída.
  • O runner tem de terminar com um código de saída 0 se tiver corrido com sucesso, independentemente do estado dos testes.

Tempo de execução permitido

O test runner dispõe de 100% de CPU e 3 GB de memória durante uma janela de 20 segundos por solução. Após 20 segundos, o processo é interrompido e é indicado um tempo limite excedido.

Note

Recomendamos vivamente que sigas o nosso documento de Boas Práticas de Desempenho para reduzir a probabilidade de tempos limite excedidos.

Formato da saída

Os seguintes campos são suportados nos ficheiros results.json:

Nível superior

Versão

chave: version, tipo: number, presença: obrigatória

versão: 1, 2, 3

A versão da especificação a que este ficheiro obedece:

  • 1: Para tracks cujo test runner não consegue fornecer informações sobre testes individuais.
  • 2: Para tracks cujo test runner consegue apresentar informações sobre testes individuais. Versão mínima obrigatória para tracks com exercícios de conceito.
  • 3: Para tracks cujo test runner consegue associar testes individuais a uma tarefa.

Estado

chave: status, tipo: string, presença: obrigatória

versão: 1, 2, 3

Os seguintes estados globais são válidos:

  • pass: Todos os testes passaram
  • fail: Pelo menos um teste tem o estado fail ou error
  • error: Nenhum teste foi executado (normalmente significa um erro de compilação ou um erro de sintaxe)

O estado error só deve ser usado se todos os testes tiverem dado erro. Em linguagens compiladas, isto resulta geralmente de o código não conseguir compilar. Em linguagens interpretadas, trata-se de um erro de execução, como um erro de sintaxe que impede o ficheiro de ser analisado.

Mensagem

chave: message, tipo: string, presença: obrigatória se status = error, ou quando status = fail e version = 1

versão: 1, 2, 3

Quando o estado é error (nenhum teste foi executado corretamente), deve ser fornecida a chave message de nível superior. Deve apresentar ao utilizador o erro que ocorreu. Como é a única informação que o utilizador recebe sobre como depurar o problema, tem de ser o mais clara possível:

  • Simplifica os caminhos para algo como <solution-dir>/relative/path em vez de /full/path/to, pois isso incluiria dados específicos do ECR que não ajudam
  • Quando possível ou aplicável, colapsa as pilhas de código que não é do utilizador
  • Nunca mostres pilhas de chamadas sem contexto (isto é, sem a mensagem de erro)
  • Não alteres a mensagem de erro (se possível), pois isso facilita a pesquisa do erro

Em Ruby, no caso de um erro de sintaxe, fornecemos o erro de execução e o rastreio da pilha. Em linguagens compiladas, deve ser fornecido o erro de compilação.

O valor message de nível superior está limitado a 65535 carateres. O comprimento máximo efetivo é menor se o valor contiver carateres multibyte.

Quando o estado não é error, define o valor como null ou omite a chave por completo.

Testes

chave: tests, tipo: array, presença: obrigatória se status = fail ou status = pass

versão: 2, 3

Este é um array com os resultados dos testes, especificado na secção «Por teste» abaixo.

Os testes DEVEM ser devolvidos pela ordem em que estão especificados no ficheiro de testes. Para linguagens que executam os testes por ordem aleatória, isto pode implicar reordenar os resultados de acordo com a ordem especificada no ficheiro de testes.

A razão para isto é que só a primeira falha é mostrada aos estudantes e, por isso, é importante que a falha correta seja mostrada. Como os testes estão geralmente ordenados no ficheiro de testes de acordo com o TDD, e como nos exercícios de prática os estudantes veem o ficheiro de testes no editor, é essencial alinhar os resultados com o ficheiro de testes.

Por teste

Nome

chave: name, tipo: string, presença: obrigatória

versão: 2, 3

É o nome do teste num formato legível por humanos.

Código do teste

chave: test_code, tipo: string, presença: obrigatória se o exercício for de conceito

versão: 2, 3

Isto DEVE estar presente nos exercícios de conceito e DEVERIA estar presente nos exercícios de prática. A diferença neste requisito vem do facto de os estudantes não verem os testes nos exercícios de conceito, pelo que resolver o exercício pode ser impossível sem que o test_code seja mostrado, ao passo que nos exercícios de prática os testes são mostrados.

É o corpo do comando que está a ser testado. Por exemplo, o seguinte teste em 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

deve devolver um valor test_code de:

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

(com as quebras de linha substituídas por \n para tornar o JSON válido).

Estado

chave: status, tipo: string, presença: obrigatória

versão: 2, 3

Os seguintes estados por teste são válidos:

  • pass: O teste passou
  • fail: O teste falhou
  • error: O teste deu erro, ou seja, não devolveu um valor

Mensagem

chave: message, tipo: string, presença: obrigatória se status for fail ou error

versão: 2, 3

A chave message por teste é usada para devolver os resultados de um teste com um status de fail ou error. Deve ser o mais legível possível para humanos. Tudo o que for escrito aqui será mostrado ao estudante quando o teste não passar. Se não houver mensagem de falha ou mensagem de erro do teste, define o valor como null ou omite a chave por completo. Também é permitido apresentar aqui a saída do conjunto de testes. O valor message não tem limite de comprimento.

Saída

chave: output, tipo: string, presença: opcional

versão: 2, 3

A chave output por teste deve ser usada para armazenar e apresentar tudo o que o utilizador produza deliberadamente para um teste.

  • Deve ser associada a todos os resultados de teste que produzam saída do utilizador.
  • Só deve ser mostrado o conteúdo que o utilizador produziu manualmente, e não a saída automática do test runner.
  • Podes capturar conteúdo que é produzido pelos meios normais (por exemplo, puts em Ruby, print em Python ou Debug.WriteLine em C#), ou podes fornecer um método que o utilizador possa usar (por exemplo, o Test Runner de Ruby disponibiliza ao utilizador um método debug global que ele pode usar, com as mesmas características do método puts padrão).
  • A saída tem de ser limitada a 500 carateres. Nesta situação, é aceitável truncar com a mensagem «A saída foi truncada. Limita a 500 carateres» ou devolver um erro.

ID da tarefa

chave: task_id, tipo: number, presença: opcional

versão: 3

Associa um teste a uma tarefa específica através do ID da tarefa, que é o número usado no início do título da tarefa. Só associa um teste a uma tarefa se for possível associá-lo a precisamente uma tarefa.

De momento, só os exercícios de conceito têm tarefas bem definidas às quais podes associar testes, mas isto pode mudar no futuro.

Por exemplo, considera o seguinte ficheiro 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 instruções definiam duas tarefas:

  1. Define o tempo esperado no forno, em minutos
  2. Calcula o tempo restante no forno, em minutos

O ficheiro results.json pode então ter uma entrada como esta:

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

Este teste está agora associado à primeira tarefa: «Define o tempo esperado no forno, em minutos». Repara que o nome não tem de corresponder à descrição da tarefa.

Há várias formas de os tracks implementarem isto:

  • Adicionar metadados aos testes dentro do ficheiro de testes (por exemplo, usando atributos/anotações/comentários) e fazer com que o test runner leia esses metadados ao correr os testes.
  • Guardar o mapeamento entre o nome do teste e o ID da tarefa num ficheiro separado (como o ficheiro .meta/config.json do exercício) e juntar essa informação ao ficheiro results.json gerado.

Exemplos

Estes são exemplos do aspeto que um ficheiro results.json válido pode ter nas diferentes versões:

Exemplo v1

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

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

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

Considerações sobre UI/UX

Em caso de falha do teste

Quando a solução de um estudante falha um teste, deve mostrar algo como:

Test Code:
  <test_code>

Test Result:
  <message>

Em caso de sucesso do teste

Quando a solução passa um teste, deve mostrar algo como:

Test Code:
  <test_code>

Como adicionar metadados ao conjunto de testes da tua linguagem

Todos os caminhos vão dar a Roma e não há um padrão prescrito para chegar a isto. Até agora, foram adotadas várias abordagens:

  • Ficheiros JSON auxiliares compilados manualmente e juntados aos resultados dos testes durante a execução dos testes.
  • Análise estática automatizada do conjunto de testes, juntada aos resultados dos testes durante a execução dos testes.
    • Isto pode ser feito através de análise de AST ou de análise textual