From f284ba6d19dc3d6c8fa81763f5450d02283d7d76 Mon Sep 17 00:00:00 2001 From: Dan Abel Date: Wed, 13 Nov 2013 12:29:44 +0000 Subject: [PATCH 1/5] [vcloud_director] tests become pending not failing on absense of testable resources --- tests/vcloud_director/models/compute/vapp_tests.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/vcloud_director/models/compute/vapp_tests.rb b/tests/vcloud_director/models/compute/vapp_tests.rb index c15333dce..3c44527a2 100644 --- a/tests/vcloud_director/models/compute/vapp_tests.rb +++ b/tests/vcloud_director/models/compute/vapp_tests.rb @@ -2,7 +2,9 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'helper')) Shindo.tests("Compute::VcloudDirector | vapps", ['vclouddirector', 'all']) do pending if Fog.mocking? - tests("#There is more than one vapp").returns(true){ vdc.vapps.size >= 1 } + + # unless there is atleast one vapp we cannot run these tests + pending if vdc.vapps.empty? vapps = vdc.vapps vapp = vapps.first From 2265bf76b2dddbfda06d3516a6ae3c582cdad6c5 Mon Sep 17 00:00:00 2001 From: Rodrigo Estebanez Date: Thu, 14 Nov 2013 11:29:15 +0100 Subject: [PATCH 2/5] adding spot price to launch configurations --- lib/fog/aws/models/auto_scaling/configuration.rb | 2 ++ .../aws/parsers/auto_scaling/describe_launch_configurations.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/fog/aws/models/auto_scaling/configuration.rb b/lib/fog/aws/models/auto_scaling/configuration.rb index 434f428c1..eaf8b5839 100644 --- a/lib/fog/aws/models/auto_scaling/configuration.rb +++ b/lib/fog/aws/models/auto_scaling/configuration.rb @@ -18,6 +18,8 @@ module Fog attribute :ramdisk_id, :aliases => 'RamdiskId' attribute :security_groups, :aliases => 'SecurityGroups' attribute :user_data, :aliases => 'UserData' + attribute :spot_price, :aliases => 'SpotPrice' + def initialize(attributes={}) #attributes[:availability_zones] ||= %w(us-east-1a us-east-1b us-east-1c us-east-1d) diff --git a/lib/fog/aws/parsers/auto_scaling/describe_launch_configurations.rb b/lib/fog/aws/parsers/auto_scaling/describe_launch_configurations.rb index f1e4df840..f70df9695 100644 --- a/lib/fog/aws/parsers/auto_scaling/describe_launch_configurations.rb +++ b/lib/fog/aws/parsers/auto_scaling/describe_launch_configurations.rb @@ -68,6 +68,8 @@ module Fog @launch_configuration[name] = value when 'KernelId', 'RamdiskId', 'UserData' @launch_configuration[name] = value + when 'SpotPrice' + @launch_configuration[name] = value.to_f when 'BlockDeviceMappings' @in_block_device_mappings = false From d79626f4d7e8c6986f5d9e8c6e452a077796d885 Mon Sep 17 00:00:00 2001 From: Kyle Rames Date: Thu, 14 Nov 2013 09:05:18 -0600 Subject: [PATCH 3/5] [rackspace] fixing broken tests caused by bad helper --- tests/rackspace/helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rackspace/helper.rb b/tests/rackspace/helper.rb index 0629df153..8fa59838f 100644 --- a/tests/rackspace/helper.rb +++ b/tests/rackspace/helper.rb @@ -69,7 +69,7 @@ module Shindo def rackspace_test_image_id(service) image_id = Fog.credentials[:rackspace_image_id] # I chose to use the first Ubuntu because it will work with the smallest flavor and it doesn't require a license - image_id ||= Fog.mocking? ? @service.images.first.id : @service.images.find {|image| image.name =~ /Ubuntu/}.id # use the first Ubuntu image + image_id ||= Fog.mocking? ? service.images.first.id : service.images.find {|image| image.name =~ /Ubuntu/}.id # use the first Ubuntu image end def rackspace_test_flavor_id(service) From ac376a7cc489f64d3f0725a4a3e50b28a0c69fdb Mon Sep 17 00:00:00 2001 From: Carlos Sanchez Date: Tue, 12 Nov 2013 18:48:59 +0100 Subject: [PATCH 4/5] [aws] Implement missing mocks for Route 53 Enable the tests --- lib/fog/aws.rb | 2 +- lib/fog/aws/dns.rb | 3 +- .../dns/change_resource_record_sets.rb | 35 +++++++--- .../aws/requests/dns/create_hosted_zone.rb | 15 +++- .../aws/requests/dns/delete_hosted_zone.rb | 33 +++++++++ lib/fog/aws/requests/dns/get_change.rb | 24 +++++++ lib/fog/aws/requests/dns/get_hosted_zone.rb | 2 +- lib/fog/aws/requests/dns/list_hosted_zones.rb | 11 +-- .../requests/dns/list_resource_record_sets.rb | 68 +++++++++++++++++++ lib/fog/core/mock.rb | 7 ++ tests/aws/models/dns/record_tests.rb | 3 +- tests/aws/models/dns/records_tests.rb | 9 +-- tests/aws/models/dns/zone_tests.rb | 2 +- tests/aws/models/dns/zones_tests.rb | 2 +- tests/aws/requests/dns/dns_tests.rb | 27 +------- 15 files changed, 183 insertions(+), 60 deletions(-) diff --git a/lib/fog/aws.rb b/lib/fog/aws.rb index 577bfae92..077fb96e6 100644 --- a/lib/fog/aws.rb +++ b/lib/fog/aws.rb @@ -270,7 +270,7 @@ module Fog "zone-#{Fog::Mock.random_hex(8)}" end def self.change_id - "change-#{Fog::Mock.random_hex(8)}" + Fog::Mock.random_letters_and_numbers(14) end def self.nameservers [ diff --git a/lib/fog/aws/dns.rb b/lib/fog/aws/dns.rb index 869ce1126..dd9ce4455 100644 --- a/lib/fog/aws/dns.rb +++ b/lib/fog/aws/dns.rb @@ -34,7 +34,8 @@ module Fog :limits => { :duplicate_domains => 5 }, - :zones => {} + :zones => {}, + :changes => {} } end end diff --git a/lib/fog/aws/requests/dns/change_resource_record_sets.rb b/lib/fog/aws/requests/dns/change_resource_record_sets.rb index e4040d90a..9444089bd 100644 --- a/lib/fog/aws/requests/dns/change_resource_record_sets.rb +++ b/lib/fog/aws/requests/dns/change_resource_record_sets.rb @@ -141,6 +141,7 @@ module Fog if (zone = self.data[:zones][zone_id]) response.status = 200 + change_id = Fog::AWS::Mock.change_id change_batch.each do |change| case change[:action] when "CREATE" @@ -149,12 +150,23 @@ module Fog end if zone[:records][change[:type]][change[:name]].nil? + # raise change.to_s if change[:resource_records].nil? + zone[:records][change[:type]][change[:name]] = + if change[:alias_target] + record = { + :alias_target => change[:alias_target] + } + else + record = { + :ttl => change[:ttl].to_s, + } + end zone[:records][change[:type]][change[:name]] = { + :change_id => change_id, + :resource_records => change[:resource_records] || [], :name => change[:name], - :type => change[:type], - :ttl => change[:ttl], - :resource_records => change[:resource_records] - } + :type => change[:type] + }.merge(record) else errors << "Tried to create resource record set #{change[:name]}. type #{change[:type]}, but it already exists" end @@ -166,12 +178,16 @@ module Fog end if errors.empty? + change = { + :id => change_id, + :status => 'INSYNC', + :submitted_at => Time.now.utc.iso8601 + } + self.data[:changes][change[:id]] = change response.body = { - 'ChangeInfo' => { - 'Id' => "/change/#{Fog::AWS::Mock.change_id}", - 'Status' => 'INSYNC', - 'SubmittedAt' => Time.now.utc.iso8601 - } + 'Id' => change[:id], + 'Status' => change[:status], + 'SubmittedAt' => change[:submitted_at] } response else @@ -184,6 +200,7 @@ module Fog response.body = "NoSuchHostedZoneA hosted zone with the specified hosted zone ID does not exist.#{Fog::AWS::Mock.request_id}" raise(Excon::Errors.status_error({:expects => 200}, response)) end + end end diff --git a/lib/fog/aws/requests/dns/create_hosted_zone.rb b/lib/fog/aws/requests/dns/create_hosted_zone.rb index 6e1fe4ccd..bbd05fd18 100644 --- a/lib/fog/aws/requests/dns/create_hosted_zone.rb +++ b/lib/fog/aws/requests/dns/create_hosted_zone.rb @@ -60,6 +60,9 @@ module Fog require 'time' def create_hosted_zone(name, options = {}) + # Append a trailing period to the name if absent. + name = name + "." unless name.end_with?(".") + response = Excon::Response.new if list_hosted_zones.body['HostedZones'].find_all {|z| z['Name'] == name}.size < self.data[:limits][:duplicate_domains] response.status = 201 @@ -77,6 +80,12 @@ module Fog :comment => options[:comment], :records => {} } + change = { + :id => Fog::AWS::Mock.change_id, + :status => 'INSYNC', + :submitted_at => Time.now.utc.iso8601 + } + self.data[:changes][change[:id]] = change response.body = { 'HostedZone' => { 'Id' => zone_id, @@ -85,9 +94,9 @@ module Fog 'Comment' => options[:comment] }, 'ChangeInfo' => { - 'Id' => "/change/#{Fog::AWS::Mock.change_id}", - 'Status' => 'INSYNC', - 'SubmittedAt' => Time.now.utc.iso8601 + 'Id' => change[:id], + 'Status' => change[:status], + 'SubmittedAt' => change[:submitted_at] }, 'NameServers' => Fog::AWS::Mock.nameservers } diff --git a/lib/fog/aws/requests/dns/delete_hosted_zone.rb b/lib/fog/aws/requests/dns/delete_hosted_zone.rb index 307909b87..d26e2553c 100644 --- a/lib/fog/aws/requests/dns/delete_hosted_zone.rb +++ b/lib/fog/aws/requests/dns/delete_hosted_zone.rb @@ -34,6 +34,39 @@ module Fog end end + + class Mock + + require 'time' + + def delete_hosted_zone(zone_id) + response = Excon::Response.new + key = [zone_id, "/hostedzone/#{zone_id}"].find{|k| !self.data[:zones][k].nil?} + if key + change = { + :id => Fog::AWS::Mock.change_id, + :status => 'INSYNC', + :submitted_at => Time.now.utc.iso8601 + } + self.data[:changes][change[:id]] = change + response.status = 200 + response.body = { + 'ChangeInfo' => { + 'Id' => change[:id], + 'Status' => change[:status], + 'SubmittedAt' => change[:submitted_at] + } + } + self.data[:zones].delete(key) + response + else + response.status = 404 + response.body = "SenderNoSuchHostedZoneThe specified hosted zone does not exist.#{Fog::AWS::Mock.request_id}" + raise(Excon::Errors.status_error({:expects => 200}, response)) + end + end + end + end end end diff --git a/lib/fog/aws/requests/dns/get_change.rb b/lib/fog/aws/requests/dns/get_change.rb index 5f2808c96..bfb605841 100644 --- a/lib/fog/aws/requests/dns/get_change.rb +++ b/lib/fog/aws/requests/dns/get_change.rb @@ -33,6 +33,30 @@ module Fog end end + + class Mock + def get_change(change_id) + response = Excon::Response.new + # find the record with matching change_id + # records = data[:zones].values.map{|z| z[:records].values.map{|r| r.values}}.flatten + change = self.data[:changes][change_id] + + if change + response.status = 200 + response.body = { + 'Id' => change[:id], + 'Status' => 'INSYNC', # TODO do some logic here + 'SubmittedAt' => change[:submitted_at] + } + response + else + response.status = 404 + response.body = "SenderNoSuchChangeCould not find resource with ID: #{change_id}#{Fog::AWS::Mock.request_id}" + raise(Excon::Errors.status_error({:expects => 200}, response)) + end + end + end + end end end diff --git a/lib/fog/aws/requests/dns/get_hosted_zone.rb b/lib/fog/aws/requests/dns/get_hosted_zone.rb index c55b8a404..8a5c48080 100644 --- a/lib/fog/aws/requests/dns/get_hosted_zone.rb +++ b/lib/fog/aws/requests/dns/get_hosted_zone.rb @@ -55,7 +55,7 @@ module Fog response else response.status = 404 - response.body = "NoSuchHostedZoneA hosted zone with the specified hosted zone ID does not exist.#{Fog::AWS::Mock.request_id}" + response.body = "SenderNoSuchHostedZoneThe specified hosted zone does not exist.#{Fog::AWS::Mock.request_id}" raise(Excon::Errors.status_error({:expects => 200}, response)) end end diff --git a/lib/fog/aws/requests/dns/list_hosted_zones.rb b/lib/fog/aws/requests/dns/list_hosted_zones.rb index 4a36c4ab2..82abc6b04 100644 --- a/lib/fog/aws/requests/dns/list_hosted_zones.rb +++ b/lib/fog/aws/requests/dns/list_hosted_zones.rb @@ -53,12 +53,7 @@ module Fog class Mock def list_hosted_zones(options = {}) - - if options[:max_items].nil? - maxitems = 100 - else - maxitems = options[:max_items] - end + maxitems = [options[:max_items]||100,100].min if options[:marker].nil? start = 0 @@ -82,8 +77,8 @@ module Fog } end, 'Marker' => options[:marker].to_s, - 'MaxItems' => options[:max_items].to_s, - 'IsTruncated' => truncated.to_s + 'MaxItems' => maxitems, + 'IsTruncated' => truncated } if truncated diff --git a/lib/fog/aws/requests/dns/list_resource_record_sets.rb b/lib/fog/aws/requests/dns/list_resource_record_sets.rb index 83643ea35..33a13e54e 100644 --- a/lib/fog/aws/requests/dns/list_resource_record_sets.rb +++ b/lib/fog/aws/requests/dns/list_resource_record_sets.rb @@ -60,6 +60,74 @@ module Fog end end + + class Mock + + def list_resource_record_sets(zone_id, options = {}) + maxitems = [options[:max_items]||100,100].min + + response = Excon::Response.new + + zone = self.data[:zones][zone_id] + if zone.nil? + response.status = 404 + response.body = "\nSenderNoSuchHostedZoneNo hosted zone found with ID: #{zone_id}#{Fog::AWS::Mock.request_id}" + raise(Excon::Errors.status_error({:expects => 200}, response)) + end + + if options[:type] + records = zone[:records][options[:type]].values + else + records = zone[:records].values.map{|r| r.values}.flatten + end + + # sort for pagination + records.sort! { |a,b| a[:name].gsub(zone[:name],"") <=> b[:name].gsub(zone[:name],"") } + + if options[:name] + name = options[:name].gsub(zone[:name],"") + records = records.select{|r| r[:name].gsub(zone[:name],"") >= name } + require 'pp' + end + + next_record = records[maxitems] + records = records[0, maxitems] + truncated = !next_record.nil? + + response.status = 200 + response.body = { + 'ResourceRecordSets' => records.map do |r| + if r[:alias_target] + record = { + 'AliasTarget' => { + 'HostedZoneId' => r[:alias_target][:hosted_zone_id], + 'DNSName' => r[:alias_target][:dns_name] + } + } + else + record = { + 'TTL' => r[:ttl] + } + end + { + 'ResourceRecords' => r[:resource_records], + 'Name' => r[:name], + 'Type' => r[:type] + }.merge(record) + end, + 'MaxItems' => maxitems, + 'IsTruncated' => truncated + } + + if truncated + response.body['NextRecordName'] = next_record[:name] + response.body['NextRecordType'] = next_record[:type] + end + + response + end + + end end end end diff --git a/lib/fog/core/mock.rb b/lib/fog/core/mock.rb index c76c744dd..09d4cd0af 100644 --- a/lib/fog/core/mock.rb +++ b/lib/fog/core/mock.rb @@ -74,6 +74,13 @@ module Fog rand(max).to_s end + def self.random_letters_and_numbers(length) + random_selection( + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + length + ) + end + def self.random_selection(characters, length) selection = '' length.times do diff --git a/tests/aws/models/dns/record_tests.rb b/tests/aws/models/dns/record_tests.rb index bc3c9bec6..5b649ad38 100644 --- a/tests/aws/models/dns/record_tests.rb +++ b/tests/aws/models/dns/record_tests.rb @@ -1,13 +1,12 @@ Shindo.tests("Fog::Dns[:aws] | record", ['aws', 'dns']) do - pending if Fog.mocking? tests("zones#create").succeeds do @zone = Fog::DNS[:aws].zones.create(:domain => generate_unique_domain) end params = { :name => @zone.domain, :type => 'A', :ttl => 3600, :value => ['1.2.3.4'] } - model_tests(@zone.records, params, false) do + model_tests(@zone.records, params) do # Newly created records should have a change id tests("#change_id") do diff --git a/tests/aws/models/dns/records_tests.rb b/tests/aws/models/dns/records_tests.rb index 1d79d10af..54239b4b3 100644 --- a/tests/aws/models/dns/records_tests.rb +++ b/tests/aws/models/dns/records_tests.rb @@ -1,5 +1,4 @@ Shindo.tests("Fog::DNS[:aws] | records", ['aws', 'dns']) do - pending if Fog.mocking? tests("zones#create").succeeds do @zone = Fog::DNS[:aws].zones.create(:domain => generate_unique_domain) @@ -13,7 +12,7 @@ Shindo.tests("Fog::DNS[:aws] | records", ['aws', 'dns']) do ] param_groups.each do |params| - collection_tests(@zone.records, params, false) + collection_tests(@zone.records, params) end records = [] @@ -24,11 +23,7 @@ Shindo.tests("Fog::DNS[:aws] | records", ['aws', 'dns']) do records << @zone.records.create(:name => "*.#{@zone.domain}", :type => "A", :ttl => 3600, :value => ['1.2.3.4']) - tests("#all!").returns(103) do - @zone.records.all!.size - end - - tests("#all!").returns(103) do + tests("#all!").returns(101) do @zone.records.all!.size end diff --git a/tests/aws/models/dns/zone_tests.rb b/tests/aws/models/dns/zone_tests.rb index 40743c9fe..1c473d89b 100644 --- a/tests/aws/models/dns/zone_tests.rb +++ b/tests/aws/models/dns/zone_tests.rb @@ -1,4 +1,4 @@ Shindo.tests("Fog::DNS[:aws] | zone", ['aws', 'dns']) do params = {:domain => generate_unique_domain } - model_tests(Fog::DNS[:aws].zones, params, false) + model_tests(Fog::DNS[:aws].zones, params) end diff --git a/tests/aws/models/dns/zones_tests.rb b/tests/aws/models/dns/zones_tests.rb index b7772eab3..a770cd7fe 100644 --- a/tests/aws/models/dns/zones_tests.rb +++ b/tests/aws/models/dns/zones_tests.rb @@ -1,4 +1,4 @@ Shindo.tests("Fog::DNS[:aws] | zones", ['aws', 'dns']) do params = {:domain => generate_unique_domain } - collection_tests(Fog::DNS[:aws].zones, params, false) + collection_tests(Fog::DNS[:aws].zones, params) end diff --git a/tests/aws/requests/dns/dns_tests.rb b/tests/aws/requests/dns/dns_tests.rb index 21fba2060..34e693652 100644 --- a/tests/aws/requests/dns/dns_tests.rb +++ b/tests/aws/requests/dns/dns_tests.rb @@ -12,8 +12,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do tests('success') do test('get current zone count') do - pending if Fog.mocking? - @org_zone_count= 0 response = @r53_connection.list_hosted_zones if response.status == 200 @@ -25,8 +23,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do end test('create simple zone') { - pending if Fog.mocking? - result = false response = @r53_connection.create_hosted_zone(@domain_name) @@ -55,8 +51,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test("get status of change #{@change_id}") { - pending if Fog.mocking? - result = false response = @r53_connection.get_change(@change_id) if response.status == 200 @@ -70,8 +64,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test("get info on hosted zone #{@zone_id}") { - pending if Fog.mocking? - result = false response = @r53_connection.get_hosted_zone(@zone_id) @@ -83,7 +75,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do ns_servers = response.body['NameServers'] # AWS returns domain with a dot at end - so when compare, remove dot - if (zone_id == @zone_id) and (name.chop == @domain_name) and (caller_ref.length > 0) and (ns_servers.count > 0) result = true @@ -94,8 +85,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test('list zones') do - pending if Fog.mocking? - result = false response = @r53_connection.list_hosted_zones @@ -120,8 +109,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do end test("add a A resource record") { - pending if Fog.mocking? - # create an A resource record host = 'www.' + @domain_name ip_addrs = ['1.2.3.4'] @@ -142,8 +129,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test("add a CNAME resource record") { - pending if Fog.mocking? - # create a CNAME resource record host = 'mail.' + @domain_name value = ['www.' + @domain_name] @@ -164,8 +149,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test("add a MX resource record") { - pending if Fog.mocking? - # create a MX resource record host = @domain_name value = ['7 mail.' + @domain_name] @@ -186,8 +169,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test("add an ALIAS resource record") { - pending if Fog.mocking? - # create a load balancer @elb_connection.create_load_balancer(["us-east-1a"], "fog", [{"Protocol" => "HTTP", "LoadBalancerPort" => "80", "InstancePort" => "80"}]) @@ -213,7 +194,7 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do puts "DNS Name (ELB): #{dns_name}" puts "Zone ID for Route 53: #{@zone_id}" - sleep 120 + sleep 120 unless Fog.mocking? response = @r53_connection.change_resource_record_sets(@zone_id, change_batch, options) if response.status == 200 change_id = response.body['Id'] @@ -225,15 +206,11 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } tests("list resource records").formats(AWS::DNS::Formats::LIST_RESOURCE_RECORD_SETS) { - pending if Fog.mocking? - # get resource records for zone @r53_connection.list_resource_record_sets(@zone_id).body } test("delete #{@new_records.count} resource records") { - pending if Fog.mocking? - result = true change_batch = [] @@ -252,8 +229,6 @@ Shindo.tests('Fog::DNS[:aws] | DNS requests', ['aws', 'dns']) do } test("delete hosted zone #{@zone_id}") { - pending if Fog.mocking? - # cleanup the ELB as well @elb_connection.delete_load_balancer("fog") From 245869d1b3b448079699956ee62f2e756d29555d Mon Sep 17 00:00:00 2001 From: geemus Date: Fri, 15 Nov 2013 10:04:37 -0600 Subject: [PATCH 5/5] [aws|storage] warn/load unf as needed --- lib/fog/aws/storage.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/fog/aws/storage.rb b/lib/fog/aws/storage.rb index ef487e6e0..9bc560191 100644 --- a/lib/fog/aws/storage.rb +++ b/lib/fog/aws/storage.rb @@ -174,6 +174,14 @@ module Fog # NOTE: differs from Fog::AWS.escape by NOT escaping `/` def escape(string) + unless @unf_loaded_or_warned + begin + require('unf/normalizer') + rescue LoadError + Fog::Logger.warning("Unable to load the 'unf' gem. Your AWS strings may not be properly encoded.") + end + @unf_loaded_or_warned = true + end string = defined?(::UNF::Normalizer) ? ::UNF::Normalizer.normalize(string, :nfc) : string string.gsub(/([^a-zA-Z0-9_.\-~\/]+)/) { "%" + $1.unpack("H2" * $1.bytesize).join("%").upcase