Тесты интеграции компилятора в Haskell

Я планирую написать небольшой компилятор игрушек в Haskell для очень простого языка, чтобы укрепить свои навыки Haskell и повеселиться при разработке нового языка. Я все еще думаю о некоторых общих решениях, и одним из самых больших открытий является то, как я буду проводить интеграционные тесты. У меня следующие требования:

  • Должно быть возможно создать список троек (вход, программа, вывод), так что мой интеграционный тест запускает мой компилятор для компиляции программы, запускает скомпилированный двоичный файл, передает ему вход и проверяет, что вывод пробег равен данному результату.
    • Должно быть возможно расширить тест интеграции на более позднем этапе для тестирования более сложных взаимодействий с программой, например. что он меняет файл или спит как минимум 20 секунд или что-то в этом роде.

У меня также есть следующие дополнительные требования:

  • Это должно быть как можно больше "от конца до конца", т.е. должно как можно больше обрабатывать wholencompiler как черный ящик, и ему не нужно иметь доступ к внутренним компонентам компилятора или что-то в этом роде.
  • Весь код должен быть написан в Haskell.
  • Было бы неплохо, если бы я получил типичную функциональность функциональности тестирования бесплатно, то есть без ее реализации. Например. зеленое сообщение "УСПЕХ" или набор сообщений об ошибках, описывающих сбои.

Я попытался найти то, что удовлетворяет мои потребности, но пока я не был успешным. Альтернативы, которые я рассматривал, следующие:

  • shunit удовлетворит все, кроме условия, что я хотел бы написать код в Haskell.
  • QuickCheck позволит мне написать все в Haskell, но, как я понимаю, он, похоже, в основном подходит для тестов, которые включают только функцию Haskell и ее результат. Поэтому мне нужно будет проверить функции в компиляторе и расслабить мое требование "от конца до конца".
  • Я мог бы просто написать программу Haskell, которая запускает компилятор в другом процессе, передает ему входную программу, а затем запускает скомпилированный код в другом процессе, передает его inpit и проверяет вывод. Это, однако, связано с большим количеством кодирования на моей стороне в ordee, чтобы реализовать все функции, которые можно получить бесплатно при использовании рамок тестирования.

Я еще не уверен, какой вариант я должен выбрать, и я все еще надеюсь, что у меня нет хорошего решения. Есть ли у вас какие-либо идеи о том, как я могу создать интеграционный тест, который удовлетворяет всем моим требованиям?

Ответ 1

Я думаю, что unsafePerformIO должен быть совершенно безопасным здесь, учитывая, что программы никогда не должны взаимодействовать ни с чем, на что зависит тестовая среда/логика, или, другими словами, компиляция и исполнение должны рассматриваться как чистая функция в контекст тестирования в контролируемой изолированной среде. И я считаю, что это действительно полезно для тестирования вашего компилятора в таких условиях. И QuickCheck должен стать вариантом даже для тестирования Blackbox. Но некоторые более ясные умы могут доказать, что я ошибаюсь.

Итак, предполагая, что ваш компилятор подобен:

type Source = String

compile :: Source -> Program

и выполнение вашей программы

data Report = Report Output TimeTaken OtherStats ...

execute :: Program -> IO Report

вы вполне можете использовать unsafePerformIO, чтобы преобразовать его в

execute' :: Program -> Report -- or perhaps 'executeUnsafe'
execute' = unsafePerformIO . execute

а затем

compileAndExec :: Source -> Report
compileAndExec = compile . execute'

и используйте это с помощью QuickCheck.


Вызывает ли execute подпроцесс, получает фактический двоичный файл, выполняет это, и т.д., или интерпретирует двоичный (или байт-код) в памяти, зависит от вас.

Но я бы рекомендовал разделить байтовый код и двоичную генерацию: таким образом вы можете протестировать компилятор отдельно от компоновщика /whatnot, что проще, а также получить интерпретатор в этом процессе.