xref: /memcached-1.4.29/t/binary.t (revision f5e4d26d)
1#!/usr/bin/perl
2
3use strict;
4use warnings;
5use Test::More 'no_plan';
6use FindBin qw($Bin);
7use lib "$Bin/lib";
8use MemcachedTest;
9
10my $server = new_memcached();
11
12ok($server, "started the server");
13
14# Based almost 100% off testClient.py which is Copyright (c) 2007  Dustin Sallings <[email protected]>
15
16# Command constants
17use constant CMD_GET     => 0;
18use constant CMD_SET     => 1;
19use constant CMD_ADD     => 2;
20use constant CMD_REPLACE => 3;
21use constant CMD_DELETE  => 4;
22use constant CMD_INCR    => 5;
23use constant CMD_DECR    => 6;
24use constant CMD_QUIT    => 7;
25use constant CMD_FLUSH   => 8;
26use constant CMD_GETQ    => 9;
27use constant CMD_NOOP    => 10;
28use constant CMD_VERSION => 11;
29
30# CAS, Flags, expiration
31use constant SET_PKT_FMT => "NNNN";
32
33# Flags, expiration, id
34use constant CAS_PKT_FMT => "NNNN";
35
36# How long until the deletion takes effect.
37use constant DEL_PKT_FMT => "N";
38
39# amount, initial value, expiration
40use constant INCRDECR_PKT_FMT => "NNNNN";
41
42use constant REQ_MAGIC_BYTE => 0x80;
43use constant RES_MAGIC_BYTE => 0x81;
44
45use constant PKT_FMT => "CCnCxxxNN";
46
47#min recv packet size
48use constant MIN_RECV_PACKET => length(pack(PKT_FMT));
49
50my $mc = MC::Client->new;
51my $check = sub {
52	my ($key, $orig_flags, $orig_value) = @_;
53	my ($flags, $value) = $mc->get($key);
54	is($flags, $orig_flags, "Flags is set properly");
55	is($value, $orig_value, "Value is set properly");
56};
57
58my $set = sub {
59	my ($key, $exp, $orig_flags, $orig_value) = @_;
60	$mc->set($key, $exp, $orig_flags, $orig_value);
61	$check->($key, $orig_flags, $orig_value);
62};
63
64my $empty = sub {
65	my $key = shift;
66	my $rv =()= eval { $mc->get($key) };
67	is($rv, 0, "Didn't get a result from get");
68	ok($@->not_found, "We got a not found error when we expected one");
69};
70
71my $delete = sub {
72	my ($key, $when) = @_;
73	$mc->delete($key, $when);
74	$empty->($key);
75};
76
77diag "Flushing...";
78$mc->flush;
79
80{
81	diag "Test Version";
82	my $v = $mc->version;
83	ok(defined $v && length($v), "Proper version: $v");
84}
85
86diag "Noop";
87$mc->noop;
88
89diag "Simple set/get";
90$set->('x', 5, 19, "somevalue");
91
92diag "Delete";
93$delete->('x');
94
95diag "Flush";
96$set->('x', 5, 19, "somevaluex");
97$set->('y', 5, 17, "somevaluey");
98$mc->flush;
99$empty->('x');
100$empty->('y');
101
102{
103	diag "Reservation delete";
104	$set->('y', 5, 19, "someothervalue");
105	$delete->('y', 1);
106	my $rv =()= eval { $mc->add('y', 5, 19, "yetanothervalue") };
107	is($rv, 0, "Add didn't return anything");
108	ok($@->exists, "We got an exists error like we expected");
109	sleep 2;
110	$mc->add('y', 5, 19, "wibblevalue");
111}
112
113{
114	diag "Add";
115	$empty->('i');
116        $mc->add('i', 5, 19, "ex");
117        $check->('i', 19, "ex");
118
119	my $rv =()= eval { $mc->add('i', 5, 19, "ex2") };
120	is($rv, 0, "Add didn't return anything");
121	ok($@->exists, "Expected exists error received");
122
123	$check->('i', 19, "ex");
124}
125
126{
127	diag "Replace";
128	$empty->('j');
129
130	my $rv =()= eval { $mc->replace('j', 5, 19, "ex") };
131	is($rv, 0, "Replace didn't return anything");
132	ok($@->not_found, "Expected not_found error received");
133
134	$empty->('j');
135
136	$mc->add('j', 5, 14, "ex2");
137	$check->('j', 14, "ex2");
138
139	$mc->replace('j', 5, 24, "ex3");
140	$check->('j', 24, "ex3");
141}
142
143{
144	diag "MultiGet";
145	$mc->add('xx', 5, 1, "ex");
146	$mc->add('wye', 5, 2, "why");
147	my $rv = $mc->getMulti(qw(xx wye zed));
148
149    # CAS is returned with all gets.
150    $rv->{xx}->[2]  = 0;
151    $rv->{wye}->[2] = 0;
152	is_deeply($rv->{xx}, [1, 'ex', 0], "X is correct");
153	is_deeply($rv->{wye}, [2, 'why', 0], "Y is correct");
154	is(keys(%$rv), 2, "Got only two answers like we expect");
155}
156
157diag "Test increment";
158$mc->flush;
159is($mc->incr("x"), 0, "First incr call is zero");
160is($mc->incr("x"), 1, "Second incr call is one");
161is($mc->incr("x", 211), 212, "Adding 211 gives you 212");
162is($mc->incr("x", 2**33), 8589934804, "Blast the 32bit border");
163
164diag "Test decrement";
165$mc->flush;
166is($mc->incr("x", undef, 5), 5, "Initial value");
167is($mc->decr("x"), 4, "Decrease by one");
168is($mc->decr("x", 211), 0, "Floor is zero");
169
170{
171	diag "CAS";
172	$mc->flush;
173
174	{
175		my $rv =()= eval { $mc->set("x", 5, 19, "bad value", 0x7FFFFFFFFF) };
176		is($rv, 0, "Empty return on expected failure");
177		ok($@->not_found, "Error was 'not found' as expected");
178	}
179
180	$mc->add("x", 5, 19, "original value");
181
182	my ($flags, $val, $i) = $mc->get("x");
183	is($val, "original value", "->gets returned proper value");
184
185    {
186		my $rv =()= eval { $mc->set("x", 5, 19, "broken value", $i+1) };
187		is($rv, 0, "Empty return on expected failure (1)");
188		ok($@->exists, "Expected error state of 'exists' (1)");
189	}
190
191	$mc->set("x", 5, 19, "new value", $i);
192
193	my ($newflags, $newval, $newi) = $mc->get("x");
194	is($newval, "new value", "CAS properly overwrote value");
195
196	{
197		my $rv =()= eval { $mc->set("x", 5, 19, "replay value", $i) };
198		is($rv, 0, "Empty return on expected failure (2)");
199		ok($@->exists, "Expected error state of 'exists' (2)");
200	}
201
202	(undef, my $newval2) = $mc->get("x");
203	is($newval2, "new value", "CAS replay didn't overwrite value");
204}
205
206$mc->flush;
207$mc->close;
208
209
210package MC::Client;
211
212use strict;
213use warnings;
214
215use fields qw(socket);
216
217use IO::Socket::INET;
218
219sub new {
220	my $self = shift;
221
222    my $sock = $server->sock;
223
224	$self = fields::new($self);
225
226	$self->{socket} = $sock;
227
228	return $self;
229}
230
231sub close {
232	my $self = shift;
233	return $self->{socket}->close(@_);
234}
235
236sub _sendCmd {
237	my $self = shift;
238	die "Not enough args to _sendCmd" unless @_ >= 4;
239	my ($cmd, $key, $val, $opaque, $extraHeader) = @_;
240
241	$extraHeader = '' unless defined $extraHeader;
242
243	my $keylen = length($key);
244	my $vallen = length($val);
245	my $extralen = length($extraHeader);
246
247	my $msg = pack(::PKT_FMT, ::REQ_MAGIC_BYTE, $cmd, $keylen, $extralen,
248                    $keylen + $vallen + $extralen, $opaque);
249	return $self->{socket}->send($msg . $extraHeader . $key . $val);
250}
251
252sub _handleSingleResponse {
253	my $self = shift;
254	my $myopaque = shift;
255
256	$self->{socket}->recv(my $response, ::MIN_RECV_PACKET);
257
258	Test::More::is(length($response), ::MIN_RECV_PACKET, "Expected read length");
259
260	my ($magic, $cmd, $errcode, $extralen, $remaining,
261        $opaque) = unpack(::PKT_FMT, $response);
262
263	Test::More::is($magic, ::RES_MAGIC_BYTE, "Got proper magic");
264
265	return ($opaque, "")
266		if $remaining == 0;
267
268	$self->{socket}->recv(my $rv, $remaining);
269
270	if (defined $myopaque) {
271		Test::More::is($opaque, $myopaque, "Expected opaque");
272	} else {
273		Test::More::pass("Implicit pass since myopaque is undefined");
274	}
275
276	if ($errcode) {
277		die MC::Error->new($errcode, $rv);
278	}
279
280	return ($opaque, $rv);
281}
282
283sub _doCmd {
284	my $self = shift;
285	die unless @_ >= 3;
286	my ($cmd, $key, $val, $extraHeader) = @_;
287
288	$extraHeader = '' unless defined $extraHeader;
289
290	my $opaque = int(rand(2**32));
291
292	$self->_sendCmd($cmd, $key, $val, $opaque, $extraHeader);
293	(undef, my $rv) = $self->_handleSingleResponse($opaque);
294	return $rv;
295}
296
297sub __parseGet {
298	my $self = shift;
299	my $rv = shift; # currently contains 4 bytes of 'flag' followed by value
300	my $header = substr $rv, 0, 12, '';
301	my ($ident_hi, $ident_lo, $flags) = unpack "NNN", $header;
302	my $ident = ($ident_hi * 2 ** 32) + $ident_lo;
303
304	return $flags, $rv, $ident;
305}
306
307sub get {
308	my $self = shift;
309	my $key = shift;
310	my $parts = $self->_doCmd(::CMD_GET, $key, '');
311	return $self->__parseGet($parts);
312}
313
314sub _mutate {
315	my $self = shift;
316	my ($cmd, $key, $exp, $flags, $val, $ident) = @_;
317
318    my $ident_hi = 0;
319    my $ident_lo = 0;
320    if ($ident) {
321        $ident_hi = int($ident / 2 ** 32);
322        $ident_lo = int($ident % 2 ** 32);
323    }
324
325	return $self->_doCmd($cmd, $key, $val, pack(::SET_PKT_FMT, $ident_hi, $ident_lo, $flags, $exp));
326}
327
328sub set {
329	my $self = shift;
330	my ($key, $exp, $flags, $val, $ident) = @_;
331
332	return $self->_mutate(::CMD_SET, $key, $exp, $flags, $val, $ident);
333}
334
335sub __incrdecr {
336	my $self = shift;
337	my ($cmd, $key, $amt, $init, $exp) = @_;
338
339	my $amt_hi = int($amt / 2 ** 32);
340	my $amt_lo = int($amt % 2 ** 32);
341
342	my $init_hi = int($init / 2 ** 32);
343	my $init_lo = int($init % 2 ** 32);
344
345	my $data = $self->_doCmd($cmd, $key, '', pack(::INCRDECR_PKT_FMT, $amt_hi, $amt_lo, $init_hi, $init_lo, $exp));
346	my $header = substr $data, 0, 12, '';
347	my ($resp_hi, $resp_lo) = unpack "NN", $header;
348	my $resp = ($resp_hi * 2 ** 32) + $resp_lo;
349    return $resp;
350}
351
352sub incr {
353	my $self = shift;
354	my ($key, $amt, $init, $exp) = @_;
355	$amt = 1 unless defined $amt;
356	$init = 0 unless defined $init;
357	$exp = 0 unless defined $exp;
358
359	return $self->__incrdecr(::CMD_INCR, $key, $amt, $init, $exp);
360}
361
362sub decr {
363	my $self = shift;
364	my ($key, $amt, $init, $exp) = @_;
365	$amt = 1 unless defined $amt;
366	$init = 0 unless defined $init;
367	$exp = 0 unless defined $exp;
368
369	return $self->__incrdecr(::CMD_DECR, $key, $amt, $init, $exp);
370}
371
372sub add {
373	my $self = shift;
374	my ($key, $exp, $flags, $val) = @_;
375	return $self->_mutate(::CMD_ADD, $key, $exp, $flags, $val);
376}
377sub replace {
378	my $self = shift;
379	my ($key, $exp, $flags, $val) = @_;
380	return $self->_mutate(::CMD_REPLACE, $key, $exp, $flags, $val);
381}
382
383sub getMulti {
384	my $self = shift;
385	my @keys = @_;
386
387	for (my $i = 0; $i < @keys; $i++) {
388		$self->_sendCmd(::CMD_GETQ, $keys[$i], '', $i);
389	}
390
391	my $terminal = @keys + 10;
392	$self->_sendCmd(::CMD_NOOP, '', '', $terminal);
393
394	my %return;
395
396	while (1) {
397		my ($opaque, $data) = $self->_handleSingleResponse;
398		last if $opaque == $terminal;
399
400		$return{$keys[$opaque]} = [$self->__parseGet($data)];
401	}
402	return %return if wantarray;
403	return \%return;
404}
405
406sub noop {
407	my $self = shift;
408	return $self->_doCmd(::CMD_NOOP, '', '');
409}
410
411sub delete {
412	my $self = shift;
413	my ($key, $when) = @_;
414	$when = 0 unless defined $when;
415
416	return $self->_doCmd(::CMD_DELETE, $key, '', pack(::DEL_PKT_FMT, $when));
417}
418
419sub version {
420	my $self = shift;
421	return $self->_doCmd(::CMD_VERSION, '', '');
422}
423
424sub flush {
425	my $self = shift;
426	return $self->_doCmd(::CMD_FLUSH, '', '');
427}
428
429package MC::Error;
430
431use strict;
432use warnings;
433
434use constant ERR_UNKNOWN_CMD => 0x81;
435use constant ERR_NOT_FOUND   => 0x1;
436use constant ERR_EXISTS      => 0x2;
437
438use overload '""' => sub {
439	my $self = shift;
440
441	return "Memcache Error ($self->[0]): $self->[1]";
442};
443
444sub new {
445	my $class = shift;
446	my $error = [@_];
447
448	my $self = bless $error, (ref $class || $class);
449
450	return $self;
451}
452
453sub not_found {
454	my $self = shift;
455
456	return $self->[0] == ERR_NOT_FOUND;
457}
458
459sub exists {
460	my $self = shift;
461
462	return $self->[0] == ERR_EXISTS;
463}
464
465# vim: filetype=perl
466