-- Regression test for Lua-keyword proto field names. -- -- protoc-gen-tarantool used to emit bare-identifier table keys and -- `t.` / `result.` accesses for every field. When a field -- name happened to be a Lua reserved word (the in-the-wild hit is -- pprof's `repeated Function function = 5`) the generated `*_pb.lua` -- failed to load with `'(' expected near ''`. -- -- Coverage: emit a .proto where every Lua keyword is used as a field -- name, run the plugin in both codegen modes, and assert: -- * The generated module loads without error. -- * Each `M._fields[]` resolves to the same name string -- (covers the field_names table emit site). -- * Encoding `{[] = ...}` and decoding the bytes round-trips -- (covers the inline-encoder `v = t.` and inline-decoder -- `result. =` sites). local t = require('luatest') local fio = require('fio') local g = t.group('codegen_lua_keywords') local REPO_ROOT = fio.abspath(fio.pathjoin( fio.dirname(debug.getinfo(1, 'S').source:sub(2)), '..')) local OPTIONS_DIR = fio.pathjoin(REPO_ROOT, 'options') local PLUGIN = fio.pathjoin(REPO_ROOT, 'protoc-gen-tarantool') local LUA_KEYWORDS = { 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while', } local function spit(path, content) local f = assert(io.open(path, 'wb')) f:write(content) f:close() end local function ensure_plugin() if fio.path.exists(PLUGIN) then return end local cmd = string.format('cd %q && go build -o %s ./cmd/protoc-gen-tarantool', REPO_ROOT, fio.basename(PLUGIN)) assert(os.execute(cmd) == 0 or os.execute(cmd) == true, 'failed to build plugin: ' .. cmd) end -- Build a .proto whose every field name is a Lua keyword. local function build_proto() local lines = { 'syntax = "proto3";', 'package keywords_test;', 'message Inner { int32 x = 1; }', 'message Outer {', } -- Field id 1: nested message named `function` (the pprof shape). table.insert(lines, ' Inner function = 1;') -- Subsequent ids: int32 scalars named after every other Lua keyword. local id = 2 for _, kw in ipairs(LUA_KEYWORDS) do if kw ~= 'function' then table.insert(lines, string.format(' int32 %s = %d;', kw, id)) id = id + 1 end end table.insert(lines, '}') return table.concat(lines, '\n') .. '\n' end local function run_plugin(mode) local tmp = fio.tempdir() local proto_dir = fio.pathjoin(tmp, 'proto') local out_dir = fio.pathjoin(tmp, 'out') assert(fio.mkdir(proto_dir)) assert(fio.mkdir(out_dir)) spit(fio.pathjoin(proto_dir, 'kw.proto'), build_proto()) local cmd = string.format( 'protoc --plugin=%q --tarantool_out=%q ' ..'--tarantool_opt=mode=%s,prefix=kw_%s ' ..'-I %q -I %q %q', PLUGIN, out_dir, mode, mode, proto_dir, OPTIONS_DIR, fio.pathjoin(proto_dir, 'kw.proto')) local ok = os.execute(cmd) assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd) return out_dir, ('kw_%s.keywords_test.kw_pb'):format(mode) end g.before_all(function() ensure_plugin() end) for _, mode in ipairs({'full', 'runtime'}) do g['test_module_loads_with_keyword_fields_'..mode] = function() local out, modname = run_plugin(mode) local prev = package.path package.path = fio.pathjoin(out, '?.lua') .. ';' .. fio.pathjoin(out, '?/init.lua') .. ';' .. prev package.loaded[modname] = nil local ok, mod = pcall(require, modname) package.path = prev t.assert(ok, 'module failed to load: ' .. tostring(mod)) t.assert_type(mod.Outer_encode, 'function') t.assert_type(mod.Outer_decode, 'function') t.assert_type(mod.Outer_fields, 'table') end g['test_field_names_table_carries_keyword_keys_'..mode] = function() local out, modname = run_plugin(mode) local prev = package.path package.path = fio.pathjoin(out, '?.lua') .. ';' .. fio.pathjoin(out, '?/init.lua') .. ';' .. prev package.loaded[modname] = nil local mod = require(modname) package.path = prev for _, kw in ipairs(LUA_KEYWORDS) do t.assert_equals(mod.Outer_fields[kw], kw, 'M.Outer_fields["' .. kw .. '"] must round-trip the keyword') end end g['test_round_trip_keyword_fields_'..mode] = function() local out, modname = run_plugin(mode) local prev = package.path package.path = fio.pathjoin(out, '?.lua') .. ';' .. fio.pathjoin(out, '?/init.lua') .. ';' .. prev package.loaded[modname] = nil local mod = require(modname) package.path = prev -- Populate every keyword-named field. local input = { [ 'function' ] = { x = 42 } } local n = 1 for _, kw in ipairs(LUA_KEYWORDS) do if kw ~= 'function' then input[kw] = n n = n + 1 end end local bytes = mod.Outer_encode(input) local decoded = mod.Outer_decode(bytes) t.assert_equals(decoded['function'].x, 42) for k, v in pairs(input) do if k ~= 'function' then t.assert_equals(decoded[k], v, 'round-trip failed for keyword field "' .. k .. '"') end end end end