vcr 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.travis.yml +2 -1
  2. data/CHANGELOG.md +58 -1
  3. data/Gemfile +0 -6
  4. data/README.md +12 -3
  5. data/Rakefile +2 -2
  6. data/features/.nav +2 -0
  7. data/features/cassettes/allow_unused_http_interactions.feature +86 -0
  8. data/features/cassettes/naming.feature +3 -2
  9. data/features/cassettes/persistence.feature +63 -0
  10. data/features/configuration/debug_logging.feature +3 -3
  11. data/features/configuration/ignore_request.feature +1 -1
  12. data/features/hooks/after_http_request.feature +1 -1
  13. data/features/step_definitions/cli_steps.rb +33 -0
  14. data/features/support/env.rb +0 -1
  15. data/lib/vcr.rb +13 -0
  16. data/lib/vcr/cassette.rb +57 -44
  17. data/lib/vcr/cassette/{reader.rb → erb_renderer.rb} +9 -11
  18. data/lib/vcr/cassette/http_interaction_list.rb +18 -0
  19. data/lib/vcr/cassette/persisters.rb +42 -0
  20. data/lib/vcr/cassette/persisters/file_system.rb +60 -0
  21. data/lib/vcr/cassette/serializers.rb +1 -1
  22. data/lib/vcr/cassette/serializers/json.rb +1 -1
  23. data/lib/vcr/cassette/serializers/psych.rb +1 -1
  24. data/lib/vcr/cassette/serializers/syck.rb +1 -1
  25. data/lib/vcr/cassette/serializers/yaml.rb +1 -1
  26. data/lib/vcr/configuration.rb +49 -25
  27. data/lib/vcr/errors.rb +6 -0
  28. data/lib/vcr/library_hooks/excon.rb +1 -1
  29. data/lib/vcr/library_hooks/fakeweb.rb +16 -3
  30. data/lib/vcr/library_hooks/faraday.rb +13 -0
  31. data/lib/vcr/library_hooks/typhoeus.rb +5 -1
  32. data/lib/vcr/library_hooks/webmock.rb +90 -35
  33. data/lib/vcr/middleware/faraday.rb +12 -4
  34. data/lib/vcr/request_handler.rb +10 -2
  35. data/lib/vcr/structs.rb +31 -7
  36. data/lib/vcr/version.rb +1 -1
  37. data/script/ci.sh +14 -0
  38. data/spec/spec_helper.rb +8 -1
  39. data/spec/support/shared_example_groups/hook_into_http_library.rb +31 -1
  40. data/spec/vcr/cassette/{reader_spec.rb → erb_renderer_spec.rb} +15 -21
  41. data/spec/vcr/cassette/http_interaction_list_spec.rb +15 -0
  42. data/spec/vcr/cassette/persisters/file_system_spec.rb +64 -0
  43. data/spec/vcr/cassette/persisters_spec.rb +39 -0
  44. data/spec/vcr/cassette/serializers_spec.rb +4 -3
  45. data/spec/vcr/cassette_spec.rb +65 -41
  46. data/spec/vcr/configuration_spec.rb +33 -2
  47. data/spec/vcr/library_hooks/fakeweb_spec.rb +11 -0
  48. data/spec/vcr/library_hooks/faraday_spec.rb +24 -0
  49. data/spec/vcr/library_hooks/webmock_spec.rb +82 -2
  50. data/spec/vcr/middleware/faraday_spec.rb +32 -0
  51. data/spec/vcr/structs_spec.rb +60 -20
  52. data/spec/vcr/util/hooks_spec.rb +7 -0
  53. data/spec/vcr_spec.rb +7 -0
  54. data/vcr.gemspec +2 -2
  55. metadata +31 -26
  56. data/Guardfile +0 -9
  57. data/script/FullBuildRakeFile +0 -56
  58. data/script/full_build +0 -1
  59. data/script/spec +0 -1
data/.travis.yml CHANGED
@@ -1,7 +1,8 @@
1
1
  language: ruby
2
2
  env: CUCUMBER_FORMAT=progress
3
+ before_install: "gem uninstall json -a -I"
3
4
  bundler_args: --without extras
4
- script: "bundle exec rake ci:build --trace"
5
+ script: "script/ci.sh"
5
6
  rvm:
