protobuf 3.10.8 → 3.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +106 -22
- data/.rubocop.yml +30 -11
- data/.rubocop_todo.yml +462 -60
- data/CHANGES.md +31 -0
- data/Gemfile +22 -0
- data/lib/protobuf/code_generator.rb +17 -5
- data/lib/protobuf/decoder.rb +33 -2
- data/lib/protobuf/field/base_field.rb +13 -5
- data/lib/protobuf/field/base_field_object_definitions.rb +40 -6
- data/lib/protobuf/field/bool_field.rb +2 -1
- data/lib/protobuf/field/int32_field.rb +12 -0
- data/lib/protobuf/field/sint32_field.rb +10 -0
- data/lib/protobuf/field/uint32_field.rb +9 -0
- data/lib/protobuf/generators/printable.rb +10 -1
- data/lib/protobuf/message/serialization.rb +4 -4
- data/lib/protobuf/railtie.rb +48 -0
- data/lib/protobuf/rpc/application_service.rb +12 -0
- data/lib/protobuf/rpc/buffer.rb +2 -2
- data/lib/protobuf/rpc/service.rb +7 -3
- data/lib/protobuf/rpc/service_filters.rb +35 -5
- data/lib/protobuf/service_loader.rb +21 -0
- data/lib/protobuf/version.rb +1 -1
- data/lib/protobuf.rb +5 -0
- data/protobuf.gemspec +9 -27
- data/spec/encoding/packed_decoding_spec.rb +233 -0
- data/spec/encoding/wire_compat_decoding_spec.rb +113 -0
- data/spec/functional/railtie_boot_spec.rb +84 -0
- data/spec/lib/protobuf/field/bool_field_spec.rb +21 -0
- data/spec/lib/protobuf/field/string_field_spec.rb +0 -2
- data/spec/lib/protobuf/generators/service_generator_spec.rb +23 -0
- data/spec/lib/protobuf/message_spec.rb +0 -2
- data/spec/lib/protobuf/railtie_spec.rb +86 -0
- data/spec/lib/protobuf/rpc/application_service_spec.rb +57 -0
- data/spec/lib/protobuf/rpc/service_filters_inheritance_spec.rb +105 -0
- data/spec/lib/protobuf/service_loader_spec.rb +88 -0
- metadata +49 -77
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# The protobuf spec: "Protobuf parsers must be able to parse repeated fields
|
|
4
|
+
# that were compiled as packed as if they were not packed, and vice versa."
|
|
5
|
+
# https://protobuf.dev/programming-guides/encoding/#packed
|
|
6
|
+
#
|
|
7
|
+
# proto3 encoders (Go, Java, C++, google-protobuf) pack repeated scalars by
|
|
8
|
+
# default, so these fields must decode packed input although they do not
|
|
9
|
+
# declare `packed`.
|
|
10
|
+
RSpec.describe 'Decoding packed and unpacked repeated scalar fields' do
|
|
11
|
+
module PackedDecodingSpec
|
|
12
|
+
class Color < ::Protobuf::Enum
|
|
13
|
+
define :RED, 0
|
|
14
|
+
define :GREEN, 1
|
|
15
|
+
define :BLUE, 2
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Unpacked < ::Protobuf::Message
|
|
19
|
+
optional :string, :name, 15
|
|
20
|
+
repeated :int32, :r_i32, 21
|
|
21
|
+
repeated :int64, :r_i64, 22
|
|
22
|
+
repeated :uint32, :r_u32, 23
|
|
23
|
+
repeated :uint64, :r_u64, 24
|
|
24
|
+
repeated :sint32, :r_s32, 25
|
|
25
|
+
repeated :sint64, :r_s64, 26
|
|
26
|
+
repeated :bool, :r_b, 27
|
|
27
|
+
repeated ::PackedDecodingSpec::Color, :r_color, 28
|
|
28
|
+
repeated :fixed32, :r_f32, 29
|
|
29
|
+
repeated :fixed64, :r_f64, 30
|
|
30
|
+
repeated :sfixed32, :r_sf32, 31
|
|
31
|
+
repeated :sfixed64, :r_sf64, 32
|
|
32
|
+
repeated :float, :r_flt, 33
|
|
33
|
+
repeated :double, :r_dbl, 34
|
|
34
|
+
repeated :string, :r_str, 35
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Packed < ::Protobuf::Message
|
|
38
|
+
repeated :int32, :r_i32, 21, :packed => true
|
|
39
|
+
repeated :bool, :r_b, 27, :packed => true
|
|
40
|
+
repeated :double, :r_dbl, 34, :packed => true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Singular < ::Protobuf::Message
|
|
44
|
+
optional :int32, :i32, 1
|
|
45
|
+
optional :bool, :b, 7
|
|
46
|
+
optional :string, :str, 15
|
|
47
|
+
map :string, :int32, :counts, 16
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def tag(field, wire_type)
|
|
52
|
+
::Protobuf::Varint.encode((field << 3) | wire_type)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def varint(value)
|
|
56
|
+
::Protobuf::Varint.encode(value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def len(field, bytes)
|
|
60
|
+
tag(field, ::Protobuf::WireType::LENGTH_DELIMITED) + varint(bytes.bytesize) + bytes
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unpacked_varints(field, values)
|
|
64
|
+
values.map { |v| tag(field, ::Protobuf::WireType::VARINT) + varint(v) }.join.b
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Bytes made by google-protobuf 4.36.2 (proto3, so every repeated scalar is
|
|
68
|
+
# packed) for:
|
|
69
|
+
# r_i32: [1, -1, 300], r_i64: [5, -9_000_000_000],
|
|
70
|
+
# r_u32: [0, 4_294_967_295], r_u64: [18_446_744_073_709_551_615],
|
|
71
|
+
# r_s32: [-1, 2, -300], r_s64: [-5_000_000_000], r_b: [true, false, true],
|
|
72
|
+
# r_color: [GREEN, BLUE], r_f32: [7, 4_294_967_295], r_f64: [8],
|
|
73
|
+
# r_sf32: [-7], r_sf64: [-8], r_flt: [1.5, -2.25], r_dbl: [3.5, -0.125]
|
|
74
|
+
let(:google_protobuf_packed) do
|
|
75
|
+
hex = 'aa010d01ffffffffffffffffff01ac02b2010b0580ccbbbcdeffffffff01ba010600ffffffff0f' \
|
|
76
|
+
'c2010affffffffffffffffff01ca01040104d704d20105ffc7afa025da0103010001e201020102' \
|
|
77
|
+
'ea010807000000fffffffff201080800000000000000fa0104f9ffffff820208f8ffffffffffffff' \
|
|
78
|
+
'8a02080000c03f000010c09202100000000000000c40000000000000c0bf'
|
|
79
|
+
[hex].pack('H*')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe 'packed input for fields that do not declare packed' do
|
|
83
|
+
subject(:message) { PackedDecodingSpec::Unpacked.decode(google_protobuf_packed) }
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
:r_i32 => [1, -1, 300],
|
|
87
|
+
:r_i64 => [5, -9_000_000_000],
|
|
88
|
+
:r_u32 => [0, 4_294_967_295],
|
|
89
|
+
:r_u64 => [18_446_744_073_709_551_615],
|
|
90
|
+
:r_s32 => [-1, 2, -300],
|
|
91
|
+
:r_s64 => [-5_000_000_000],
|
|
92
|
+
:r_b => [true, false, true],
|
|
93
|
+
:r_f32 => [7, 4_294_967_295],
|
|
94
|
+
:r_f64 => [8],
|
|
95
|
+
:r_sf32 => [-7],
|
|
96
|
+
:r_sf64 => [-8],
|
|
97
|
+
:r_flt => [1.5, -2.25],
|
|
98
|
+
:r_dbl => [3.5, -0.125],
|
|
99
|
+
}.each do |field, expected|
|
|
100
|
+
it "decodes #{field}" do
|
|
101
|
+
expect(message.public_send(field).to_a).to eq(expected)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'decodes enums' do
|
|
106
|
+
expect(message.r_color.to_a).to eq([PackedDecodingSpec::Color::GREEN, PackedDecodingSpec::Color::BLUE])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'decodes the report case: packed bools are not all false' do
|
|
110
|
+
expect(PackedDecodingSpec::Unpacked.decode(len(27, "\x01".b)).r_b.to_a).to eq([true])
|
|
111
|
+
expect(PackedDecodingSpec::Unpacked.decode(len(27, "\x01\x01".b)).r_b.to_a).to eq([true, true])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'decodes the report case: packed int64 does not raise' do
|
|
115
|
+
expect(PackedDecodingSpec::Unpacked.decode(len(22, "\x05\x09".b)).r_i64.to_a).to eq([5, 9])
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe 'packed records' do
|
|
120
|
+
it 'concatenates a packed run that is split over two records' do
|
|
121
|
+
bytes = len(21, "\x01\x02".b) + len(15, 'x') + len(21, "\x03".b)
|
|
122
|
+
expect(PackedDecodingSpec::Unpacked.decode(bytes).r_i32.to_a).to eq([1, 2, 3])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'accepts packed and unpacked records for the same field' do
|
|
126
|
+
bytes = len(21, "\x01\x02".b) + unpacked_varints(21, [3])
|
|
127
|
+
expect(PackedDecodingSpec::Unpacked.decode(bytes).r_i32.to_a).to eq([1, 2, 3])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'adds no values for an empty packed record' do
|
|
131
|
+
expect(PackedDecodingSpec::Unpacked.decode(len(21, ''.b)).r_i32.to_a).to eq([])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'does not change other fields' do
|
|
135
|
+
bytes = len(15, 'name') + len(21, "\x01".b) + len(35, 'a') + len(35, 'b')
|
|
136
|
+
message = PackedDecodingSpec::Unpacked.decode(bytes)
|
|
137
|
+
expect([message.name, message.r_i32.to_a, message.r_str.to_a]).to eq(['name', [1], %w[a b]])
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe 'unpacked input for fields that declare packed' do
|
|
142
|
+
it 'decodes varints' do
|
|
143
|
+
bytes = unpacked_varints(21, [1, 2])
|
|
144
|
+
expect(PackedDecodingSpec::Packed.decode(bytes).r_i32.to_a).to eq([1, 2])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'decodes bools' do
|
|
148
|
+
bytes = unpacked_varints(27, [1, 0])
|
|
149
|
+
expect(PackedDecodingSpec::Packed.decode(bytes).r_b.to_a).to eq([true, false])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'decodes fixed64 values' do
|
|
153
|
+
bytes = [3.5, -0.125].map { |v| tag(34, ::Protobuf::WireType::FIXED64) + [v].pack('E') }.join.b
|
|
154
|
+
expect(PackedDecodingSpec::Packed.decode(bytes).r_dbl.to_a).to eq([3.5, -0.125])
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe 'unchanged behavior' do
|
|
159
|
+
it 'decodes unpacked input for fields that do not declare packed' do
|
|
160
|
+
bytes = unpacked_varints(21, [1, 2]) + unpacked_varints(27, [1])
|
|
161
|
+
message = PackedDecodingSpec::Unpacked.decode(bytes)
|
|
162
|
+
expect([message.r_i32.to_a, message.r_b.to_a]).to eq([[1, 2], [true]])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'decodes packed input for fields that declare packed' do
|
|
166
|
+
expect(PackedDecodingSpec::Packed.decode(len(21, "\x01\x02".b)).r_i32.to_a).to eq([1, 2])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'round-trips both declarations' do
|
|
170
|
+
unpacked = PackedDecodingSpec::Unpacked.new(:r_i32 => [1, -1], :r_b => [true, false], :r_dbl => [1.5])
|
|
171
|
+
packed = PackedDecodingSpec::Packed.new(:r_i32 => [1, -1], :r_b => [true, false], :r_dbl => [1.5])
|
|
172
|
+
expect(PackedDecodingSpec::Unpacked.decode(unpacked.encode)).to eq(unpacked)
|
|
173
|
+
expect(PackedDecodingSpec::Packed.decode(packed.encode)).to eq(packed)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'decodes across declarations: packed writer, unpacked reader and back' do
|
|
177
|
+
packed = PackedDecodingSpec::Packed.new(:r_i32 => [1, 300], :r_b => [true, false], :r_dbl => [1.5, 2.5])
|
|
178
|
+
unpacked = PackedDecodingSpec::Unpacked.decode(packed.encode)
|
|
179
|
+
expect([unpacked.r_i32.to_a, unpacked.r_b.to_a, unpacked.r_dbl.to_a]).to eq([[1, 300], [true, false], [1.5, 2.5]])
|
|
180
|
+
|
|
181
|
+
back = PackedDecodingSpec::Packed.decode(unpacked.encode)
|
|
182
|
+
expect(back).to eq(packed)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'does not change encoding' do
|
|
186
|
+
message = PackedDecodingSpec::Unpacked.new(:r_i32 => [1, 2])
|
|
187
|
+
expect(message.encode).to eq(unpacked_varints(21, [1, 2]))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe 'a record with the wrong wire type for a known field' do
|
|
192
|
+
it 'is skipped for a singular scalar field' do
|
|
193
|
+
bytes = len(1, 'xx') + len(7, "\x01".b) + len(15, 'ok')
|
|
194
|
+
message = PackedDecodingSpec::Singular.decode(bytes)
|
|
195
|
+
expect([message.i32, message.field?(:i32), message.b, message.field?(:b), message.str]).to eq([0, false, false, false, 'ok'])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'is skipped for a repeated scalar field' do
|
|
199
|
+
bytes = tag(29, ::Protobuf::WireType::VARINT) + varint(5) + len(21, "\x01".b)
|
|
200
|
+
message = PackedDecodingSpec::Unpacked.decode(bytes)
|
|
201
|
+
expect([message.r_f32.to_a, message.r_i32.to_a]).to eq([[], [1]])
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'is skipped for a repeated string field' do
|
|
205
|
+
bytes = tag(35, ::Protobuf::WireType::VARINT) + varint(5) + len(35, 'ok')
|
|
206
|
+
expect(PackedDecodingSpec::Unpacked.decode(bytes).r_str.to_a).to eq(['ok'])
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'is skipped for a map field' do
|
|
210
|
+
entry = len(1, 'a') + tag(2, ::Protobuf::WireType::VARINT) + varint(3)
|
|
211
|
+
bytes = tag(16, ::Protobuf::WireType::VARINT) + varint(5) + len(16, entry)
|
|
212
|
+
expect(PackedDecodingSpec::Singular.decode(bytes).counts.to_h).to eq('a' => 3)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
describe 'callers that do not know the wire type' do
|
|
217
|
+
it 'Decoder.decode_each_field still works with a two-argument block' do
|
|
218
|
+
seen = []
|
|
219
|
+
::Protobuf::Decoder.decode_each_field(StringIO.new(unpacked_varints(21, [7]))) { |tag, bytes| seen << [tag, bytes] }
|
|
220
|
+
expect(seen).to eq([[21, 7]])
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'BaseField#set without a wire type uses the field declaration' do
|
|
224
|
+
message = PackedDecodingSpec::Packed.new
|
|
225
|
+
PackedDecodingSpec::Packed.get_field(:r_i32).set(message, "\x01\x02".b)
|
|
226
|
+
expect(message.r_i32.to_a).to eq([1, 2])
|
|
227
|
+
|
|
228
|
+
unpacked = PackedDecodingSpec::Unpacked.new
|
|
229
|
+
PackedDecodingSpec::Unpacked.get_field(:r_i32).set(unpacked, 3)
|
|
230
|
+
expect(unpacked.r_i32.to_a).to eq([3])
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Input that other protobuf implementations decode, but that made this gem
|
|
4
|
+
# raise for the whole message.
|
|
5
|
+
RSpec.describe 'Decoding input from other protobuf implementations' do
|
|
6
|
+
module WireCompatDecodingSpec
|
|
7
|
+
class Message < ::Protobuf::Message
|
|
8
|
+
optional :int32, :i32, 1
|
|
9
|
+
optional :uint32, :u32, 2
|
|
10
|
+
optional :sint32, :s32, 3
|
|
11
|
+
repeated :int32, :r_i32, 4
|
|
12
|
+
optional :int64, :i64, 5
|
|
13
|
+
optional :string, :str, 15
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def tag(field, wire_type)
|
|
18
|
+
::Protobuf::Varint.encode((field << 3) | wire_type)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def varint(field, value)
|
|
22
|
+
tag(field, ::Protobuf::WireType::VARINT) + ::Protobuf::Varint.encode(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def len(field, bytes)
|
|
26
|
+
tag(field, ::Protobuf::WireType::LENGTH_DELIMITED) + ::Protobuf::Varint.encode(bytes.bytesize) + bytes
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def decode(bytes)
|
|
30
|
+
WireCompatDecodingSpec::Message.decode(bytes.b)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The language guide calls int32, uint32, int64, and uint64 compatible: a
|
|
34
|
+
# parser keeps the low 32 bits, as in a C++ cast.
|
|
35
|
+
describe 'a 32-bit field that gets a wider varint' do
|
|
36
|
+
it 'keeps the low 32 bits of an int32' do
|
|
37
|
+
expect(decode(varint(1, 0xffff_ffff)).i32).to eq(-1)
|
|
38
|
+
expect(decode(varint(1, 0x1_0000_0005)).i32).to eq(5)
|
|
39
|
+
expect(decode(varint(1, 0xffff_ffff_8000_0000)).i32).to eq(-2_147_483_648)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'keeps the low 32 bits of a uint32' do
|
|
43
|
+
expect(decode(varint(2, 0x1_0000_0001)).u32).to eq(1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'keeps the low 32 bits of a sint32 before the zigzag decode' do
|
|
47
|
+
# zigzag(-2) is 3; the bits above 32 are dropped.
|
|
48
|
+
expect(decode(varint(3, 0x4_0000_0003)).s32).to eq(-2)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'keeps the low 32 bits in a packed run' do
|
|
52
|
+
run = ::Protobuf::Varint.encode(0x1_0000_0007) + ::Protobuf::Varint.encode(1)
|
|
53
|
+
expect(decode(len(4, run)).r_i32.to_a).to eq([7, 1])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'decodes the other fields of the message' do
|
|
57
|
+
message = decode(varint(1, 0x1_0000_0005) + len(15, 'ok'))
|
|
58
|
+
expect([message.i32, message.str]).to eq([5, 'ok'])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'does not change values in range' do
|
|
62
|
+
message = WireCompatDecodingSpec::Message.new(
|
|
63
|
+
:i32 => -2_147_483_648, :u32 => 4_294_967_295, :s32 => -7, :r_i32 => [-1, 2_147_483_647], :i64 => -2**40,
|
|
64
|
+
)
|
|
65
|
+
expect(WireCompatDecodingSpec::Message.decode(message.encode)).to eq(message)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Groups (wire types 3 and 4) come from proto2 group fields and from
|
|
70
|
+
# editions DELIMITED encoding. This gem has no group fields, so it skips a
|
|
71
|
+
# group as an unknown field.
|
|
72
|
+
describe 'a group' do
|
|
73
|
+
let(:start_group) { tag(95, ::Protobuf::WireType::START_GROUP) }
|
|
74
|
+
let(:end_group) { tag(95, ::Protobuf::WireType::END_GROUP) }
|
|
75
|
+
|
|
76
|
+
it 'is skipped, with the fields inside it' do
|
|
77
|
+
bytes = start_group + varint(1, 9) + len(15, 'in') + end_group + varint(1, 5)
|
|
78
|
+
message = decode(bytes)
|
|
79
|
+
expect([message.i32, message.str]).to eq([5, ''])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'is skipped with every wire type inside it' do
|
|
83
|
+
inner = varint(1, 1) + tag(2, ::Protobuf::WireType::FIXED64) + ("\x00" * 8) +
|
|
84
|
+
tag(3, ::Protobuf::WireType::FIXED32) + ("\x00" * 4) + len(4, 'abc')
|
|
85
|
+
expect(decode(start_group + inner + end_group + len(15, 'ok')).str).to eq('ok')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'is skipped with groups inside it' do
|
|
89
|
+
inner_start = tag(7, ::Protobuf::WireType::START_GROUP)
|
|
90
|
+
inner_end = tag(7, ::Protobuf::WireType::END_GROUP)
|
|
91
|
+
bytes = start_group + inner_start + varint(1, 9) + inner_end + end_group + varint(1, 5)
|
|
92
|
+
expect(decode(bytes).i32).to eq(5)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'is skipped when its field number is a known field' do
|
|
96
|
+
bytes = tag(1, ::Protobuf::WireType::START_GROUP) + varint(1, 9) + tag(1, ::Protobuf::WireType::END_GROUP)
|
|
97
|
+
expect(decode(bytes).field?(:i32)).to be(false)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'raises when the group has no end' do
|
|
101
|
+
expect { decode(start_group + varint(1, 9)) }.to raise_error(::Protobuf::InvalidWireType)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'raises when the end does not match the start' do
|
|
105
|
+
bytes = start_group + tag(96, ::Protobuf::WireType::END_GROUP)
|
|
106
|
+
expect { decode(bytes) }.to raise_error(::Protobuf::InvalidWireType)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'raises for an end group without a start' do
|
|
110
|
+
expect { decode(end_group) }.to raise_error(::Protobuf::InvalidWireType)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
# Boots a real Rails app in a subprocess, with the load order that the
|
|
8
|
+
# `rpc_server` CLI uses: protobuf loads first, then the app file loads Rails.
|
|
9
|
+
RSpec.describe 'Protobuf::Railtie in a booted Rails app' do
|
|
10
|
+
let(:lib_dir) { ::File.expand_path('../../../lib', __FILE__) }
|
|
11
|
+
|
|
12
|
+
def write(root, relative_path, contents)
|
|
13
|
+
full = ::File.join(root, relative_path)
|
|
14
|
+
::FileUtils.mkdir_p(::File.dirname(full))
|
|
15
|
+
::File.write(full, contents)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build_app(root, services_path)
|
|
19
|
+
config_line = services_path ? "config.protobuf.services_path = #{services_path.inspect}" : ''
|
|
20
|
+
|
|
21
|
+
write(root, 'config/environment.rb', <<-RUBY)
|
|
22
|
+
require 'rails'
|
|
23
|
+
class RailtieBootApp < Rails::Application
|
|
24
|
+
config.root = #{root.inspect}
|
|
25
|
+
config.eager_load = false
|
|
26
|
+
config.logger = Logger.new(nil)
|
|
27
|
+
config.secret_key_base = 'x' * 64
|
|
28
|
+
#{config_line}
|
|
29
|
+
end
|
|
30
|
+
Rails.application.initialize!
|
|
31
|
+
RUBY
|
|
32
|
+
|
|
33
|
+
# app/services is a Zeitwerk root, so app/services/rpc/ is the Rpc namespace.
|
|
34
|
+
write(root, 'app/services/rpc/echo_service.rb', <<-RUBY)
|
|
35
|
+
module Rpc
|
|
36
|
+
class EchoService < ::Protobuf::Rpc::ApplicationService
|
|
37
|
+
rpc :echo, ::Protobuf::Socketrpc::Request, ::Protobuf::Socketrpc::Response
|
|
38
|
+
def echo; end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
RUBY
|
|
42
|
+
|
|
43
|
+
write(root, 'app/services/plain_object.rb', <<-RUBY)
|
|
44
|
+
class PlainObject
|
|
45
|
+
$stdout.puts 'PLAIN_OBJECT_LOADED'
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def boot(root)
|
|
51
|
+
script = <<-RUBY
|
|
52
|
+
require 'protobuf/socket'
|
|
53
|
+
require #{::File.join(root, 'config/environment').inspect}
|
|
54
|
+
puts "BEFORE_BIND=\#{::Protobuf::Rpc::Service.implemented_services.inspect}"
|
|
55
|
+
::ActiveSupport::Notifications.instrument('before_server_bind')
|
|
56
|
+
puts "AFTER_BIND=\#{::Protobuf::Rpc::Service.implemented_services.inspect}"
|
|
57
|
+
RUBY
|
|
58
|
+
|
|
59
|
+
Open3.capture2e(::RbConfig.ruby, '-I', lib_dir, '-e', script, :chdir => root)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'loads the configured services directory when the server binds' do
|
|
63
|
+
::Dir.mktmpdir do |root|
|
|
64
|
+
build_app(root, 'app/services/rpc')
|
|
65
|
+
output, status = boot(root)
|
|
66
|
+
|
|
67
|
+
expect(status).to be_success, output
|
|
68
|
+
expect(output).to include('BEFORE_BIND=[]')
|
|
69
|
+
expect(output).to include('AFTER_BIND=["Rpc::EchoService"]')
|
|
70
|
+
expect(output).not_to include('PLAIN_OBJECT_LOADED')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'loads nothing when services_path is not configured' do
|
|
75
|
+
::Dir.mktmpdir do |root|
|
|
76
|
+
build_app(root, nil)
|
|
77
|
+
output, status = boot(root)
|
|
78
|
+
|
|
79
|
+
expect(status).to be_success, output
|
|
80
|
+
expect(output).to include('AFTER_BIND=[]')
|
|
81
|
+
expect(output).not_to include('PLAIN_OBJECT_LOADED')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -53,6 +53,27 @@ RSpec.describe Protobuf::Field::BoolField do
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
describe '#decode' do
|
|
57
|
+
subject { described_class.new(SomeBoolMessage, :optional, described_class, :b, 1, :b, {}) }
|
|
58
|
+
|
|
59
|
+
it 'decodes 0 as false' do
|
|
60
|
+
expect(subject.decode(0)).to be(false)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'decodes 1 as true' do
|
|
64
|
+
expect(subject.decode(1)).to be(true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'decodes any other non-zero varint as true, like the reference parsers' do
|
|
68
|
+
expect(subject.decode(2)).to be(true)
|
|
69
|
+
expect(subject.decode(0xffff_ffff_ffff_ffff)).to be(true)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'decodes a non-canonical varint (2) on the wire as true' do
|
|
74
|
+
expect(SomeBoolMessage.decode("\x08\x02".b).some_bool).to be(true)
|
|
75
|
+
end
|
|
76
|
+
|
|
56
77
|
it 'defines ? method' do
|
|
57
78
|
instance.required_bool = false
|
|
58
79
|
expect(instance.required_bool?).to be(false)
|
|
@@ -95,5 +95,28 @@ EOF
|
|
|
95
95
|
expect(subject.to_s).to eq(compiled)
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
context 'with PB_SERVICE_PARENT in the environment' do
|
|
100
|
+
let(:service_parent) { ' ::Protobuf::Rpc::ApplicationService ' }
|
|
101
|
+
|
|
102
|
+
before do
|
|
103
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
104
|
+
allow(ENV).to receive(:[]).with('PB_SERVICE_PARENT').and_return(service_parent)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'uses the given parent class' do
|
|
108
|
+
subject.compile
|
|
109
|
+
expect(subject.to_s).to start_with("class TestService < ::Protobuf::Rpc::ApplicationService\n")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
context 'when it is empty' do
|
|
113
|
+
let(:service_parent) { '' }
|
|
114
|
+
|
|
115
|
+
it 'uses ::Protobuf::Rpc::Service' do
|
|
116
|
+
subject.compile
|
|
117
|
+
expect(subject.to_s).to eq(compiled)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
98
121
|
end
|
|
99
122
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'protobuf/railtie'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Protobuf::Railtie do
|
|
5
|
+
let(:config) do
|
|
6
|
+
double('config', :protobuf => ::ActiveSupport::OrderedOptions.new)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:app) { double('app', :config => config, :root => Pathname.new('/srv/app')) }
|
|
10
|
+
|
|
11
|
+
describe '.services_path' do
|
|
12
|
+
it 'is nil when not configured (opt-in)' do
|
|
13
|
+
expect(described_class.services_path(app)).to be_nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'is nil when configured as an empty string' do
|
|
17
|
+
config.protobuf.services_path = ''
|
|
18
|
+
expect(described_class.services_path(app)).to be_nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'honors an absolute path' do
|
|
22
|
+
config.protobuf.services_path = '/custom/services'
|
|
23
|
+
expect(described_class.services_path(app)).to eq('/custom/services')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'resolves a relative path against the app root' do
|
|
27
|
+
config.protobuf.services_path = 'app/services/rpc'
|
|
28
|
+
expect(described_class.services_path(app)).to eq('/srv/app/app/services/rpc')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'accepts a Pathname' do
|
|
32
|
+
config.protobuf.services_path = Pathname.new('/custom/services')
|
|
33
|
+
expect(described_class.services_path(app)).to eq('/custom/services')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '.load_services' do
|
|
38
|
+
let(:loader) { double('autoloader') }
|
|
39
|
+
|
|
40
|
+
before do
|
|
41
|
+
allow(described_class).to receive(:services_path).and_return('/svc')
|
|
42
|
+
allow(::File).to receive(:directory?).and_call_original
|
|
43
|
+
allow(::File).to receive(:directory?).with('/svc').and_return(true)
|
|
44
|
+
stub_const('Rails', double('Rails', :autoloaders => double(:main => loader)))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'does nothing when no path is configured' do
|
|
48
|
+
allow(described_class).to receive(:services_path).and_return(nil)
|
|
49
|
+
expect(::Protobuf::ServiceLoader).not_to receive(:load_services)
|
|
50
|
+
described_class.load_services(app)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'does nothing when the directory is missing' do
|
|
54
|
+
allow(::File).to receive(:directory?).with('/svc').and_return(false)
|
|
55
|
+
expect(::Protobuf::ServiceLoader).not_to receive(:load_services)
|
|
56
|
+
described_class.load_services(app)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'eager-loads through the autoloader when eager_load_dir is available' do
|
|
60
|
+
allow(loader).to receive(:respond_to?).with(:eager_load_dir).and_return(true)
|
|
61
|
+
expect(loader).to receive(:eager_load_dir).with('/svc')
|
|
62
|
+
expect(::Protobuf::ServiceLoader).not_to receive(:load_services)
|
|
63
|
+
described_class.load_services(app)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'falls back to ServiceLoader when eager_load_dir is unavailable' do
|
|
67
|
+
allow(loader).to receive(:respond_to?).with(:eager_load_dir).and_return(false)
|
|
68
|
+
expect(::Protobuf::ServiceLoader).to receive(:load_services).with('/svc')
|
|
69
|
+
described_class.load_services(app)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'falls back to ServiceLoader with the classic autoloader (no main loader)' do
|
|
73
|
+
stub_const('Rails', double('Rails', :autoloaders => double(:main => nil)))
|
|
74
|
+
expect(::Protobuf::ServiceLoader).to receive(:load_services).with('/svc')
|
|
75
|
+
described_class.load_services(app)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'falls back to ServiceLoader when the path is not autoloader-managed' do
|
|
79
|
+
stub_const('Zeitwerk::Error', Class.new(StandardError))
|
|
80
|
+
allow(loader).to receive(:respond_to?).with(:eager_load_dir).and_return(true)
|
|
81
|
+
allow(loader).to receive(:eager_load_dir).and_raise(::Zeitwerk::Error)
|
|
82
|
+
expect(::Protobuf::ServiceLoader).to receive(:load_services).with('/svc')
|
|
83
|
+
described_class.load_services(app)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require SUPPORT_PATH.join('resource_service')
|
|
3
|
+
|
|
4
|
+
RSpec.describe Protobuf::Rpc::ApplicationService do
|
|
5
|
+
it 'is a subclass of Protobuf::Rpc::Service' do
|
|
6
|
+
expect(described_class.ancestors).to include(Protobuf::Rpc::Service)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'inherits Service class methods (host, port, configure)' do
|
|
10
|
+
expect(described_class).to respond_to(:host, :port, :configure, :rpc, :rpcs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe 'integration with implemented_services' do
|
|
14
|
+
let!(:app_subclass) do
|
|
15
|
+
Class.new(described_class) do
|
|
16
|
+
def self.name
|
|
17
|
+
'ImplementedAppSubclassService'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
rpc :find, Test::ResourceFindRequest, Test::Resource
|
|
21
|
+
def find; end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let!(:abstract_subclass) do
|
|
26
|
+
Class.new(described_class) do
|
|
27
|
+
def self.name
|
|
28
|
+
'AbstractAppSubclassService'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
rpc :find, Test::ResourceFindRequest, Test::Resource
|
|
32
|
+
# no #find method defined -> not "implemented"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
after do
|
|
37
|
+
# `descendants` holds these classes until GC, which can run late. Remove
|
|
38
|
+
# the rpc method so other specs do not see this class as implemented.
|
|
39
|
+
app_subclass.send(:remove_method, :find)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'is discovered by Service.implemented_services (descendants traversal)' do
|
|
43
|
+
names = Protobuf::Rpc::Service.implemented_services
|
|
44
|
+
expect(names).to include('ImplementedAppSubclassService')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'omits subclasses with no implemented rpc methods' do
|
|
48
|
+
names = Protobuf::Rpc::Service.implemented_services
|
|
49
|
+
expect(names).not_to include('AbstractAppSubclassService')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'does not list the ApplicationService base itself' do
|
|
53
|
+
names = Protobuf::Rpc::Service.implemented_services
|
|
54
|
+
expect(names).not_to include(described_class.name)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|