A interface do executor de testes


Os Test Runners têm a única responsabilidade de pegar uma solução, rodar todos os testes e retornar uma saída padronizada. 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. Você pode encontrar mais informações no arquivo docker.md.
  • O script vai receber três parâmetros:
    • O slug do exercício (por exemplo, two-fer).
    • Um caminho para um diretório de entrada (com uma barra no final) contendo o(s) arquivo(s) da solução enviada e qualquer outro(s) arquivo(s) do exercício. Esse diretório deve ser considerado somente leitura. É tecnicamente possível escrever nele, mas é melhor usar /tmp para arquivos temporários (por exemplo, para compilar o código).
    • Um caminho para um diretório de saída (com uma barra no final). Esse diretório é gravável.
  • O script deve escrever um arquivo results.json no diretório de saída.
  • O runner deve terminar com um código de saída 0 se tiver rodado com sucesso, independentemente do status dos testes.

Tempo de execução permitido

O test runner recebe 100% de CPU com 3GB de memória por uma janela de 20 segundos por solução. Depois de 20 segundos, o processo é interrompido e reporta um time-out.

Note

Recomendamos fortemente seguir nosso documento de Boas Práticas de Desempenho para reduzir a chance de time-outs.

Formato de saída

Os campos a seguir são suportados em arquivos 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 à qual este arquivo obedece:

  • 1: Para trilhas cujo test runner não consegue fornecer informações sobre testes individuais.
  • 2: Para trilhas cujo test runner consegue emitir informações sobre testes individuais. Versão mínima obrigatória para trilhas com Exercícios de Conceito.
  • 3: Para trilhas cujo test runner consegue vincular testes individuais a uma tarefa.

Status

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

versão: 1, 2, 3

Os seguintes status gerais são válidos:

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

O status error deve ser usado somente se todos os testes deram erro. Para linguagens compiladas, isso geralmente é resultado de o código não conseguir compilar. Para linguagens interpretadas, isso é um erro de execução, como um erro de sintaxe que impede o arquivo 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 status é error (nenhum teste foi executado corretamente), a chave message de nível superior deve ser fornecida. Ela deve informar ao usuário o erro que ocorreu. Como é a única informação que o usuário vai receber sobre como depurar o problema, ela precisa ser o mais clara possível:

  • Simplifique os caminhos para algo como <solution-dir>/relative/path em vez de /full/path/to, pois isso vai incluir dados específicos do ECR que não ajudam em nada
  • Quando possível ou aplicável, compacte as pilhas de chamadas que não são do código do usuário
  • Nunca mostre pilhas de chamadas sem contexto (ou seja, a mensagem de erro)
  • Não altere a mensagem de erro (se possível), pois isso facilita a busca pelo erro

Em Ruby, no caso de um erro de sintaxe, nós fornecemos o erro de execução e a pilha de chamadas. Em linguagens compiladas, o erro de compilação deve ser fornecido.

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

Quando o status não for error, defina o valor como null ou omita 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 seção "Por teste" abaixo.

Os testes DEVEM ser retornados na ordem em que estão especificados no arquivo de testes. Para linguagens que executam os testes em ordem aleatória, isso pode significar reordenar os resultados conforme a ordem especificada no arquivo de testes.

A razão disso é que só a primeira falha é mostrada aos estudantes e, portanto, é importante que a falha correta seja mostrada. Como os testes geralmente estão ordenados no arquivo de testes de uma forma TDD, e como nos Exercícios de Prática os estudantes veem o arquivo de testes no editor, alinhar os resultados com o arquivo de testes é essencial.

Por teste

Nome

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

versão: 2, 3

Este é o nome do teste em um formato legível por humanos.

Código do teste

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

versão: 2, 3

Isto DEVE estar presente para os Exercícios de Conceito e DEVERIA estar presente para os Exercícios de Prática. A diferença nesse requisito vem do fato de que os estudantes não veem os testes nos Exercícios de Conceito, então pode ser impossível resolver o exercício sem que o test_code seja mostrado, enquanto nos Exercícios de Prática os testes são mostrados.

Este é o corpo do comando que está sendo 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 retornar um valor de test_code igual a:

"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).

Status

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

versão: 2, 3

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

  • pass: O teste passou
  • fail: O teste falhou
  • error: O teste deu erro, ou seja, não retornou 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 retornar os resultados de um teste com status fail ou error. Ela deve ser o mais legível por humanos possível. Tudo o que for escrito aqui será exibido ao estudante quando o teste dele não passar. Se não houver mensagem de falha nem mensagem de erro, defina o valor como null ou omita a chave por completo. Também é permitido exibir aqui a saída da suíte de testes. O valor de 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 exibir qualquer coisa que o usuário imprima deliberadamente para um teste.

  • Ela deve ser anexada a todos os resultados de teste que produzam saída do usuário.
  • Só deve ser exibido conteúdo que o usuário imprimiu manualmente, e não a saída automática do test runner.
  • Você pode capturar o conteúdo impresso por meios normais (por exemplo, puts no Ruby, print no Python ou Debug.WriteLine no C#), ou pode fornecer um método que o usuário possa usar (por exemplo, o Test Runner de Ruby fornece ao usuário um método debug disponível globalmente que ele pode usar, com as mesmas características do método puts padrão).
  • A saída deve ser limitada a 500 caracteres. Truncar com uma mensagem do tipo "A saída foi truncada. Limite a 500 caracteres" ou retornar um erro nessa situação são ambas alternativas aceitáveis.

ID da tarefa

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

versão: 3

Vincule um teste a uma tarefa específica por meio do ID da tarefa, que é o número usado no início do título da tarefa. Só vincule um teste a uma tarefa se ele puder ser vinculado a exatamente uma tarefa.

No momento, só os Exercícios de Conceito têm tarefas bem definidas às quais você pode vincular testes, mas isso pode mudar no futuro.

Por exemplo, considere o seguinte arquivo 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

...

Essas instruções definem duas tarefas:

  1. Defina o tempo esperado de forno em minutos
  2. Calcule o tempo restante de forno em minutos

O arquivo results.json poderia então ter uma entrada assim:

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

Este teste agora está vinculado à primeira tarefa: "Defina o tempo esperado de forno em minutos". Observe que o nome não precisa corresponder à descrição da tarefa.

Há várias formas de as trilhas implementarem isso:

  • Adicionar metadados aos testes dentro do arquivo de testes (por exemplo, usando atributos/anotações/comentários) e fazer o test runner ler esses metadados ao rodar os testes.
  • Armazenar o mapeamento entre nome do teste e ID da tarefa em um arquivo separado (como o arquivo .meta/config.json do exercício) e mesclar essa informação no arquivo results.json gerado.

Exemplos

Estes são exemplos de como um arquivo results.json válido pode ser nas diferentes versões:

Exemplo da v1

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

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

Questões de UI/UX

Quando um teste falha

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

Test Code:
  <test_code>

Test Result:
  <message>

Quando um teste passa

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

Test Code:
  <test_code>

Como adicionar metadados para a suíte de testes da sua linguagem

Todos os caminhos levam a Roma e não existe um padrão prescrito para chegar lá. Várias abordagens já foram adotadas até agora:

  • Arquivos JSON auxiliares compilados manualmente e mesclados com os resultados dos testes durante a execução dos testes.
  • Análise estática automatizada da suíte de testes, mesclada com os resultados dos testes durante a execução dos testes.
    • Isso pode ser feito por análise de AST ou por análise textual