6
7
  - 1.8.7
7
8
  - 1.9.2
data/CHANGELOG.md CHANGED
@@ -1,6 +1,63 @@
1
1
  ## In git
2
2
 
3
- [Full Changelog](http://github.com/myronmarston/vcr/compare/v2.1.1...master)
3
+ [Full Changelog](http://github.com/myronmarston/vcr/compare/v2.2.0...master)
4
+
5
+ ## 2.2.0 (May 31, 2012)
6
+
7
+ [Full Changelog](http://github.com/myronmarston/vcr/compare/v2.1.1...v2.2.0)
8
+
9
+ Enhancements:
10
+
11
+ * Add new `:persist_with` cassette option. It allows you to provide a
12
+ customized persistence implementation so you can persist it to
13
+ something other than disk (i.e. a key-value store or a database).
14
+ Thanks to [Chris Le](https://github.com/chrisle) for the idea and
15
+ help with the implementation.
16
+ * Allow requests to be stubbed by external libraries (e.g. WebMock,
17
+ FakeWeb or Typhoeus) without needing to turn VCR off.
18
+ * Add new `:allow_unused_http_interactions` cassette option. When set
19
+ to false, an error will be raised when a cassette is ejected and
20
+ there are remaining unused HTTP interactions. Thanks to
21
+ [Mattias Putman](https://github.com/challengee) for the idea
22
+ and initial implementation.
23
+
24
+ Bug Fixes:
25
+
26
+ * Fix `after_http_request` to handle symbol request predicate filters
27
+ (e.g. `:ignored?`, `:stubbed?`, `:recordable?`, `:unhandled?`, `:real?`)
28
+ properly. Previously using one of these would raise an ArgumentError.
29
+ Thanks to [playupchris](https://github.com/playupchris) for reporting
30
+ the bug and providing a fix.
31
+ * Fix FakeWeb hook so that it no longer breaks
32
+ `FakeWeb.allow_net_connect?` with arguments. Thanks to
33
+ [Ingemar](https://github.com/ingemar) for reporting the bug and
34
+ providing a fix.
35
+ * Fix WebMock hook so that it no longer breaks
36
+ `WebMock.net_connect_allowed?` with arguments. Thanks to
37
+ [Gordon Wilson](https://github.com/gordoncww) for reporting the bug and
38
+ providing a fix.
39
+ * Print a warning when VCR is used with a poorly behaved Faraday
40
+ connection stack that has a middleware after the HTTP adapter.
41
+ VCR may work improperly in this case.
42
+ * Raise an error if a response object is recorded with a non-string
43
+ body. This fails early and indicates the problem rather than failing
44
+ later with a strange error.
45
+ * Fix `filter_sensitive_data`/`define_cassette_placeholder` so that they
46
+ handle non-strings gracefully (e.g. the port number as a Fixnum).
47
+ * Gracefully handle Faraday connection stacks that do not explicitly
48
+ specify an HTTP adapter. Thanks to [Patrick Roby](https://github.com/proby)
49
+ for reporting the bug.
50
+ * Work around a bug in WebMock's em-http-request adapter that prevented
51
+ VCR from working when using the `:redirects` option with
52
+ em-http-request. This change is just a work around. It fixes the main
53
+ problem, but some features (such as the http request hooks) may not
54
+ work properly for this case. The bug will ultimately need to be
55
+ [fixed in WebMock](https://github.com/bblimke/webmock/pull/185).
56
+ Thanks to [Mark Abramov](https://github.com/markiz) for reporting
57
+ the bug and providing a great example test case.
58
+ * Fix bug in handling of Faraday requests with multipart uploads.
59
+ Thanks to [Tyler Hunt](https://github.com/tylerhunt) for reporting
60
+ and fixing the bug.
4
61
 
5
62
  ## 2.1.1 (April 24, 2012)
6
63
 
data/Gemfile CHANGED
@@ -11,8 +11,6 @@ gem 'yard'
11
11
 
12
12
  # Additional gems that are useful, but not required for development.
13
13
  group :extras do
14
- gem 'guard-rspec'
15
- gem 'growl'
16
14
  gem 'relish', '~> 0.5.0'
17
15
  gem 'fuubar'
18
16
  gem 'fuubar-cucumber'
@@ -20,10 +18,6 @@ group :extras do
20
18
  gem 'redcarpet', '~> 1.17.2'
21
19
  gem 'github-markup'
22
20
 
23
- platforms :mri do
24
- gem 'rb-fsevent'
25
- end
26
-
27
21
  platforms :mri_18, :jruby do
28
22
  gem 'ruby-debug'
29
23
  end
data/README.md CHANGED
@@ -142,9 +142,12 @@ Thanks also to the following people who have contributed patches or helpful sugg
142
142
  * [Bradley Isotope](https://github.com/bradleyisotope)
143
143
  * [Carlos Kirkconnell](https://github.com/kirkconnell)
144
144
  * [Chad Jolly](https://github.com/cjolly)
145
+ * [Chris Le](https://github.com/chrisle)
145
146
  * [Eric Allam](http://github.com/rubymaverick)
146
147
  * [Ezekiel Templin](https://github.com/ezkl)
147
148
  * [Flaviu Simihaian](https://github.com/closedbracket)
149
+ * [Gordon Wilson](https://github.com/gordoncww)
150
+ * [Ingemar](https://github.com/ingemar)
148
151
  * [Jeff Pollard](https://github.com/Fluxx)
149
152
  * [Justin Smestad](https://github.com/jsmestad)
150
153
  * [Karl Baum](https://github.com/kbaum)
@@ -154,25 +157,31 @@ Thanks also to the following people who have contributed patches or helpful sugg
154
157
  * [Oliver Searle-Barnes](https://github.com/opsb)
155
158
  * [Omer Rauchwerger](https://github.com/rauchy)
156
159
  * [Paco Guzmán](https://github.com/pacoguzman)
160
+ * [playupchris](https://github.com/playupchris)
157
161
  * [Ryan Bates](https://github.com/ryanb)
158
162
  * [Sathya Sekaran](https://github.com/sfsekaran)
163
+ * [Tyler Hunt](https://github.com/tylerhunt)
159
164
  * [Wesley Beary](https://github.com/geemus)
160
165
 
161
166
  ## Ports in other languages
162
167
 
163
168
  * [Betamax](https://github.com/robfletcher/betamax) (Groovy)
164
- * [VCR.js](https://github.com/elcuervo/vcr.js) (JavaScript)
165
- * [TapeDeck.js](https://github.com/EndangeredMassa/TapeDeck.js) (JavaScript)
166
169
  * [Mimic](https://github.com/acoulton/mimic) (PHP/Kohana)
170
+ * [Nock](https://github.com/flatiron/nock) (JavaScript/Node)
171
+ * [NSURLConnectionVCR](https://bitbucket.org/martijnthe/nsurlconnectionvcr) (Objective C)
172
+ * [TapeDeck.js](https://github.com/EndangeredMassa/TapeDeck.js) (JavaScript)
173
+ * [VCR.js](https://github.com/elcuervo/vcr.js) (JavaScript)
174
+ * [VCR.py](https://github.com/kevin1024/vcrpy) (Python)
175
+ * [vcr-clj](https://github.com/ifesdjeen/vcr-clj) (Clojure)
167
176
 
168
177
  ## Similar Libraries in Ruby
169
178
 
170
179
  * [Ephemeral Response](https://github.com/sandro/ephemeral_response)
171
180
  * [Net::HTTP Spy](http://github.com/martinbtt/net-http-spy)
172
181
  * [NetRecorder](https://github.com/chrisyoung/netrecorder)
182
+ * [REST-assured](https://github.com/BBC/REST-assured)
173
183
  * [Stale Fish](https://github.com/jsmestad/stale_fish)
174
184
  * [WebFixtures](http://github.com/trydionel/web_fixtures)
175
- * [REST-assured](https://github.com/BBC/REST-assured)
176
185
 
177
186
  ## Copyright
178
187
 
data/Rakefile CHANGED
@@ -26,10 +26,10 @@ task :default => [:spec, :cucumber]
26
26
  desc "Ensures we keep up 100% YARD coverage"
27
27
  task :yard_coverage do
28
28
  coverage_stats = `yard stats --list-undoc 2>&1`
29
+ puts coverage_stats
29
30
  if coverage_stats.include?('100.00% documented')
30
- puts "Nice work! 100% documentation coverage"
31
+ puts "\nNice work! 100% documentation coverage"
31
32
  else
32
- puts coverage_stats
33
33
  raise "Documentation coverage is less than 100%"
34
34
  end
35
35
  end
data/features/.nav CHANGED
@@ -13,6 +13,8 @@
13
13
  - exclusive.feature
14
14
  - update_content_length_header.feature
15
15
  - decompress.feature
16
+ - persistence.feature
17
+ - allow_unused_http_interactions.feature
16
18
  - record_modes:
17
19
  - once.feature
18
20
  - new_episodes.feature
@@ -0,0 +1,86 @@
1
+ Feature: Allow Unused HTTP Interactions
2
+
3
+ If set to false, this cassette option will cause VCR to raise an error
4
+ when a cassette is ejected and there are unused HTTP interactions remaining.
5
+
6
+ It verifies that all requests included in the cassette were made, and allows
7
+ VCR to function a bit like a mock object at the HTTP layer.
8
+
9
+ The option defaults to true (mostly for backwards compatibility).
10
+
11
+ Background:
12
+ Given a file named "vcr_config.rb" with:
13
+ """ruby
14
+ require 'vcr'
15
+
16
+ VCR.configure do |c|
17
+ c.hook_into :fakeweb
18
+ c.cassette_library_dir = 'cassettes'
19
+ end
20
+ """
21
+ And a previously recorded cassette file "cassettes/example.yml" with:
22
+ """
23
+ ---
24
+ http_interactions:
25
+ - request:
26
+ method: get
27
+ uri: http://example.com/foo
28
+ body:
29
+ encoding: UTF-8
30
+ string: ""
31
+ headers: {}
32
+ response:
33
+ status:
34
+ code: 200
35
+ message: OK
36
+ headers:
37
+ Content-Length:
38
+ - "5"
39
+ body:
40
+ encoding: UTF-8
41
+ string: Hello
42
+ http_version: "1.1"
43
+ recorded_at: Tue, 01 Nov 2011 04:58:44 GMT
44
+ recorded_with: VCR 2.0.0
45
+ """
46
+
47
+ Scenario: Unused HTTP interactions are allowed by default
48
+ Given a file named "allowed_by_default.rb" with:
49
+ """ruby
50
+ require 'vcr_config'
51
+
52
+ VCR.use_cassette("example") do
53
+ # no requests
54
+ end
55
+ """
56
+ When I run `ruby allowed_by_default.rb`
57
+ Then it should pass
58
+
59
+ Scenario: Error raised if option is false and there are unused interactions
60
+ Given a file named "disallowed_with_no_requests.rb" with:
61
+ """ruby
62
+ require 'vcr_config'
63
+
64
+ VCR.use_cassette("example", :allow_unused_http_interactions => false) do
65
+ # no requests
66
+ end
67
+ """
68
+ When I run `ruby disallowed_with_no_requests.rb`
69
+ Then it should fail with an error like:
70
+ """
71
+ There are unused HTTP interactions left in the cassette:
72
+ - [get http://example.com/foo] => [200 "Hello"]
73
+ """
74
+
75
+ Scenario: No error raised if option is false and all interactions are used
76
+ Given a file named "disallowed_with_all_requests.rb" with:
77
+ """ruby
78
+ require 'vcr_config'
79
+
80
+ VCR.use_cassette("example", :allow_unused_http_interactions => false) do
81
+ Net::HTTP.get_response(URI("http://example.com/foo"))
82
+ end
83
+ """
84
+ When I run `ruby disallowed_with_all_requests.rb`
85
+ Then it should pass
86
+
@@ -1,8 +1,9 @@
1
1
  Feature: Naming
2
2
 
3
3
  When inserting or using a cassette, the first argument is the cassette name.
4
- You can use any string for the name. VCR will sanitize the string before
5
- using it as a file name, so that it is a file-system friendly file name.
4
+ You can use any string for the name. If you use the default `:file_system`
5
+ storage backend, VCR will sanitize the string before using it as a file name,
6
+ so that it is a file-system friendly file name.
6
7
 
7
8
  Scenario: Name sanitizing
8
9
  Given a file named "name_sanitizing.rb" with:
@@ -0,0 +1,63 @@
1
+ Feature: Cassette Persistence
2
+
3
+ By default, cassettes will be persisted to the file system. However, you
4
+ can easily configure VCR to persist the cassettes to a database, a key-value
5
+ store, or anywhere you like.
6
+
7
+ To use something besides the file system, you must provide an object
8
+ that provides a hash-like interface:
9
+
10
+ * `persister[name]` should return the content previously persisted for the
11
+ given cassette name.
12
+ * `persister[name] = content` should persist the content for the
13
+ given cassette name.
14
+
15
+ Register this object with VCR, and then you can configure all cassettes
16
+ to use it (using the `default_cassette_options`) or just some cassettes
17
+ to use it (by passing it as an option to individual cassettes).
18
+
19
+ Scenario: Persist cassettes in Redis
20
+ Given the redis DB has no data
21
+ And a file named "use_redis.rb" with:
22
+ """ruby
23
+ if ARGV.include?('--with-server')
24
+ start_sinatra_app(:port => 7777) do
25
+ get('/') { "Hello" }
26
+ end
27
+ end
28
+
29
+ require 'redis'
30
+
31
+ class RedisCassettePersister
32
+ def initialize(redis)
33
+ @redis = redis
34
+ end
35
+
36
+ def [](name)
37
+ @redis.get(name)
38
+ end
39
+
40
+ def []=(name, content)
41
+ @redis.set(name, content)
42
+ end
43
+ end
44
+
45
+ require 'vcr'
46
+
47
+ VCR.configure do |c|
48
+ c.hook_into :fakeweb
49
+ c.cassette_persisters[:redis] = RedisCassettePersister.new(Redis.connect)
50
+ c.default_cassette_options = { :persist_with => :redis }
51
+ end
52
+
53
+ VCR.use_cassette("redis_example") do
54
+ response = Net::HTTP.get_response('localhost', '/', 7777)
55
+ puts "Response: #{response.body}"
56
+ end
57
+ """
58
+ When I run `ruby use_redis.rb --with-server`
59
+ Then it should pass with "Hello"
60
+ And the value stored at the redis key "redis_example.yml" should include "Hello"
61
+
62
+ When I run `ruby use_redis.rb`
63
+ Then it should pass with "Hello"
@@ -30,7 +30,7 @@ Feature: Debug Logging
30
30
  When I run `ruby debug_logger.rb record.log --with-server`
31
31
  Then the file "record.log" should contain exactly:
32
32
  """
33
- [Cassette: 'example'] Initialized with options: {:record=>:once, :match_requests_on=>[:method, :uri], :serialize_with=>:yaml}
33
+ [Cassette: 'example'] Initialized with options: {:record=>:once, :match_requests_on=>[:method, :uri], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system}
34
34
  [fakeweb] Handling request: [get http://localhost:7777/] (disabled: false)
35
35
  [Cassette: 'example'] Initialized HTTPInteractionList with request matchers [:method, :uri] and 0 interaction(s): { }
36
36
  [fakeweb] Identified request type (recordable) for [get http://localhost:7777/]
@@ -40,13 +40,13 @@ Feature: Debug Logging
40
40
  When I run `ruby debug_logger.rb playback.log`
41
41
  Then the file "playback.log" should contain exactly:
42
42
  """
43
- [Cassette: 'example'] Initialized with options: {:record=>:once, :match_requests_on=>[:method, :uri], :serialize_with=>:yaml}
43
+ [Cassette: 'example'] Initialized with options: {:record=>:once, :match_requests_on=>[:method, :uri], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system}
44
44
  [fakeweb] Handling request: [get http://localhost:7777/] (disabled: false)
45
45
  [Cassette: 'example'] Initialized HTTPInteractionList with request matchers [:method, :uri] and 1 interaction(s): { [get http://localhost:7777/] => [200 "Hello World"] }
46
46
  [Cassette: 'example'] Checking if [get http://localhost:7777/] matches [get http://localhost:7777/] using [:method, :uri]
47
47
  [Cassette: 'example'] method (matched): current request [get http://localhost:7777/] vs [get http://localhost:7777/]
48
48
  [Cassette: 'example'] uri (matched): current request [get http://localhost:7777/] vs [get http://localhost:7777/]
49
49
  [Cassette: 'example'] Found matching interaction for [get http://localhost:7777/] at index 0: [200 "Hello World"]
50
- [fakeweb] Identified request type (stubbed) for [get http://localhost:7777/]
50
+ [fakeweb] Identified request type (stubbed_by_vcr) for [get http://localhost:7777/]
51
51
 
52
52
  """
@@ -65,7 +65,7 @@ Feature: Ignore Request
65
65
  puts response_body_for(:get, "http://localhost:8888/")
66
66
  """
67
67
  When I run `ruby ignore_request.rb`
68
- Then it should fail with:
68
+ Then it should fail with an error like:
69
69
  """
70
70
  An HTTP request has been made that VCR does not know how to handle:
71
71
  GET http://localhost:8888/
@@ -33,7 +33,7 @@ Feature: after_http_request hook
33
33
  <configuration>
34
34
  c.cassette_library_dir = 'cassettes'
35
35
  c.ignore_localhost = true
36
- c.after_http_request(lambda { |req| req.uri =~ /foo/ }) do |request, response|
36
+ c.after_http_request(:ignored?, lambda { |req| req.uri =~ /foo/ }) do |request, response|
37
37
  puts "Response for #{request.method} #{request.uri}: #{response.body}"
38
38
  end
39
39
  end
@@ -73,6 +73,13 @@ module VCRHelpers
73
73
  File.open(file_name, 'w') { |f| f.write(file) }
74
74
  end
75
75
  end
76
+
77
+ def redis
78
+ @redis ||= begin
79
+ require 'redis'
80
+ Redis.connect
81
+ end
82
+ end
76
83
  end
77
84
  World(VCRHelpers)
78
85
 
@@ -92,6 +99,10 @@ Given /^it is (.*)$/ do |date_string|
92
99
  set_env('DATE_STRING', date_string)
93
100
  end
94
101
 
102
+ Given /^the redis DB has no data$/ do
103
+ redis.flushdb
104
+ end
105
+
95
106
  When /^I modify the file "([^"]*)" to replace "([^"]*)" with "([^"]*)"$/ do |file_name, orig_text, new_text|
96
107
  modify_file(file_name, orig_text, new_text)
97
108
  end
@@ -112,6 +123,20 @@ Then /^it should (pass|fail) with "([^"]*)"$/ do |pass_fail, partial_output|
112
123
  assert_exit_status_and_partial_output(pass_fail == 'pass', partial_output)
113
124
  end
114
125
 
126
+ Then /^it should (pass|fail) with an error like:$/ do |pass_fail, partial_output|
127
+ assert_success(pass_fail == 'pass')
128
+
129
+ # different implementations place the exception class at different
130
+ # places relative to the message (i.e. with a multiline error message)
131
+ process_output = all_output.gsub(/\s*\(VCR::Errors::\w+\)/, '')
132
+
133
+ # Some implementations include extra leading spaces, for some reason...
134
+ process_output.gsub!(/^\s*/, '')
135
+ partial_output.gsub!(/^\s*/, '')
136
+
137
+ assert_partial_output(partial_output, process_output)
138
+ end
139
+
115
140
  Then /^the output should contain each of the following:$/ do |table|
116
141
  table.raw.flatten.each do |string|
117
142
  assert_partial_output(string, all_output)
@@ -168,3 +193,11 @@ Then /^the cassette "([^"]*)" should have the following response bodies:$/ do |f
168
193
  actual_response_bodies.should =~ expected_response_bodies
169
194
  end
170
195
 
196
+ Then /^the value stored at the redis key "([^"]*)" should include "([^"]*)"$/ do |key, value_fragment|
197
+ redis.get(key).should include(value_fragment)
198
+ end
199
+
200
+ Then /^it should (pass|fail)$/ do |pass_fail|
201
+ assert_success(pass_fail == 'pass')
202
+ end
203
+
@@ -2,7 +2,6 @@ require 'rubygems'
2
2
  require 'bundler'
3
3
  Bundler.setup
4
4
 
5
- require 'limited_red/plugins/cucumber'
6
5
  require 'ruby-debug' if !defined?(RUBY_ENGINE) && RUBY_VERSION != '1.9.3' && !ENV['CI']
7
6
 
8
7
  require 'aruba/cucumber'