Skip to content

Commit

Permalink
Generate specs for protocol tests (#270)
Browse files Browse the repository at this point in the history
  • Loading branch information
alextwoods authored Feb 22, 2025
1 parent 1ab8553 commit d74ab6e
Show file tree
Hide file tree
Showing 16 changed files with 4,791 additions and 47 deletions.
27 changes: 20 additions & 7 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ namespace :smithy do
t.rspec_opts += ' --tag rbs_test' if ENV['SMITHY_RUBY_RBS_TEST']
end

task 'spec:endpoints', [:rbs_test] do |_t, args|
task 'spec:generated', [:suite, :rbs_test] do |_t, args|
require_relative 'gems/smithy/spec/spec_helper'

spec_paths = []
include_paths = []
include_paths = ['gems/smithy/spec/support/matchers']
plans = []
rbs_targets = %w[Smithy Smithy::* Smithy::Client]
sig_paths = %w[gems/smithy-client/sig gems/smithy-schema/sig]
Dir.glob('gems/smithy/spec/fixtures/endpoints/*/model.json') do |model_path|
Dir.glob("gems/smithy/spec/fixtures/#{args[:suite]}/*/model.json") do |model_path|
test_name = model_path.split('/')[-2]
test_module = test_name.gsub('-', '').camelize
plan = SpecHelper.generate_gem(test_module, :client, fixture: "endpoints/#{test_name}")
plan = SpecHelper.generate_gem(test_module, :client, fixture: "#{args[:suite]}/#{test_name}")
plans << plan
tmpdir = plan.destination_root
spec_paths << "#{tmpdir}/spec"
Expand Down Expand Up @@ -55,7 +55,15 @@ namespace :smithy do
plans.each { |plan| SpecHelper.cleanup_gem(plan) }
end

task 'spec' => %w[spec:unit spec:endpoints]
task 'spec:endpoints', [:rbs_test] do |_t, args|
task('smithy:spec:generated').invoke('endpoints', args[:rbs_test])
end

task 'spec:protocols', [:rbs_test] do |_t, args|
task('smithy:spec:generated').invoke('protocol_tests', args[:rbs_test])
end

task 'spec' => %w[spec:unit spec:endpoints spec:protocols]

desc 'Convert all fixture smithy models to JSON AST representation.'
task 'sync-fixtures' do
Expand Down Expand Up @@ -109,8 +117,13 @@ namespace :smithy do
task('smithy:spec:endpoints').invoke('rbs_test')
end

desc 'Run RBS spy tests for all generated protocol test specs.'
task 'rbs:protocol_tests' do
task('smithy:spec:protocols').invoke('rbs_test')
end

desc 'Run RBS spy tests for unit tests and generated endpoint provider specs.'
task 'rbs' => %w[rbs:unit rbs:endpoints]
task 'rbs' => %w[rbs:unit rbs:endpoints rbs:protocol_tests]
end

namespace 'smithy-client' do
Expand Down Expand Up @@ -163,7 +176,7 @@ namespace 'smithy-schema' do
'RBS_TEST_RAISE' => 'true',
'RBS_TEST_LOGLEVEL' => 'error',
'RBS_TEST_OPT' => '-I gems/smithy-client/sig',
'RBS_TEST_TARGET' => '"Smithy::Model,Smithy::Model::*"',
'RBS_TEST_TARGET' => '"Smithy::Schema,Smithy::Schema::*"',
'RBS_TEST_DOUBLE_SUITE' => 'rspec'
}
sh(env,
Expand Down
6 changes: 3 additions & 3 deletions gems/smithy-client/lib/smithy-client/codecs/cbor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def format_blob(value)
def format_data(value, shape)
case shape
when StructureShape then format_structure(value, shape)
when ListShape then format_list(value, shape)
when MapShape then format_map(value, shape)
when BlobShape then format_blob(value)
when ListShape then format_list(value, shape)
when MapShape then format_map(value, shape)
when BlobShape then format_blob(value)
else value
end
end
Expand Down
1 change: 1 addition & 0 deletions gems/smithy/lib/smithy/generators/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def spec_files
Enumerator.new do |e|
e.yield 'spec/spec_helper.rb', Views::Client::SpecHelper.new(@plan).render
e.yield "spec/#{@gem_name}/endpoint_provider_spec.rb", Views::Client::EndpointProviderSpec.new(@plan).render
e.yield "spec/#{@gem_name}/protocol_spec.rb", Views::Client::ProtocolSpec.new(@plan).render
end
end

Expand Down
164 changes: 164 additions & 0 deletions gems/smithy/lib/smithy/templates/client/protocol_spec.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# This is generated code!

require_relative '../spec_helper'
<% additional_requires.each do |r| -%>
require '<%= r %>'
<% end -%>

module <%= module_name %>
# TODO: Can be replaced by stub_responses config once implemented
class StubSend < Smithy::Client::Plugin
option(:stub_response)
handle(step: :send) do |context|
if (stub_response = context.config.stub_response)
resp = context.response
resp.signal_headers(stub_response[:status_code], stub_response.fetch(:headers, {}))
resp.signal_data(stub_response[:body]) if stub_response[:body]
resp.signal_done
end
Smithy::Client::Output.new(context: context)
end
end

describe Client do
before(:all) { Client.add_plugin(StubSend) }
after(:all) { Client.remove_plugin(StubSend) }

let(:client_options) do
{
# stub_responses: true,
# validate_input: false,
endpoint: 'http://127.0.0.1',
# retry_strategy: # disable?
}
end

let(:client) { Client.new(client_options) }
<% all_operation_tests.each do |operation_tests| %>
describe '#<%= operation_tests.name %>' do
<% unless operation_tests.request_tests.empty? -%>
describe 'requests' do
<% operation_tests.request_tests.each do |test| %>
<% test.comments.each do |line| -%>
# <%= line %>
<% end -%>
<% if test.skip? -%>
it '<%= test.id %>', skip: '<%= test.skip_reason %>' do
<% else -%>
it '<%= test.id %>' do
<% end -%>
<% if test['host'] -%>
client = Client.new(client_options.merge(endpoint: '<%= test.endpoint %>'))
<% end -%>
<% if test.idempotency_token_trait? -%>
allow(SecureRandom).to receive(:uuid).and_return('00000000-0000-4000-8000-000000000000')
<% end -%>
resp = client.<%= operation_tests.name %>(<%= test.params %>)
request = resp.context.request
expect(request.http_method).to eq('<%= test['method'] %>')
expect(request.endpoint.path).to eq('<%= test['uri'] %>')
<% if test['resolvedHost'] -%>
expect(request.endpoint.host).to eq('<%= test['resolvedHost'] %>')
<% end %>
<% if test.query_expect? -%>
actual_query = CGI.parse(request.endpoint.query)
<% if test['queryParams'] %>
expected_query = CGI.parse("<%= test['queryParams'].join('&') %>")
expected_query.each do |k, v|
actual = actual_query[k].map { |s| s.force_encoding('utf-8') }
expect(actual).to eq(v)
end
<% end -%>
<% if test['forbidQueryParams'] -%>
<%= test['forbidQueryParams']%>.each do |query|
expect(actual_query.key?(query)).to be false
end
<% end -%>
<% if test['requireQueryParams'] -%>
<%= test['requireQueryParams']%>.each do |query|
expect(actual_query.key?(query)).to be true
end
<% end -%>
<% end -%>
<% test['headers']&.each do |k,v| -%>
expect(request.headers['<%= k %>']).to eq('<%= v %>')
<% end -%>
<% test['forbidHeaders']&.each do |k| -%>
expect(request.headers.key?('<%= k %>')).to be(false)
<% end -%>
<% test['requireHeaders']&.each do |k| -%>
expect(request.headers.key?('<%= k %>')).to be(true)
<% end %>
<% if test['body'] -%>
<%= test.body_expect %>
<% end -%>
end
<% end -%>
end
<% end %>
<% unless operation_tests.response_tests.empty? -%>
describe 'responses' do
<% operation_tests.response_tests.each do |test| %>
<% test.comments.each do |line| -%>
# <%= line %>
<% end -%>
<% if test.skip? -%>
it '<%= test.id %>', skip: '<%= test.skip_reason %>' do
<% else -%>
it '<%= test.id %>' do
<% end -%>
response = { status_code: <%= test['code'] %> }
<% if test['headers'] -%>
response[:headers] = <%= test['headers'] %>
<% end -%>
<% if test['body'] -%>
response[:body] = <%= test.stub_body %>
<% end -%>

client = Client.new(client_options.merge(stub_response: response, validate_params: false))
allow(client.config.protocol).to receive(:build)
resp = client.<%= operation_tests.name %>

<% if (member_name, _shape = test.streaming_member) -%>
resp.data.<%= member_name.underscore %>.rewind
resp.data.<%= member_name.underscore %> = resp.data.<%= member_name.underscore %>.read
resp.data.<%= member_name.underscore %> = nil if resp.data.<%= member_name.underscore %>.empty?
<% end -%>
<%= test.data_expect %>
end
<% end -%>
end
<% end -%>
<% unless operation_tests.error_tests.empty? -%>
describe 'response errors' do
<% operation_tests.error_tests.each do |test| %>
<% test.comments.each do |line| -%>
# <%= line %>
<% end -%>
<% if test.skip? -%>
it '<%= test.id %>: <%= test.error_name %>', skip: '<%= test.skip_reason %>' do
<% else -%>
it '<%= test.id %>: <%= test.error_name %>' do
<% end -%>
response = { status_code: <%= test['code'] %> }
<% if test['headers'] -%>
response[:headers] = <%= test['headers'] %>
<% end -%>
<% if test['body'] -%>
response[:body] = <%= test.stub_body %>
<% end -%>

client = Client.new(client_options.merge(stub_response: response, validate_params: false))
allow(client.config.protocol).to receive(:build)
expect { client.<%= operation_tests.name %> }.to raise_error do |e|
expect(e).to be_a(Errors::<%= test.error_name %>)
<%= test.data_expect %>
end
end
<% end -%>
end
<% end -%>
end
<% end -%>
end
end
2 changes: 2 additions & 0 deletions gems/smithy/lib/smithy/views/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module Client; end
require_relative 'client/plugin'
require_relative 'client/plugin_list'
require_relative 'client/request_response_example'
require_relative 'client/shape_to_hash'

# views
require_relative 'client/client'
Expand All @@ -30,6 +31,7 @@ module Client; end
require_relative 'client/module'
require_relative 'client/module_rbs'
require_relative 'client/protocol_plugin'
require_relative 'client/protocol_spec'
require_relative 'client/rubocop_yml'
require_relative 'client/schema'
require_relative 'client/schema_rbs'
Expand Down
40 changes: 3 additions & 37 deletions gems/smithy/lib/smithy/views/client/endpoint_provider_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,50 +87,16 @@ def build_client_params(data)

def build_operation_params(data, input)
data.fetch('operationParams', {}).map do |k, v|
member_shape = @model['shapes'][input['members'][k]['target']]
Param.new(k.underscore, transform_operation_values(v, member_shape))
member_shape = Model.shape(@model, input['members'][k]['target'])
Param.new(k.underscore, ShapeToHash.transform_value(@model, v, member_shape))
end
end

def find_input(data, operations)
input_target = operations.find do |k, _v|
k.split('#').last == data['operationName']
end.last['input']['target']
@model['shapes'][input_target]
end

def transform_operation_values(value, shape)
return value unless shape

case shape['type']
when 'structure', 'union'
transform_structure(shape, value)
when 'list'
transform_list(shape, value)
when 'map'
transform_map(shape, value)
else
value
end
end

def transform_map(shape, value)
member_shape = @model['shapes'][shape['value']['target']]
value.transform_values do |v|
transform_operation_values(v, member_shape)
end
end

def transform_list(shape, value)
member_shape = @model['shapes'][shape['member']['target']]
value.map { |v| transform_operation_values(v, member_shape) }
end

def transform_structure(shape, value)
value.each_with_object({}) do |(k, v), o|
member_shape = @model['shapes'][shape['members'][k]['target']]
o[k.underscore.to_sym] = transform_operation_values(v, member_shape)
end
Model.shape(@model, input_target)
end

def built_in_bindings
Expand Down
Loading

0 comments on commit d74ab6e

Please sign in to comment.