Add HW 2
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import grpc_tools
|
||||
from google.protobuf import descriptor
|
||||
from google.protobuf import descriptor_pb2
|
||||
from google.protobuf import descriptor_pool
|
||||
from grpc_tools import protoc
|
||||
|
||||
|
||||
SCRIPT_DIR = pathlib.Path(__file__).parent.resolve()
|
||||
PROTO_DIR = SCRIPT_DIR.parent / 'solution' / 'proto'
|
||||
PROTO_FILE = PROTO_DIR / 'messenger.proto'
|
||||
WELL_KNOWN_PROTO_DIR = pathlib.Path(grpc_tools.__file__).parent / '_proto'
|
||||
|
||||
|
||||
def compile_descriptor_set(output_path):
|
||||
result = protoc.main([
|
||||
'grpc_tools.protoc',
|
||||
f'-I{PROTO_DIR}',
|
||||
f'-I{WELL_KNOWN_PROTO_DIR}',
|
||||
f'--descriptor_set_out={output_path}',
|
||||
'--include_imports',
|
||||
str(PROTO_FILE),
|
||||
])
|
||||
assert result == 0, 'messenger.proto must compile successfully'
|
||||
|
||||
descriptor_set = descriptor_pb2.FileDescriptorSet()
|
||||
descriptor_set.ParseFromString(output_path.read_bytes())
|
||||
return descriptor_set
|
||||
|
||||
|
||||
def build_descriptor_pool(descriptor_set):
|
||||
pool = descriptor_pool.DescriptorPool()
|
||||
remaining = list(descriptor_set.file)
|
||||
while remaining:
|
||||
deferred = []
|
||||
for file_descriptor in remaining:
|
||||
try:
|
||||
pool.Add(file_descriptor)
|
||||
except TypeError:
|
||||
deferred.append(file_descriptor)
|
||||
assert len(deferred) < len(remaining), 'messenger.proto imports could not be resolved'
|
||||
remaining = deferred
|
||||
return pool
|
||||
|
||||
|
||||
def require_singular_field(message_type, field_name, field_type, message_type_name=None):
|
||||
assert field_name in message_type.fields_by_name, \
|
||||
f'{message_type.full_name} must contain field {field_name}'
|
||||
field = message_type.fields_by_name[field_name]
|
||||
assert not field.is_repeated, f'{field.full_name} must be a singular field'
|
||||
assert field.type == field_type, f'{field.full_name} has an invalid type'
|
||||
if message_type_name is not None:
|
||||
assert field.message_type is not None
|
||||
assert field.message_type.full_name == message_type_name, \
|
||||
f'{field.full_name} has an invalid message type'
|
||||
|
||||
|
||||
def require_fields_can_coexist(message_type, field_names):
|
||||
fields_by_oneof = {}
|
||||
for field_name in field_names:
|
||||
field = message_type.fields_by_name[field_name]
|
||||
if field.containing_oneof is None:
|
||||
continue
|
||||
previous_field = fields_by_oneof.setdefault(field.containing_oneof.full_name, field_name)
|
||||
assert previous_field == field_name, \
|
||||
f'{message_type.full_name} fields must allow simultaneous values'
|
||||
|
||||
|
||||
def test_proto_contract():
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
descriptor_path = pathlib.Path(temporary_directory) / 'messenger.pb'
|
||||
descriptor_set = compile_descriptor_set(descriptor_path)
|
||||
|
||||
submitted_file = next(
|
||||
(file_descriptor for file_descriptor in descriptor_set.file
|
||||
if pathlib.PurePosixPath(file_descriptor.name).name == PROTO_FILE.name),
|
||||
None,
|
||||
)
|
||||
assert submitted_file is not None, 'messenger.proto descriptor is missing'
|
||||
assert submitted_file.syntax == 'proto3', 'messenger.proto must use proto3 syntax'
|
||||
assert submitted_file.package == 'mes_grpc', 'messenger.proto must use package mes_grpc'
|
||||
|
||||
pool = build_descriptor_pool(descriptor_set)
|
||||
try:
|
||||
messenger = pool.FindServiceByName('mes_grpc.MessengerServer')
|
||||
except KeyError:
|
||||
raise AssertionError('gRPC service must be named mes_grpc.MessengerServer') from None
|
||||
|
||||
assert 'SendMessage' in messenger.methods_by_name, \
|
||||
'MessengerServer must contain method SendMessage'
|
||||
send_message = messenger.methods_by_name['SendMessage']
|
||||
assert not send_message.client_streaming and not send_message.server_streaming, \
|
||||
'SendMessage must be unary'
|
||||
require_singular_field(
|
||||
send_message.input_type,
|
||||
'author',
|
||||
descriptor.FieldDescriptor.TYPE_STRING,
|
||||
)
|
||||
require_singular_field(
|
||||
send_message.input_type,
|
||||
'text',
|
||||
descriptor.FieldDescriptor.TYPE_STRING,
|
||||
)
|
||||
require_fields_can_coexist(send_message.input_type, ('author', 'text'))
|
||||
require_singular_field(
|
||||
send_message.output_type,
|
||||
'sendTime',
|
||||
descriptor.FieldDescriptor.TYPE_MESSAGE,
|
||||
'google.protobuf.Timestamp',
|
||||
)
|
||||
|
||||
assert 'ReadMessages' in messenger.methods_by_name, \
|
||||
'MessengerServer must contain method ReadMessages'
|
||||
read_messages = messenger.methods_by_name['ReadMessages']
|
||||
assert not read_messages.client_streaming and read_messages.server_streaming, \
|
||||
'ReadMessages must be a unary request with a server stream response'
|
||||
assert not read_messages.input_type.fields, \
|
||||
'ReadMessages request must not contain fields'
|
||||
require_singular_field(
|
||||
read_messages.output_type,
|
||||
'author',
|
||||
descriptor.FieldDescriptor.TYPE_STRING,
|
||||
)
|
||||
require_singular_field(
|
||||
read_messages.output_type,
|
||||
'text',
|
||||
descriptor.FieldDescriptor.TYPE_STRING,
|
||||
)
|
||||
require_singular_field(
|
||||
read_messages.output_type,
|
||||
'sendTime',
|
||||
descriptor.FieldDescriptor.TYPE_MESSAGE,
|
||||
'google.protobuf.Timestamp',
|
||||
)
|
||||
require_fields_can_coexist(
|
||||
read_messages.output_type,
|
||||
('author', 'text', 'sendTime'),
|
||||
)
|
||||
Reference in New Issue
Block a user