appengine-apis 0.0.2 → 0.0.3

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.
@@ -19,6 +19,7 @@
19
19
  # Switches the Merb Logger class to use the Google App Engine logging API.
20
20
 
21
21
  require 'merb-core/logger'
22
+ require 'appengine-apis/apiproxy.rb'
22
23
 
23
24
  module Merb # :nodoc:
24
25
 
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/ruby1.8 -w
2
+ #
3
+ # Copyright:: Copyright 2009 Google Inc.
4
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'java'
20
+
21
+ module AppEngine
22
+
23
+ # Helper methods to locate the App Engine SDK and add it to the class path.
24
+ module SDK
25
+ class << self
26
+
27
+ # Tries to load the ApiProxy class.
28
+ def load_apiproxy
29
+ with_jars(%w{lib impl appengine-api.jar}) do
30
+ return Java.ComGoogleApphostingApi.ApiProxy
31
+ end
32
+ end
33
+
34
+ # Tries to load the ApiProxyLocalFactory class.
35
+ def load_local_apiproxy_factory
36
+ with_jars(%w{lib shared appengine-local-runtime-shared.jar},
37
+ %w{lib impl appengine-api-stubs.jar},
38
+ %w{lib impl appengine-local-runtime.jar}
39
+ ) do
40
+ return Java.ComGoogleAppengineToolsDevelopment.ApiProxyLocalFactory
41
+ end
42
+ end
43
+
44
+ def with_jars(*jars) # :nodoc:
45
+ begin
46
+ failed = false
47
+ yield
48
+ rescue NameError => ex
49
+ if failed
50
+ raise ex
51
+ else
52
+ failed = true
53
+ jars.each do |jar|
54
+ $CLASSPATH << sdk_path(*jar)
55
+ end
56
+ retry
57
+ end
58
+ end
59
+ end
60
+
61
+ # Tries to find the Google App Engine SDK for Java.
62
+ #
63
+ # Looks for appcfg.sh in these directories (in order):
64
+ # - ENV['APPENGINE_JAVA_SDK']/bin
65
+ # - each directory in ENV['PATH']
66
+ # - '/usr/local/appengine-java-sdk/bin'
67
+ # - 'c:\appengine-java-sdk\bin'
68
+ #
69
+ # Returns File.join(sdk_directory, *pieces)
70
+ #
71
+ def sdk_path(*pieces)
72
+ puts "sdk_path(#{pieces.join('/')})"
73
+ unless @sdk_path
74
+ base_path = File.join(ENV['APPENGINE_JAVA_SDK'] || '', 'bin')
75
+ exec_paths = ENV['PATH'].split(File::PATH_SEPARATOR)
76
+ exec_paths << '/usr/local/appengine-java-sdk/bin'
77
+ exec_paths << 'c:\appengine-java-sdk\bin'
78
+ # TODO also check for reggae
79
+
80
+ while !exec_paths.empty?
81
+ if File.exist?(File.join(base_path, 'appcfg.sh'))
82
+ @sdk_path = File.dirname(base_path)
83
+ return File.join(@sdk_path, *pieces)
84
+ end
85
+ base_path = exec_paths.shift
86
+ end
87
+ raise "Unable to locate the Google App Engine SDK for Java."
88
+ end
89
+ File.join(@sdk_path, *pieces)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/ruby1.8 -w
2
2
  #
3
- # Copyright:: Copyright 2007 Google Inc.
3
+ # Copyright:: Copyright 2009 Google Inc.
4
4
  # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,8 +31,6 @@ module AppEngine
31
31
  # To run outside this environment, you need to install a test environment and
32
32
  # api stubs.
33
33
  module Testing
34
- import com.google.appengine.tools.development.ApiProxyLocalFactory
35
- import com.google.appengine.api.datastore.dev.LocalDatastoreService
36
34
 
37
35
  class TestEnv # :nodoc:
38
36
  include AppEngine::ApiProxy::Environment
@@ -43,7 +41,9 @@ module AppEngine
43
41
  def initialize
44
42
  @appid = "test"
45
43
  @version = "1.0"
46
- @auth_domain = "gmail.com"
44
+ @auth_domain = @request_namespace = "gmail.com"
45
+ @default_namespace = ""
46
+ @email = ""
47
47
  end
48
48
 
49
49
  def getAppId
@@ -59,7 +59,7 @@ module AppEngine
59
59
  end
