-- Regression test for descriptor-option preservation. -- -- protoc-gen-tarantool used to discard everything in *Options messages. -- The plugin now walks every populated standard field and extension and -- surfaces them on the generated descriptor as a plain Lua table named -- `options`, keyed by proto field name for standard options and by -- fully-qualified extension name (in bracket-string form) for -- extensions. Empty options remain absent (the `options` key is not -- emitted at all), so option-free protos produce byte-identical output -- to before. -- -- Coverage: -- * Message-level options (extension `(opttest_msg_tags)`, repeated). -- * Field-level options (standard `deprecated`, scalar extensions). -- * Method-level options (standard `deprecated`, scalar extension, -- message-valued extension recursing into a oneof + repeated -- nested-message sub-field — the shape google.api.http uses, the -- primary real-world consumer of this feature). -- * Service-level options are exercised on the side too. -- * Determinism: standard fields sort alphabetically before -- extensions (which sort alphabetically by full name) — the encoded -- extension keys land in a predictable order regardless of how -- protoc ranges them internally. local t = require('luatest') local fio = require('fio') local g = t.group('codegen_options') 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 PROTO_BODY = [[ syntax = "proto3"; package opttest; import "google/protobuf/descriptor.proto"; // Message-typed extension mirroring google.api.HttpRule's shape — a // `pattern` oneof, a `body` scalar, and a repeated self-reference for // additional bindings. Exercises the recursive option emitter on // nested-message + oneof + repeated-of-message in one shot, without // pulling in googleapis. message HttpRule { oneof pattern { string get = 1; string post = 2; } string body = 3; repeated HttpRule additional_bindings = 4; } extend google.protobuf.FieldOptions { string opttest_field_doc = 50000; bool opttest_field_secret = 50001; } extend google.protobuf.MessageOptions { repeated string opttest_msg_tags = 50002; } extend google.protobuf.ServiceOptions { string opttest_svc_owner = 50005; } extend google.protobuf.MethodOptions { int32 opttest_timeout_ms = 50003; HttpRule opttest_http = 50004; } message Req { option (opttest_msg_tags) = "alpha"; option (opttest_msg_tags) = "beta"; string id = 1 [ (opttest_field_doc) = "primary id", (opttest_field_secret) = true, deprecated = true ]; string note = 2; } message Resp { string out = 1; } service Demo { option (opttest_svc_owner) = "platform"; rpc Plain(Req) returns (Resp); rpc Annotated(Req) returns (Resp) { option deprecated = true; option (opttest_timeout_ms) = 5000; option (opttest_http) = { post: "/v1/demo" body: "*" additional_bindings { post: "/v2/demo" body: "*" } }; } } ]] 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 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, 'opttest.proto'), PROTO_BODY) local cmd = string.format( 'protoc --plugin=%q --tarantool_out=%q ' ..'--tarantool_opt=mode=%s,prefix=opt_%s ' ..'-I %q -I %q %q', PLUGIN, out_dir, mode, mode, proto_dir, OPTIONS_DIR, fio.pathjoin(proto_dir, 'opttest.proto')) local ok = os.execute(cmd) assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd) return out_dir, ('opt_%s.opttest.opttest_pb'):format(mode) end local function load_module(out, modname) package.path = fio.pathjoin(out, '?.lua') .. ';' .. fio.pathjoin(out, '?/init.lua') .. ';' .. package.path package.loaded[modname] = nil return require(modname) end g.before_all(function() ensure_plugin() end) for _, mode in ipairs({'full', 'runtime'}) do g['test_message_options_'..mode] = function() local out, modname = run_plugin(mode) local mod = load_module(out, modname) local opts = mod.Req_descriptor.options t.assert_type(opts, 'table', 'Req should carry options from (opttest_msg_tags)') local tags = opts['opttest.opttest_msg_tags'] t.assert_equals(tags, {'alpha', 'beta'}, 'repeated extension preserves declaration order') -- Option-free message must NOT carry an options key — keeps -- generated output byte-identical for the common case. t.assert_equals(mod.Resp_descriptor.options, nil, 'Resp has no options; the key must be absent') end g['test_field_options_'..mode] = function() local out, modname = run_plugin(mode) local mod = load_module(out, modname) -- Field id 1 carries deprecated + two custom extensions; field 2 has nothing. local id_field = mod.Req_descriptor.field_by_name.id local note_field = mod.Req_descriptor.field_by_name.note t.assert_type(id_field.options, 'table', 'id field should carry options') t.assert_equals(id_field.options.deprecated, true, 'standard option `deprecated` surfaces as bare key') t.assert_equals(id_field.options['opttest.opttest_field_doc'], 'primary id', 'string extension preserved verbatim') t.assert_equals(id_field.options['opttest.opttest_field_secret'], true, 'bool extension preserved') t.assert_equals(note_field.options, nil, 'note has no options; the key must be absent') end g['test_method_options_'..mode] = function() local out, modname = run_plugin(mode) local mod = load_module(out, modname) -- Plain has no options block; Annotated carries three. t.assert_equals(mod.Demo_service.methods.Plain.options, nil, 'Plain has no method options') local annotated = mod.Demo_service.methods.Annotated t.assert_type(annotated.options, 'table', 'Annotated should carry method options') t.assert_equals(annotated.options.deprecated, true) t.assert_equals(annotated.options['opttest.opttest_timeout_ms'], 5000) local http = annotated.options['opttest.opttest_http'] t.assert_type(http, 'table', 'message-typed extension recurses into a nested Lua table') t.assert_equals(http.post, '/v1/demo', 'oneof branch surfaces as a normal key (last-set wins)') t.assert_equals(http.body, '*') t.assert_type(http.additional_bindings, 'table') t.assert_equals(#http.additional_bindings, 1, 'repeated nested-message extension yields a Lua array') t.assert_equals(http.additional_bindings[1].post, '/v2/demo') t.assert_equals(http.additional_bindings[1].body, '*') end g['test_service_options_'..mode] = function() local out, modname = run_plugin(mode) local mod = load_module(out, modname) t.assert_type(mod.Demo_service.options, 'table') t.assert_equals(mod.Demo_service.options['opttest.opttest_svc_owner'], 'platform') end g['test_deterministic_key_order_'..mode] = function() -- The walker sorts standard fields alphabetically then extensions -- alphabetically by full name. Re-run the plugin twice and assert -- the raw file bytes match — independent of protoc's internal -- field-range order. local out1 = run_plugin(mode) local out2 = run_plugin(mode) local function slurp(path) local f = assert(io.open(path, 'rb')) local s = f:read('*a') f:close() return s end local rel = fio.pathjoin(('opt_%s'):format(mode), 'opttest', 'opttest_pb.lua') t.assert_equals(slurp(fio.pathjoin(out1, rel)), slurp(fio.pathjoin(out2, rel)), 'codegen output must be byte-identical across runs') end end