Тестирование компилятора
В настоящее время я работаю над компилятором, созданным с использованием sablecc.
Короче говоря, компилятор примет в качестве входных данных файлы спецификаций (это то, что мы анализируем) и файлы.class и обработает байт-код файлов.class, чтобы убедиться, что при запуске файлов.class любые из спецификаций не нарушается (это немного похоже на контракты jml / code! но намного мощнее).
У нас есть несколько десятков системных тестов, которые охватывают большую часть фазы анализа (связанные с обеспечением того, что спецификации имеют смысл, и что они также соответствуют файлам.class, которые они должны указывать).
Мы разделили их на два набора: действительные тесты и недействительные тесты.
Допустимые тесты состоят из файлов исходного кода, которые при компиляции нашим компилятором не должны отображать никаких ошибок / предупреждений компилятора.
Недопустимые тесты состоят из файлов исходного кода, которые при компиляции нашим компилятором должны отображать как минимум одну ошибку / предупреждение компилятора.
Это хорошо послужило нам, пока мы были на этапе анализа. Теперь вопрос в том, как проверить фазу генерации кода. В прошлом я проводил системные тесты на небольшом компиляторе, который я разработал на курсе по компиляторам. Каждый тест будет состоять из пары исходных файлов этого языка и output.txt
, При запуске теста я компилирую исходные файлы, а затем запускаю его метод main, проверяя, что результат вывода будет равен output.txt
, Конечно, все это было автоматизировано.
Теперь, имея дело с этим большим компилятором / байт-кодом-инструментарием, все не так просто. Это непросто скопировать то, что я сделал, с помощью моего простого компилятора. Я полагаю, что на этом этапе нужно отказаться от системных тестов и сосредоточиться на модульных тестах.
Как известно любому разработчику компилятора, компилятор состоит из множества посетителей. Я не слишком уверен, как приступить к их модульному тестированию. Из того, что я видел, большинство посетителей вызывают аналогичный класс, у которого есть методы, связанные с этим посетителем (я думаю, идея состояла в том, чтобы сохранить SRP для посетителей).
Есть несколько приемов, которые я могу использовать для модульного тестирования моего компилятора:
Модульное тестирование каждого из методов посетителя в отдельности. Это кажется хорошей идеей для посетителей без стеков, но выглядит как ужасная идея для посетителей, которые используют один (или более) стеков. Затем я собираюсь также провести модульное тестирование всех остальных методов из стандартных (читай, не посещающих) классов традиционным способом.
Юнит тестирование всего посетителя за один раз. То есть я создаю дерево, которое потом посещаю. В конце я проверяю, правильно ли обновлена таблица символов или нет. Меня не волнует насмешка над его зависимостями.
То же, что 2), но теперь издевается над зависимостями посетителя.
Какие другие?
У меня все еще есть проблема, что юнит-тесты будут очень тесно связаны с AST sabbleCC (что действительно ужасно).
В настоящее время мы не проводим никаких новых испытаний, но я хотел бы вернуть поезд на ходу, поскольку я уверен, что не тестирование системы - это то же самое, что кормить монстра, который рано или поздно вернется, чтобы укусить нас в прикладом, когда мы меньше всего этого ожидаем;-(
Кто-нибудь имел опыт тестирования компилятора, который мог бы дать какой-то удивительный совет о том, как действовать сейчас? Я как бы здесь потерялся!
1 ответ
Я участвую в проекте, где Java AST переведен на другой язык, OpenCL, с использованием компилятора Eclipse, и у меня есть похожие проблемы.
У меня нет волшебных решений для вас, но я поделюсь своим опытом, если это поможет.
Ваша методика тестирования с ожидаемым выходным значением (с output.txt) - это то, с чего я и начал, но это стало абсолютным кошмаром обслуживания для тестов. Когда по какой-то причине мне пришлось сменить генератор или выход (что происходило несколько раз), мне пришлось переписать все ожидаемые выходные файлы - и их было огромное количество. Я начал вообще не хотеть менять вывод из-за боязни сломать все тесты (что было плохо), но в итоге я их отбросил и вместо этого провел тестирование на полученном AST. Это означало, что я мог "свободно" проверить вывод. Например, если я хотел протестировать генерацию операторов if, я мог бы просто найти единственный-единственный оператор if в сгенерированном классе (я написал вспомогательные методы для выполнения всего этого общего AST), проверить несколько вещей об этом и быть сделано Этот тест не заботился о том, как был назван класс или были ли дополнительные аннотации или комментарии. В итоге все получилось довольно хорошо, так как тесты были более сфокусированными. Недостатком является то, что тесты были более тесно связаны с кодом, поэтому, если я когда-нибудь захочу разорвать компилятор Eclipse / библиотеку AST и использовать что-то еще, мне нужно будет переписать все мои тесты. В конце концов, потому что генерация кода со временем менялась, я был готов заплатить эту цену.
Я также в значительной степени полагаюсь на интеграционные тесты - тесты, которые фактически компилируют и запускают сгенерированный код на целевом языке. У меня было гораздо больше таких тестов, чем модульных, просто потому, что они казались более полезными и улавливали больше проблем.
Что касается тестирования посетителей, я снова провожу с ними больше тестов в стиле интеграции - получаю действительно маленький / специфический исходный файл Java, загружаю его с помощью компилятора Eclipse, запускаю одного из моих посетителей и проверяю результаты. Единственный другой способ тестирования без вызова компилятора Eclipse - это макетирование всего AST, что просто невозможно - большинство посетителей были нетривиальными и требовали полностью построенного / действительного Java AST, так как они читали аннотации из основного класса., Таким образом, большинство посетителей были тестируемыми, потому что они либо генерировали небольшие фрагменты кода OpenCL, либо создавали структуру данных, которую могли проверить модульные тесты.
Да, все мои тесты очень тесно связаны с компилятором Eclipse. Но так же актуально и программное обеспечение, которое мы пишем. Использование чего-либо еще означало бы, что мы все равно должны были бы переписать всю программу, так что это цена, которую мы очень рады заплатить. Я полагаю, что единого решения не существует - вам нужно взвесить стоимость жесткой связи в сравнении с ремонтопригодностью / простотой.
У нас также есть достаточное количество кода для тестирования, например, настройка компилятора Eclipse с настройками по умолчанию, код для извлечения узлов тела деревьев методов и т. Д. Мы стараемся сделать тесты как можно меньше (я знаю, что это наверное здравый смысл но возможно стоит упомянуть).
(Изменения / дополнения ниже в ответах на комментарии - легче читать / форматировать, чем комментарии комментариев)
"Я также в значительной степени полагаюсь на интеграционные тесты - тесты, которые фактически компилируют и запускают сгенерированный код на целевом языке" Что на самом деле делали эти тесты? Чем они отличаются от тестов output.txt?
(Отредактируйте снова: после перечитывания вопроса я понимаю, что наши подходы одинаковы, поэтому проигнорируйте это)
Вместо того, чтобы просто генерировать исходный код и сравнивать его с ожидаемым результатом, который я делал изначально, интеграционные тесты генерируют код OpenCL, компилируют его и запускают. Весь сгенерированный код производит вывод, и этот вывод затем сравнивается.
Например, у меня есть класс Java, который, если генератор работает правильно, должен генерировать код OpenCL, который суммирует значения в двух буферах и помещает значение в третий буфер. Сначала я написал бы текстовый файл с ожидаемым кодом OpenCL и сравнил его в моем тесте. Теперь интеграционный тест генерирует код, запускает его через компилятор OpenCL, запускает его, а затем тест проверяет значения.
"Что касается тестирования посетителей, я снова провожу с ними больше тестов в стиле интеграции - получаю действительно маленький / специфический исходный файл Java, загружаю его с помощью компилятора Eclipse, запускаю с ним одного из моих посетителей и проверяю результаты". с одним из ваших посетителей или подвести всех посетителей к посетителю, которого вы хотите проверить?
Большинство посетителей могли работать независимо друг от друга. По возможности, я мог работать только с посетителем, которого я тестирую, или, если есть зависимость от других, требовался минимальный набор посетителей (обычно требовался только один другой). Посетители не общаются друг с другом напрямую, а используют объекты контекста, которые передаются. Они могут быть созданы искусственно в тестах, чтобы привести вещи в известное состояние.
Другой вопрос, вы используете издевательства - вообще, в этом проекте? Кроме того, вы регулярно используете насмешки в других проектах? Я просто пытаюсь получить четкое представление о человеке, с которым я разговариваю:P
В этом проекте мы используем макеты примерно в 5% тестов, возможно, даже меньше. И я не издеваюсь по компиляторам Eclipse.
Дело в том, что с mocks мне нужно понимать, что я делаю хорошо, а с компилятором Eclipse это не так. Есть много методов посетителя, которые вызываются, и иногда я не уверен, какой из них должен быть вызван (например, вызывается ли визит ExtendedStringLiteral или визит StringLiteral для строковых литералов?) Если бы я это сделал и предположил, один или другой это может не соответствовать действительности, и программа не сможет работать, даже если тесты пройдут - не желательно. Единственное, что мы делаем, - это пара API-интерфейса процессора аннотаций, пара адаптеров компилятора Eclipse и некоторые из наших собственных базовых классов.
В других проектах, таких как Java EE, использовалось больше издевательств, но я все еще не заядлый их пользователь. Чем более определенным, понятным и предсказуемым является API, тем больше вероятность того, что я рассмотрю использование макетов.
Первые фазы нашей программы похожи на обычный компилятор. Мы извлекаем информацию из исходных файлов и заполняем (большую и сложную!) Таблицу символов. Как бы вы пошли о тестировании системы это? Теоретически, я мог бы создать тест с исходными файлами, а также symbolTable.txt (или.xml или любой другой), который содержит всю информацию о symbolTable, но это, я думаю, было бы немного сложнее сделать. Каждый из этих интеграционных тестов будет сложным для выполнения!
Я бы попробовал использовать подход тестирования маленьких кусочков таблицы символов, а не всего лота за один раз. Если бы я проверял, правильно ли построено дерево Java, у меня было бы что-то вроде:
один тест только для операторов if:
- иметь исходный код с одним методом, содержащим один оператор if
- строит символическое / дерево из этого источника
- вытащить дерево операторов только из тела метода из основного класса (провальный тест, если>1 или нет тел методов, найдены классы, узлы операторов верхнего уровня в теле метода)
- сравнить, если атрибуты узла оператора (условие, тело) программно
по крайней мере, один тест для каждого другого вида утверждения в похожем стиле.
- другие тесты, возможно, для нескольких утверждений и т. д. или все, что нужно
Этот подход представляет собой интеграционное тестирование, но каждый интеграционный тест тестирует только небольшую часть системы.
По сути, я бы старался сделать тесты как можно меньше. Большая часть тестового кода для извлечения битов дерева может быть перемещена в служебные методы, чтобы держать тестовые классы небольшими.
Я подумал, что, возможно, мне удастся создать симпатичный принтер, который будет брать таблицу символов и выводить соответствующие исходные файлы (которые, если все будет в порядке, будут такими же, как и исходные файлы). Проблема в том, что исходные файлы могут иметь вещи в другом порядке, чем то, что печатает мой симпатичный принтер. Боюсь, что при таком подходе я могу просто открыть еще одну банку с червями. Я неустанно рефакторинг частей кода, и ошибки начинают хвастаться. Мне действительно нужно несколько интеграционных тестов, чтобы держать меня на правильном пути.
Это именно тот подход, который я выбрал. Однако в моей системе порядок вещей не сильно меняется. У меня есть генераторы, которые по сути выводят код в ответ на узлы Java AST, но есть некоторая свобода в том, что генераторы могут рекурсивно называть себя. Например, генератор 'if', который срабатывает в ответ на узел AST оператора Java Java, может записать 'if (', затем попросить другие генераторы отрендерить условие, а затем написать ') {', попросить другие генераторы написать из тела, затем напишите '}'.