Занятие 22 мая
План занятия
- привязка сишных структур в Lua. Метатаблицы для типа userdata
- юнит-тесты. Busted. Покрытие тестами
- напутствие перед зачётом
Привязка структур из C в Lua
Предположим, у нас есть структура Point с числовыми полями x и y. Её можно реализоать на Lua (и так вначале и стоит сделать), но в программе есть очень много объектов этого типа, поэтому хочется вынести эту структуру в C, чтобы сэкономить память и ускорить функции, работающи с этой структурой, вынеся их в C. (Альтернативный сценарий: уже есть сишная структура и функции, работающие с ней, хочется получить к ним доступ из Lua.)
typedef struct Point { double x; double y; } Point;
Для хранения "сишных" данных в Lua используется тип userdata. Это единственный тип данных Lua, который можно менять напрямую из C. (Напрямую - значит не привлекая для этого функции Lua API.)
Функция lua_newuserdata принимает L и размер выделямой памяти. Она создаёт userdata указанного размера, кладёт его на стек Lua и возвращает указатель типа void* на начало куска памяти. Что делать с этой памятью - решает сишный код. Кстати, альтернативный способ использования этой функции: выделить временный кусок памяти, который не придётся удалять с помощью free, так как это сделает сборщик мусора спустя какое-то время после ухода этого объекта сос стека.
Разумно выделить память размера нашей структуры (Point) и присвоить результат функции lua_newuserdata к указателю на Point. После этого можно инициализировать поля структуры.
Point* point = lua_newuserdata(L, sizeof(Point)); point->x = x; point->y = y;
Чтобы пользоваться типом userdata, нужно уметь "добывать" из него такой указатель. Для этого служит функция lua_touserdata, которая принимает L и индекс объекта на стеке Lua:
Point* point = lua_touserdata(L, -1);
Однако такой способ довольно опасен, так как он не отслеживает соответствие типа userdata типу сишного объекта. Если нам попадётся userdata, в котором хранится какая-то другая структура, то мы запросто попортим её и вероятно получим ошибку при работе с памятью. Чтобы использовать userdata более безопасным способом, надо присвоить ему метатаблицу и при каждом получении сишного указателя проверять соответствие этой метатаблицы. Для этого существуют сишные функции luaL_newmetatable и luaL_checkudata.
luaL_newmetatable принимает L и строку-название будущей метатаблицы. Такие метатаблицы хранятся в специальном месте состояния Lua, которое названиеся Lua Registry. Если метатаблица с таким именем уже есть в Lua Registry, то функция luaL_newmetatable возвращает 0, иначе создаёт пустую таблицу, кладёт её в Lua Registry под указанным именем и возвращает 1. В обоих случаях push'ит таблицу на вершину стека. Эту таблицу надо заполнить метаметодами (ниже обсудим, какими). Каждому экземпляру этого типа после создания надо присваивать эту метатаблицу с помощью функции lua_setmetatable.
luaL_checkudata принимает L, индекс переменной на стеке и название ожидаемой метатаблицы. Если тип не совпал, порождается ошибка. Если тип совпал, то возвращается указатель, как и в случае с lua_touserdata.
Названия метатаблицам принято давать по следующей схеме: префикс модуля, подчёркивание, название типа. Пример: test_Point
В метатаблицу надо занести все метаметоды, которые мы хотим присвоить нашим объектам. дополнительные методы "живут" в подтаблице с именем __index внутри основной метатаблице. Есть важный метаметод: __gc - финализатор. Он вызывается перед удалением объекта сборщиком мусора. Если для удаления структуры надо очистить выделенную память, высвободить иные ресурсы (закрыть файлы) или произвести другие действия, это нужно делать в финализаторе. Наша структура Point не выделяет дополнительную память, поэтому нам финализатор не нужен.
#include <math.h> #include <lua.h> #include <lauxlib.h> typedef struct Point { double x; double y; } Point; int lua_point_dist(lua_State* L) { Point* p = luaL_checkudata(L, 1, "test_Point"); double d = sqrt(p->x * p->x + p->y * p->y); lua_pushnumber(L, d); return 1; } int lua_point_constructor(lua_State* L) { // object Point* p = lua_newuserdata(L, sizeof(Point)); p->x = luaL_checknumber(L, 1); p->y = luaL_checknumber(L, 2); // metatable if (luaL_newmetatable(L, "test_Point")) { lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); // mt.__intex = mt lua_pushcfunction(L, lua_point_dist); lua_setfield(L, -2, "dist"); } lua_setmetatable(L, -2); return 1; // Point object } int luaopen_point(lua_State* L) { lua_pushcfunction(L, lua_point_constructor); return 1; }
Использование:
Point = require 'point' p = Point (3, 4) p:dist() --> 5
Юнит-тесты. Busted. Покрытие тестами
Большой проект разумней писать в форме библиотеки, состоящей из простых функций, вызывающих друг друга, а "прикладную" часть держать небольшой. Помимо прочих достоинств такой схемы, это позволяет нам тестировать отдельные части кода независимо друг от друга. Такой подход называется юнит-тестами. Идеальный юнит-тест представляет собой функцию, которая может точно определить, является ли реализация какой-то функции правильной или нет. К сожалению, на практике обычно не удаётся написать юнит-тесты, которые бы выдавали на 100% гарантию корректности кода. Поэтому, если юнит-тесты прошли успешно, мы ещё не можем гарантировать, что проверяемый код корректен, однако если тесты не прошли, то где-то наверняка есть ошибка: либо в проверяемом коде, либо в юнит-тестах.
Тестирование - это такая область, в которой программирование граничит с философией. В течении критического рационализма (Карл Поппер) научной теорией считается только та теория, которая предоставляет механизм собственного опровержения. (Следствием такого определения научности теории является отстутсвие догматов, т.е. абсолютного знания.) Коротко можно сформулировать этот принцип так: "Нельзя доказать теорию, её можно только опровергнуть". Ни один эксперимент не может доказать теорию, но достаточно одного опровергающего эксперимента, чтобы отвергнуть теорию. Надежными теориями считаются теории, которые достаточное время не были опровергнуты.
Перенесем это определение на программирование - каждая функция должна предоставлять механизм собственного опровержения - юнит-тест. Юнит-тест - это аналог экспериента для программного кода. Как выше отмечалось, положительный результат юнит-теста не говорит о корректности кода, а отрицательный - говорит о некорректности. Надежным кодом считают код, который выстоял множество юнит-тестов.
Кстати, удобней тестировать чистые функции, то есть функции, которые возвращают одинаковый результат при постоянных значениях аргументов и не имеют сторонних эффектов. В этом смысле функциональные языки программирования находятся в выигрыше.
Как оценить качество юнит-тестов? Качественный критерий: насколько юнит-тесты соответствуют определению функции. В хорошем случае юнит-тесты могут служить примерами использования функции и документацией к функции. (Есть такой подход программирования Test-driven development (TDD), при котором тесты пишутся до изменения (или написания) кода и служат описанием того, что хотят от кода, фактически заменяя документацию. Затем пишут код так, чтобы он удовлетворил юнит-тестам. Повторяют этот цикл многократно.) Есть и количественный критерий: покрытие. Покрытие - это доля кода, которая исполняется в юнит-тестах. Обычно считают строки кода, которые хотя бы раз работают в юнит-тестах. Хорошим считается покрытие в 100% или почти в 100%. Если покрытие меньше, то это говорит о наличии в коде "белых пятен" - кода, который вообще никак не сказывается на результатах юнит-тестов, а следовательно, является "лакомым" местом для возникновения ошибок.
Чем ещё хороши юнит-тесты? Время от времени код претерпевает относительно большие изменения (скажем, "перетащили" медленные функции из Lua в C). Как убедиться, что при этом ничего не сломалось? Юнит-тестами! При любых изменениях кода - больших и малых - юнит-тесты являются ориентиром. Если после внесения изменений в код юнит-тесты свалились, приходится исправлять ошибки. Лучше так, чем если бы остался неправильный вариант кода и эти ошибки "всплыли" спустя месяцы или годы.
В отличие от реальных экспериентов, юнит-тесты прогоняются автоматически компьютером. Их можно перезапускать после каждого измененеия кода. Рассмотрим инструменты для написания и прогона юнит-тестов для Lua.
Busted. Программа для запуска юнит-тестов Lua. Юнит-тесты находятся в файлах с суффиксом _spec в папке spec проекта. Каждый файл определяет юнит-тесты в следующей форме:
describe("название модуля", function() it("описание функции 1", function() -- юнит-тесты для функции 1 end) it("описание функции 2", function() -- юнит-тесты для функции 2 end) end)
Юнит-тесты могут включать любой код. Проверки нужно делать с помощью assert, который расширен по сравнению со стандартным. В частности, можно написать assert.equal(ожидаемое значение, полученное значение), assert.not_equal, assert.has_error и так далее - подробнее на сайте busted.
При написании кода рекомендуется код юнит-тестов прилагать к тому же коммиту, в котором происходят изменения кода. Тем самым, для каждого коммита будут юнит-тесты, полностью соответствующие состоянию кода, что впоследствии будет удобно для поиска коммита, внесшего баг в код.
Travis - сервис, автоматически прогоняющий юнит-тесты проектов с Github после каждого push. Чтобы Travis начал это делать, нужно влогиниться аккаунтом github и подключить репозиторий. Присылает письмо на почту, если юнит-тесты не проходят.
coveralls - сервис, отображающий покрытие кода тестами. Используется в связке с Travis и Github (есть и другие варианты). Подкрашивает зеленым и красным покрытый и непокрытый код.
Для использования всего этого хозяйства с Lua есть готовый пример: lua-travis-example. Пример использования юнит-тестов представлен в зачетном задании.
Зачёт
Описание зачетного задания находится по ссылке https://github.com/LuaAndC/credits