60
60
 
61
61
  def isLoggedIn
62
- !(@email.nil? || @auth_domain.nil?)
62
+ !(@email.nil? || @auth_domain.nil? || @email.empty?)
63
63
  end
64
64
 
65
65
  def isAdmin
@@ -80,7 +80,11 @@ module AppEngine
80
80
 
81
81
  def setDefaultNamespace(s)
82
82
  @default_namespace = s
83
- end
83
+ end
84
+
85
+ def inspect
86
+ "#<TestEnv>"
87
+ end
84
88
  end
85
89
 
86
90
  class << self
@@ -99,7 +103,7 @@ module AppEngine
99
103
  end
100
104
 
101
105
  def factory # :nodoc:
102
- @factory ||= ApiProxyLocalFactory.new
106
+ @factory ||= AppEngine::SDK.load_local_apiproxy_factory.new
103
107
  end
104
108
 
105
109
  # The application directory, or '.' if not set.
@@ -124,13 +128,15 @@ module AppEngine
124
128
  def install_test_datastore
125
129
  self.app_dir = '.' if app_dir.nil?
126
130
  delegate = factory.create
131
+ lds = Java.ComGoogleAppengineApiDatastoreDev.LocalDatastoreService
132
+
127
133
  delegate.set_property(
128
- LocalDatastoreService::NO_STORAGE_PROPERTY, "true")
134
+ lds::NO_STORAGE_PROPERTY, "true")
129
135
  delegate.set_property(
130
- LocalDatastoreService::MAX_QUERY_LIFETIME_PROPERTY,
136
+ lds::MAX_QUERY_LIFETIME_PROPERTY,
131
137
  java.lang.Integer::MAX_VALUE.to_s)
132
138
  delegate.set_property(
133
- LocalDatastoreService::MAX_TRANSACTION_LIFETIME_PROPERTY,
139
+ lds::MAX_TRANSACTION_LIFETIME_PROPERTY,
134
140
  java.lang.Integer::MAX_VALUE.to_s)
135
141
  ApiProxy::setDelegate(delegate)
136
142
  delegate
@@ -149,6 +155,45 @@ module AppEngine
149
155
  delegate
150
156
  end
151
157
 
158
+ # Loads stub API implementations if no API implementation is
159
+ # currently configured.
160
+ #
161
+ # Sets up a datastore saved to disk in +app_dir+. +app_dir+ defaults
162
+ # to ENV['APPLICATION_ROOT'] or '.' if not specified.
163
+ #
164
+ # Does nothing is APIs are already configured (e.g. in production).
165
+ #
166
+ # As a shortcut you can use
167
+ # require 'appengine-apis/local_boot'
168
+ #
169
+ def boot(app_dir=nil)
170
+ app_dir ||= ENV['APPLICATION_ROOT'] || '.'
171
+ unless AppEngine::ApiProxy.current_environment
172
+ env = install_test_env
173
+ appid = get_app_id(app_dir)
174
+ env.appid = appid if appid
175
+ end
176
+ unless AppEngine::ApiProxy.delegate
177
+ self.app_dir = app_dir
178
+ install_api_stubs
179
+ end
180
+ end
181
+
182
+ # Looks for app.yaml or WEB-INF/appengine-web.xml in +app_dir+
183
+ # and parses the application id.
184
+ def get_app_id(app_dir)
185
+ aeweb_path = File.join(app_dir, 'WEB-INF', 'appengine-web.xml')
186
+ app_yaml_path = File.join(app_dir, 'app.yaml')
187
+ if File.exist?(app_yaml_path)
188
+ app_yaml = YAML.load(File.open(app_yaml_path))
189
+ return app_yaml['application']
190
+ elsif File.exist?(aeweb_path)
191
+ require 'rexml/document'
192
+ aeweb = REXML::Document.new(File.new(aeweb_path))
193
+ return aeweb.root.elements['application'].text
194
+ end
195
+ end
196
+
152
197
  end
153
198
 
154
199
  end
@@ -25,6 +25,11 @@ module AppEngine
25
25
  #
26
26
  # Chunked and hanging requests are not supported, and all content will be
27
27
  # returned in a single block.
28
+ #
29
+ # URLFetch::HTTP also provides a drop-in replacement for Net::HTTP.
30
+ # To replace the standard implementation throughout your app you can do:
31
+ # require 'appengine-apis/urlfetch'
32
+ # Net::HTTP = AppEngine::URLFetch::HTTP
28
33
  module URLFetch
