Тестирование компилятора - PullRequest
8 голосов
/ 01 августа 2011

В настоящее время я работаю над компилятором, созданным с использованием sablecc .

Короче говоря, компилятор примет в качестве входных данных оба файла спецификаций (это то, что мы анализируем) и файлы .class и обработает байт-код файлов .class, чтобы убедиться, что при запуске файлов .class любой спецификаций не нарушается (это немного похоже на контракты jml / code! но намного мощнее).

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

Мы разделили их на два набора: действительные тесты и недействительные тесты.

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

  • Недопустимые тесты состоят из файлов исходного кода, которые при компиляции нашим компилятором должны отображать как минимум одну ошибку / предупреждение компилятора.

Это хорошо послужило нам, пока мы были на этапе анализа. Теперь вопрос в том, как проверить фазу генерации кода. В прошлом я проводил системные тесты на небольшом компиляторе, который я разработал на курсе по компиляторам. Каждый тест будет состоять из пары исходных файлов на этом языке и output.txt. При запуске теста я компилировал исходные файлы, а затем запускал его метод main, проверяя, что результат вывода будет равен output.txt. Все это было автоматизировано, конечно.

Теперь, имея дело с этим большим компилятором / байт-кодом-инструментарием, все не так просто. Это непросто скопировать то, что я сделал, с помощью моего простого компилятора. Я полагаю, что на этом этапе нужно отказаться от системных тестов и сосредоточиться на модульных тестах.


Как известно любому разработчику компилятора, компилятор состоит из множества посетителей. Я не слишком уверен, как приступить к их модульному тестированию. Из того, что я видел, большинство посетителей вызывают аналогичный класс, у которого есть методы, связанные с этим посетителем (я думаю, идея состояла в том, чтобы сохранить SRP для посетителей).

Существует несколько методов, которые я могу использовать для модульного тестирования моего компилятора:

  1. Юнит-тестирование каждого из методов посетителя в отдельности. Это кажется хорошей идеей для посетителей без стеков, но выглядит ужасной идеей для посетителей, которые используют один (или более) стеков. Затем я также провожу модульное тестирование всех остальных методов из стандартных (читай, не посещающих) классов традиционным способом.

  2. Юнит-тестирование всего посетителя за один раз. То есть я создаю дерево, которое потом посещаю. В конце я проверяю, правильно ли обновлена ​​таблица символов или нет. Меня не волнует насмешка над его зависимостями.

  3. То же, что 2), но теперь высмеивает зависимости посетителя.

  4. Какие еще?

У меня все еще есть проблема в том, что юнит-тесты будут очень тесно связаны с AST sabbleCC (что действительно ужасно).


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

Кто-нибудь имел опыт тестирования компилятора, который мог бы дать какой-то удивительный совет о том, как действовать сейчас? Я как бы потерялся здесь!

1 Ответ

7 голосов
/ 01 августа 2011

Я участвую в проекте, где 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, тем больше вероятность того, что я рассмотрю использование mocks.

Первые этапы нашей программы аналогичны обычному компилятору.Мы извлекаем информацию из исходных файлов и заполняем (большую и сложную!) Таблицу символов.Как бы вы пошли о тестировании системы это?Теоретически, я мог бы создать тест с исходными файлами, а также symbolTable.txt (или .xml или любой другой), который содержит всю информацию о symbolTable, но это, я думаю, было бы немного сложнее сделать.Каждый из этих интеграционных тестов будет сложным для выполнения!

Я бы попробовал использовать метод тестирования маленьких битов таблицы символов, а не всей партии за один раз.Если бы я проверял, правильно ли построено дерево Java, у меня было бы что-то вроде:

  • один тест только для операторов if:

    • имеют исходный кодс одним методом, содержащим один оператор if
    • строит символьное / дерево из этого источника
    • извлекает дерево операторов только из тела метода из основного класса (провальный тест, если> 1 или нет тел методов, найдены классы, узлы операторов верхнего уровня в теле метода)
    • сравнить, если атрибуты узла оператора (условие, тело) программно
  • хотя бы один тест для каждого другого видаизложения в похожем стиле.

  • другие тесты, возможно, для нескольких операторов и т. Д. Или все, что нужно

Этот подход представляет собой интеграционное тестирование, но каждый интеграционный тест тестирует только небольшую частьсистема.

По сути, я бы старался сделать тесты как можно меньше.Большую часть тестового кода для извлечения кусочков дерева можно перенести в служебные методы, чтобы сделать тестовые классы небольшими.Обозначьте таблицу символов и выведите соответствующие исходные файлы (которые, если бы все было в порядке, были бы такими же, как исходные файлы).Проблема в том, что исходные файлы могут иметь вещи в другом порядке, чем то, что печатает мой симпатичный принтер.Боюсь, что при таком подходе я могу просто открыть еще одну банку с червями.Я неустанно рефакторинг частей кода, и ошибки начинают хвастаться.Мне действительно нужно несколько интеграционных тестов, чтобы держать меня в курсе.

Именно такой подход я выбрал.Однако в моей системе порядок вещей не сильно меняется.У меня есть генераторы, которые по сути выводят код в ответ на узлы Java AST, но есть некоторая свобода в том, что генераторы могут называть себя рекурсивно.Например, генератор 'if', который запускается в ответ на Java-оператор узла AST, может выписать 'if (', затем попросить другие генераторы отобразить условие, а затем написать ') {', попросить другие генераторы написатьиз тела, затем напишите '}'.

...