29
34
  import com.google.appengine.api.urlfetch.FetchOptions
30
35
  import com.google.appengine.api.urlfetch.HTTPHeader
@@ -44,12 +49,12 @@ module AppEngine
44
49
 
45
50
  module_function
46
51
 
47
- #Fetches the given HTTP URL, blocking until the result is returned.
52
+ # Fetches the given HTTP URL, blocking until the result is returned.
48
53
  #
49
54
  # Supported options:
50
55
  # [:method] GET, POST, HEAD, PUT, or DELETE
51
56
  # [:payload] POST or PUT payload (implies method is not GET, HEAD,
52
- # or DELETE)
57
+ # or DELETE)
53
58
  # [:headers]
54
59
  # HTTP headers to send with the request. May be a Hash or
55
60
  # Net::HTTPHeaders.
@@ -194,5 +194,16 @@ describe AppEngine::Datastore::Entity do
194
194
  end
195
195
  props.should == {}
196
196
  end
197
-
197
+
198
+ it "should support update" do
199
+ @entity.update('foo' => 'bar', 'count' => 3)
200
+ @entity[:foo].should == 'bar'
201
+ @entity[:count].should == 3
202
+ end
203
+
204
+ it "should support to_hash" do
205
+ props = {'foo' => 'bar', 'count' => 3}
206
+ @entity.merge!(props)
207
+ @entity.to_hash.should == props
208
+ end
198
209
  end
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/ruby1.8 -w
2
+ #
3
+ # Copyright:: Copyright 2009 Google Inc.
4
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require File.dirname(__FILE__) + '/spec_helper.rb'
20
+ require 'appengine-apis/apiproxy'
21
+ require 'appengine-apis/mail'
22
+
23
+ MailMessage = JavaUtilities.get_proxy_or_package_under_package(
24
+ com.google.appengine.api.mail,
25
+ 'MailServicePb$MailMessage'
26
+ )
27
+
28
+ describe AppEngine::Mail do
29
+ before :each do
30
+ @delegate = mock_delegate
31
+ AppEngine::ApiProxy.set_delegate(@delegate)
32
+ end
33
+
34
+ def should_send(message)
35
+ should_make_call('Send', message)
36
+ end
37
+
38
+ def should_send_to_admins(message)
39
+ should_make_call('SendToAdmins', message)
40
+ end
41
+
42
+ def should_make_call(call, message)
43
+ @delegate.should_receive(:makeSyncCall).with(
44
+ anything, 'mail', call, proto(MailMessage, message))
45
+ end
46
+
47
+ it "should send simple message" do
48
+ should_send({
49
+ :sender => 'ribrdb@example.com',
50
+ :to => ['bob@example.com'],
51
+ :subject => 'Howdy',
52
+ :text_body => 'Sup?'
53
+ })
54
+ AppEngine::Mail.send(
55
+ 'ribrdb@example.com', 'bob@example.com',
56
+ 'Howdy', 'Sup?')
57
+ end
58
+
59
+ it "should support cc" do
60
+ should_send({
61
+ :sender => 'ribrdb@example.com',
62
+ :to => ['bob@example.com'],
63
+ :cc => ['fred@example.com'],
64
+ :subject => 'Howdy',
65
+ :text_body => 'Sup?'
66
+ })
67
+ AppEngine::Mail.send(
68
+ 'ribrdb@example.com', 'bob@example.com',
69
+ 'Howdy', 'Sup?', :cc => 'fred@example.com')
70
+ end
71
+
72
+ it "should support bcc" do
73
+ should_send({
74
+ :sender => 'ribrdb@example.com',
75
+ :to => ['bob@example.com'],
76
+ :bcc => ['fred@example.com'],
77
+ :subject => 'Howdy',
78
+ :text_body => 'Sup?'
79
+ })
80
+ AppEngine::Mail.send(
81
+ 'ribrdb@example.com', 'bob@example.com',
82
+ 'Howdy', 'Sup?', :bcc => 'fred@example.com')
83
+ end
84
+
85
+ it "should support multiple recipients" do
86
+ should_send({
87
+ :sender => 'ribrdb@example.com',
88
+ :to => ['bob@example.com', 'fred@example.com'],
89
+ :cc => ['bob@example.com', 'fred@example.com'],
90
+ :bcc => ['bob@example.com', 'fred@example.com'],
91
+ :subject => 'Howdy',
92
+ :text_body => 'Sup?'
93
+ })
94
+ AppEngine::Mail.send(
95
+ 'ribrdb@example.com', ['bob@example.com', 'fred@example.com'],
96
+ 'Howdy', 'Sup?',
97
+ :cc => ['bob@example.com', 'fred@example.com'],
98
+ :bcc => ['bob@example.com', 'fred@example.com'])
99
+ end
100
+
101
+ it "should support no to" do
102
+ should_send({
103
+ :sender => 'ribrdb@example.com',
104
+ :to => [],
105
+ :bcc => ['fred@example.com'],
106
+ :subject => 'Howdy',
107
+ :text_body => 'Sup?'
108
+ })
109
+ AppEngine::Mail.send(
110
+ 'ribrdb@example.com', nil,
111
+ 'Howdy', 'Sup?', :bcc => 'fred@example.com')
112
+ end
113
+
114
+ it "should support html" do
115
+ should_send({
116
+ :sender => 'ribrdb@example.com',
117
+ :to => ['bob@example.com'],
118
+ :subject => 'Howdy',
119
+ :text_body => 'Sup?',
120
+ :html_body => '<h1>Sup?</h1>',
121
+ })
122
+ AppEngine::Mail.send(
123
+ 'ribrdb@example.com', 'bob@example.com',
124
+ 'Howdy', 'Sup?', :html => '<h1>Sup?</h1>')
125
+ end
126
+
127
+ it "should support html only" do
128
+ should_send({
129
+ :sender => 'ribrdb@example.com',
130
+ :to => ['bob@example.com'],
131
+ :subject => 'Howdy',
132
+ :html_body => '<h1>Sup?</h1>',
133
+ })
134
+ AppEngine::Mail.send(
135
+ 'ribrdb@example.com', 'bob@example.com',
136
+ 'Howdy', nil, :html => '<h1>Sup?</h1>')
137
+ end
138
+
139
+ it "should support attachments hash" do
140
+ should_send({
141
+ :sender => 'ribrdb@example.com',
142
+ :to => ['bob@example.com'],
143
+ :subject => 'Howdy',
144
+ :text_body => 'Sup?',
145
+ :attachment => [
146
+ {:file_name => 'foo.gif', :data => 'foo'},
147
+ ]
148
+ })
149
+ AppEngine::Mail.send(
150
+ 'ribrdb@example.com', 'bob@example.com',
151
+ 'Howdy', 'Sup?', :attachments => {'foo.gif' => 'foo'})
152
+ end
153
+
154
+ it "should support attachments list" do
155
+ should_send({
156
+ :sender => 'ribrdb@example.com',
157
+ :to => ['bob@example.com'],
158
+ :subject => 'Howdy',
159
+ :text_body => 'Sup?',
160
+ :attachment => [
161
+ {:file_name => 'foo.gif', :data => 'foo1'},
162
+ {:file_name => 'foo.gif', :data => 'foo2'},
163
+ ]
164
+ })
165
+ AppEngine::Mail.send(
166
+ 'ribrdb@example.com', 'bob@example.com',
167
+ 'Howdy', 'Sup?',
168
+ :attachments => [['foo.gif', 'foo1'], ['foo.gif', 'foo2']])
169
+ end
170
+
171
+ it "should support sending to admins" do
172
+ should_send_to_admins({
173
+ :sender => 'ribrdb@example.com',
174
+ :subject => 'Howdy',
175
+ :text_body => 'Sup?'
176
+ })
177
+ AppEngine::Mail.send_to_admins('ribrdb@example.com', 'Howdy', 'Sup?')
178
+ end
179
+ end
@@ -0,0 +1,385 @@
1
+ # Copyright:: Copyright 2009 Google Inc.
2
+ # Original Author:: Ryan Brown (mailto:ribrdb@google.com)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require File.dirname(__FILE__) + '/spec_helper.rb'
17
+ require 'appengine-apis/memcache'
18
+
19
+ describe AppEngine::Memcache do
20
+ before :each do
21
+ AppEngine::Testing::install_test_datastore
22
+ @cache = AppEngine::Memcache.new
23
+ end
24
+
25
+ before :all do
26
+ AppEngine::Testing::install_test_env
27
+ end
28
+
29
+ describe 'get' do
30
+
31
+ it 'should return nil on miss' do
32
+ @cache.get('foobar').should == nil
33
+ @cache['foobar'].should == nil
34
+ end
35
+
36
+ it 'should support strings' do
37
+ @cache.set('foobar', 'test').should == true
38
+ @cache.get('foobar').should == 'test'
39
+ @cache['foobar'].should == 'test'
40
+ end
41
+
42
+ it 'should support multiple values' do
43
+ @cache.get('foo', 'bar', 'baz').should == [nil, nil, nil]
44
+ @cache['foo', 'bar', 'baz'].should == [nil, nil, nil]
45
+ @cache.set('bar', 'food').should == true
46
+ @cache['foo', 'bar', 'baz'].should == [nil, 'food', nil]
47
+ end
48
+
49
+ it 'should support getting an array of keys' do
50
+ @cache.set('foobar', 'test').should == true
51
+ @cache.get(['foobar', 'flowers']).should == ['test', nil]
52
+ @cache.get(['foobar']).should == ['test']
53
+ end
54
+
55
+ it 'should support numbers' do
56
+ @cache.set('one', 1).should == true
57
+ @cache.set('pi', 3.14).should == true
58
+ @cache.get('pi').should == 3.14
59
+ @cache.get('one').should === 1
60
+ end
61
+
62
+ it 'should support symbol keys' do
63
+ @cache.set(:a, 'A')
64
+ @cache.get(:a).should == 'A'
65
+ end
66
+
67
+ it 'should support booleans' do
68
+ @cache.set('true', true)
69
+ @cache.set('false', false)
70
+ @cache.get('true').should == true
71
+ @cache.get('false').should == false
72
+ end
73
+
74
+ it 'should support marshaled objects' do
75
+ @cache.set('a', 1..5)
76
+ @cache.get('a').should == (1..5)
77
+ end
78
+
79
+ it 'should support false keys' do
80
+ @cache.set(nil, 'nil')
81
+ @cache.set(false, 'false')
82
+ @cache.get(nil).should == 'nil'
83
+ @cache.get(false).should == 'false'
84
+ end
85
+ end
86
+
87
+ describe 'get_hash' do
88
+ it 'should not include missing keys' do
89
+ @cache.get_hash(:a, :b).should == {}
90
+ end
91
+
92
+ it 'should get objects' do
93
+ @cache.set('a', 'A')
94
+ @cache.set('b', 3)
95
+ @cache.get_hash('a', 'c', 'b').should == {'a' => 'A', 'b' => 3 }
96
+ end
97
+ end
98
+
99
+ describe 'clear' do
100
+ it 'should clear the cache' do
101
+ @cache.set(:a, 'A')
102
+ @cache.set('b', 'B')
103
+ @cache.clear
104
+ @cache.get_hash(:a, 'b', :c).should == {}
105
+ end
106
+ end
107
+
108
+ describe 'flush_all' do
109
+ it 'should clear the cache' do
110
+ @cache.set(:a, 'A')
111
+ @cache.set('b', 'B')
112
+ @cache.flush_all
113
+ @cache.get_hash(:a, 'b', :c).should == {}
114
+ end
115
+ end
116
+
117
+ describe 'stats' do
118
+ it 'should count items' do
119
+ @cache.stats[:items].should == 0
120
+ @cache.set(:a, 'A')
121
+ @cache.stats[:items].should == 1
122
+ end
123
+ end
124
+
125
+ describe 'delete' do
126
+ it 'should remove items' do
127
+ @cache.set(:a, 'A')
128
+ @cache.get(:a).should == 'A'
129
+ @cache.delete(:a).should == true
130
+ @cache.get(:a).should == nil
131
+ @cache.get_hash(:a).should == {}
132
+ end
133
+
134
+ it 'should return false on missing items' do
135
+ @cache.delete(:a).should == false
136
+ end
137
+
138
+ it 'should allow blocking adds' do
139
+ @cache.set(:a, '1')
140
+ @cache.set(:b, '2')
141
+ @cache.delete(:a)
142
+ @cache.delete(:b, 1)
143
+ @cache.add(:a, 'A').should == true
144
+ @cache.add(:b, 'B').should == false
145
+ end
146
+ end
147
+
148
+ describe 'delete_many' do
149
+ it 'should remove items' do
150
+ @cache.set(:a, 'A')
151
+ @cache.set(:b, 'B')
152
+ @cache.set(:c, 'C')
153
+ @cache.delete_many([:a, :c])
154
+ @cache.get_hash(:a, :b, :c).should == {:b => 'B'}
155
+ end
156
+
157
+ it 'should return removed keys' do
158
+ @cache.set('a', 'A')
159
+ @cache.set('b', 'B')
160
+ @cache.set('c', 'C')
161
+ removed = @cache.delete_many(['a', 'c'])
162
+ removed.sort.should == ['a', 'c']
163
+ end
164
+
165
+ it 'should not return missing keys' do
166
+ @cache.set('a', 'A')
167
+ @cache.set('b', 'B')
168
+ @cache.set('c', 'C')
169
+ removed = @cache.delete_many(['a', 'c', 'd', 'e'])
170
+ removed.sort.should == ['a', 'c']
171
+ @cache.delete_many(['e']).should == []
172
+ end
173
+
174
+
175
+ it 'should allow blocking adds' do
176
+ @cache.set(:a, '1')
177
+ @cache.set(:b, '2')
178
+ @cache.delete_many([:a])
179
+ @cache.delete([:b, :c], 1)
180
+ @cache.add(:a, 'A').should == true
181
+ @cache.add(:b, 'B').should == false
182
+ end
183
+ end
184
+
185
+ describe 'add' do
186
+ it 'should add missing' do
187
+ @cache.add(:a, 'A').should == true
188
+ @cache.get(:a).should == 'A'
189
+ end
190
+
191
+ it 'should not replace existing entries' do
192
+ @cache.set(:a, 1)
193
+ @cache.add(:a, 'A').should == false
194
+ @cache.get(:a).should == 1
195
+ end
196
+ end
197
+
198
+ describe 'add_many' do
199
+ it 'should add missing' do
200
+ @cache.add_many({:a => 1, :b =>2})
201
+ @cache.get(:b, :a).should == [2, 1]
202
+ end
203
+
204
+ it 'should not replace existing entries' do
205
+ @cache.set(:a, 1)
206
+ @cache.add_many({:a => 'A', :b => 'B'})
207
+ @cache.get(:a, :b).should == [1, 'B']
208
+ end
209
+
210
+ it 'should return existing keys' do
211
+ @cache.set(:a, 1)
212
+ @cache.add_many({:a => 'A', :b => 'B'}).should == [:a]
213
+ end
214
+ end
215
+
216
+ describe 'set' do
217
+ it 'should add missing' do
218
+ @cache.set(:a, :A).should == true
219
+ @cache.get(:a).should == :A
220
+ end
221
+
222
+ it 'should replace existing' do
223
+ @cache.set(:a, :A).should == true
224
+ @cache.get(:a).should == :A
225
+ @cache.set(:a, 1).should == true
226
+ @cache.get(:a).should == 1
227
+ end
228
+ end
229
+
230
+ describe 'set_many' do
231
+ it 'should set multiple values' do
232
+ @cache.set_many({:a => 1, :b => 2}).should == []
233
+ end
234
+ end
235
+
236
+ describe 'replace' do
237
+ it 'should replace existing' do
238
+ @cache.set(:a, :A).should == true
239
+ @cache.get(:a).should == :A
240
+ @cache.replace(:a, 1).should == true
241
+ @cache.get(:a).should == 1
242
+ end
243
+
244
+ it 'should not replace missing' do
245
+ @cache.replace(:a, :A).should == false
246
+ end
247
+ end
248
+
249
+ describe 'replace_many' do
250
+ it 'should replace many' do
251
+ @cache.set_many({:a => 1, :c => 3})
252
+ @cache.replace_many({:a => :A, :b => :B, :c => :C}).should == [:b]
253
+ @cache.get(:a, :b, :c).should == [:A, nil, :C]
254
+ end
255
+ end
256
+
257
+ describe 'dict access' do
258
+ it 'should support getting with []' do
259
+ @cache.set(:a, 7)
260
+ @cache[:a].should == 7
261
+ @cache.set(:b, :B)
262
+ @cache[:a, :b].should == [7, :B]
263
+ end
264
+
265
+ it 'should support setting with []=' do
266
+ @cache[:a, :b] = [1, :B]
267
+ @cache[:c] = 3
268
+ @cache[:a].should == 1
269
+ @cache[:b].should == :B
270
+ @cache[:c].should == 3
271
+ end
272
+ end
273
+
274
+ describe 'incr' do
275
+ it 'should increment number' do
276
+ @cache.set(:a, 1)
277
+ @cache.incr(:a).should == 2
278
+ @cache.incr(:a, 7).should == 9
279
+ @cache.get(:a).should == 9
280
+ end
281
+
282
+ it 'should wrap' do
283
+ @cache.set(:a, 7)
284
+ @cache.incr(:a, 2**62).should == 2**62 + 7
285
+ @cache.incr(:a, 2**62).should < 2**62
286
+ end
287
+
288
+ it 'should fail if not number' do
289
+ @cache.incr(:a).should == nil
290
+ @cache.get_hash([:a]).should == {}
291
+ @cache.set(:b, :foo)
292
+ lambda {@cache.incr(:b)}.should raise_error(
293
+ AppEngine::Memcache::InvalidValueError)
294
+ end
295
+
296
+ it 'should increment strings' do
297
+ @cache.set(:a, '7')
298
+ @cache.incr(:a).should == 8
299
+ end
300
+ end
301
+
302
+ describe 'decr' do
303
+ it 'should decrement number' do
304
+ @cache.set(:a, 7)
305
+ @cache.decr(:a).should == 6
306
+ @cache.get(:a).should == 6
307
+ @cache.decr(:a, 2).should == 4
308
+ @cache.get(:a).should == 4
309
+ @cache.decr(:a, 4).should == 0
310
+ @cache.get(:a).should == 0
311
+ pending 'local decrement wrapping' do
312
+ @cache.decr(:a, 6).should == 0
313
+ @cache.get(:a).should == 0
314
+ end
315
+ end
316
+ end
317
+
318
+ describe 'readonly' do
319
+ before :each do
320
+ @rocache = AppEngine::Memcache.new(:readonly => true)
321
+ @ex = AppEngine::Memcache::MemcacheError
322
+ end
323
+
324
+ it 'should allow reads' do
325
+ @cache[:a] = 1
326
+ @rocache.get(:a).should == 1
327
+ end
328
+
329
+ it 'should allow stats' do
330
+ @cache[:a] = 7
331
+ @rocache.stats[:items].should == 1
332
+ end
333
+
334
+ it 'should block delete' do
335
+ @cache[:a] = 1
336
+ lambda {@rocache.delete(:a)}.should raise_error(@ex)
337
+ @cache[:a].should == 1
338
+ end
339
+
340
+ it 'should block add' do
341
+ lambda {@rocache.add(:a, 1)}.should raise_error(@ex)
342
+ lambda {@rocache.add_many({:b => 1})}.should raise_error(@ex)
343
+ @rocache.get_hash(:a, :b).should == {}
344
+ end
345
+
346
+ it 'should block set' do
347
+ lambda {@rocache.set(:a, 1)}.should raise_error(@ex)
348
+ lambda {@rocache.set_many({:b => 1})}.should raise_error(@ex)
349
+ @rocache.get_hash(:a, :b).should == {}
350
+ end
351
+
352
+ it 'should block replace' do
353
+ @cache[:a, :b] = :A, :B
354
+ lambda {@rocache.replace(:a, 1)}.should raise_error(@ex)
355
+ lambda {@rocache.replace_many({:b => 1})}.should raise_error(@ex)
356
+ @rocache[:a, :b].should == [:A, :B]
357
+ end
358
+
359
+ it 'should block clear' do
360
+ @cache[:a] = 1
361
+ lambda {@rocache.clear}.should raise_error(@ex)
362
+ lambda {@rocache.flush_all}.should raise_error(@ex)
363
+ @rocache[:a].should == 1
364
+ end
365
+
366
+ it 'should block incr' do
367
+ @cache[:a] = 1
368
+ lambda {@rocache.incr(:a)}.should raise_error(@ex)
369
+ @rocache[:a].should == 1
370
+ end
371
+
372
+ it 'should block decr' do
373
+ @cache[:a] = 1
374
+ lambda {@rocache.decr(:a)}.should raise_error(@ex)
375
+ @rocache[:a].should == 1
376
+ end
377
+ end
378
+
379
+ describe "namespaces" do
380
+ it 'should get namespace from initialize' do
381
+ cache = AppEngine::Memcache.new(:namespace => "foo")
382
+ cache.namespace.should == "foo"
383
+ end
384
+ end
385
+ end