From 4bd8916b1257378f8173c4ada41b4606e9c0226b Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Tue, 8 Jul 2008 00:47:49 +0000 Subject: [PATCH] Merge branch 'fastbinary' git-svn-id: https://svn.apache.org/repos/asf/incubator/thrift/trunk@674688 13f79535-47bb-0310-9956-ffa450edef68 --- configure.ac | 8 + lib/Makefile.am | 6 +- lib/rb/Makefile.am | 13 + lib/rb/Manifest | 7 + lib/rb/Rakefile | 11 +- lib/rb/ext/binaryprotocolaccelerated.c | 1231 +++++++++++++ lib/rb/ext/extconf.rb | 7 + .../protocol/binaryprotocolaccelerated.rb | 19 + lib/rb/lib/thrift/server/nonblockingserver.rb | 6 +- lib/rb/lib/thrift/struct.rb | 46 +- lib/rb/lib/thrift/transport.rb | 47 +- lib/rb/lib/thrift/transport/socket.rb | 16 +- lib/rb/setup.rb | 1585 +++++++++++++++++ lib/rb/spec/socket_spec_shared.rb | 10 +- lib/rb/spec/transport_spec.rb | 14 +- test/Makefile.am | 4 + test/SmallTest.thrift | 4 + test/rb/Makefile | 22 - test/rb/Makefile.am | 9 + test/rb/benchmarks/protocol_benchmark.rb | 158 ++ .../core/test_binary_protocol_accelerated.rb | 275 +++ test/rb/fixtures/structs.rb | 225 +++ test/rb/generation/test_struct.rb | 3 + .../accelerated_buffered_client.rb | 145 ++ .../accelerated_buffered_server.rb | 47 + test/rb/integration/buffered_client.rb | 145 ++ test/rb/integration/simple_client.rb | 145 ++ test/rb/integration/simple_server.rb | 46 + test/rb/test_helper.rb | 12 + test/rb/test_suite.rb | 2 +- 30 files changed, 4197 insertions(+), 71 deletions(-) create mode 100644 lib/rb/Makefile.am create mode 100644 lib/rb/ext/binaryprotocolaccelerated.c create mode 100644 lib/rb/ext/extconf.rb create mode 100644 lib/rb/lib/thrift/protocol/binaryprotocolaccelerated.rb create mode 100644 lib/rb/setup.rb delete mode 100644 test/rb/Makefile create mode 100644 test/rb/Makefile.am create mode 100644 test/rb/benchmarks/protocol_benchmark.rb create mode 100644 test/rb/core/test_binary_protocol_accelerated.rb create mode 100644 test/rb/fixtures/structs.rb create mode 100644 test/rb/integration/accelerated_buffered_client.rb create mode 100644 test/rb/integration/accelerated_buffered_server.rb create mode 100644 test/rb/integration/buffered_client.rb create mode 100644 test/rb/integration/simple_client.rb create mode 100644 test/rb/integration/simple_server.rb diff --git a/configure.ac b/configure.ac index df5c2294..ef163f74 100644 --- a/configure.ac +++ b/configure.ac @@ -61,6 +61,12 @@ if test "$with_py" = "yes"; then fi AM_CONDITIONAL(WITH_PYTHON, [test -n "$PYTHON" -a "$PYTHON" != ":"]) +AX_THRIFT_LIB(ruby, [Ruby], yes) +if test "$with_ruby" = "yes"; then + AC_PATH_PROG([RUBY], [ruby]) +fi +AM_CONDITIONAL(ENABLE_RUBY, [test -n "$RUBY"]) + AC_C_CONST AC_C_INLINE AC_C_VOLATILE @@ -165,10 +171,12 @@ AC_CONFIG_FILES([ lib/csharp/Makefile lib/java/Makefile lib/py/Makefile + lib/rb/Makefile if/Makefile test/Makefile test/py/Makefile test/java/Makefile + test/rb/Makefile ]) AC_OUTPUT diff --git a/lib/Makefile.am b/lib/Makefile.am index 09236cec..0765f031 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -17,12 +17,14 @@ if WITH_ERLANG SUBDIRS += erl endif +if ENABLE_RUBY +SUBDIRS += rb +endif + EXTRA_DIST = \ cocoa \ hs \ ocaml \ perl \ php \ - rb \ - erl \ st diff --git a/lib/rb/Makefile.am b/lib/rb/Makefile.am new file mode 100644 index 00000000..43acfff2 --- /dev/null +++ b/lib/rb/Makefile.am @@ -0,0 +1,13 @@ +EXTRA_DIST = setup.rb lib ext + +all-local: + $(RUBY) setup.rb config + $(RUBY) setup.rb setup + +install-exec-hook: + $(RUBY) setup.rb install + +clean-local: + $(RUBY) setup.rb clean + +check-local: all diff --git a/lib/rb/Manifest b/lib/rb/Manifest index b90a86df..1b408fce 100644 --- a/lib/rb/Manifest +++ b/lib/rb/Manifest @@ -8,11 +8,14 @@ benchmark/server.rb benchmark/thin_server.rb CHANGELOG COPYING +ext/binaryprotocolaccelerated.c +ext/extconf.rb lib/thrift/client.rb lib/thrift/deprecation.rb lib/thrift/exceptions.rb lib/thrift/processor.rb lib/thrift/protocol/binaryprotocol.rb +lib/thrift/protocol/binaryprotocolaccelerated.rb lib/thrift/protocol/tbinaryprotocol.rb lib/thrift/protocol/tprotocol.rb lib/thrift/protocol.rb @@ -34,9 +37,13 @@ lib/thrift/transport.rb lib/thrift/types.rb lib/thrift.rb LICENSE +Makefile +Makefile.am +Makefile.in Manifest Rakefile README +setup.rb spec/backwards_compatibility_spec.rb spec/binaryprotocol_spec.rb spec/client_spec.rb diff --git a/lib/rb/Rakefile b/lib/rb/Rakefile index e88e9844..6ddca522 100644 --- a/lib/rb/Rakefile +++ b/lib/rb/Rakefile @@ -23,7 +23,7 @@ task :test do # ensure this is a full thrift checkout and not a tarball of the ruby libs cmd = 'head -1 ../../README 2>/dev/null | grep Thrift >/dev/null 2>/dev/null' system(cmd) or fail "rake test requires a full thrift checkout" - sh 'make', '-C', File.dirname(__FILE__) + "/../../test/rb" + sh 'make', '-C', File.dirname(__FILE__) + "/../../test/rb", "check" end desc 'Compile the .thrift files for the specs' @@ -57,6 +57,15 @@ begin p.url = "http://incubator.apache.org/thrift/" p.include_rakefile = true end + + task :install => [:check_site_lib] + + require 'rbconfig' + task :check_site_lib do + if File.exist?(File.join(Config::CONFIG['sitelibdir'], 'thrift.rb')) + fail "thrift is already installed in site_ruby" + end + end rescue LoadError [:install, :package].each do |t| desc "Stub for #{t}" diff --git a/lib/rb/ext/binaryprotocolaccelerated.c b/lib/rb/ext/binaryprotocolaccelerated.c new file mode 100644 index 00000000..2725bfcb --- /dev/null +++ b/lib/rb/ext/binaryprotocolaccelerated.c @@ -0,0 +1,1231 @@ +// Half of this file comes from contributions from Nitay Joffe (nitay@powerset.com) +// Much of the rest (almost) directly ported (or pulled) from thrift-py's fastbinary.c +// Everything else via Kevin Clark (kevin@powerset.com) +#include +#include + +#include +#include +#include + +// #define __DEBUG__ + +#ifndef HAVE_STRLCPY + +static +size_t +strlcpy (char *dst, const char *src, size_t dst_sz) +{ + size_t n; + + for (n = 0; n < dst_sz; n++) { + if ((*dst++ = *src++) == '\0') + break; + } + + if (n < dst_sz) + return n; + if (n > 0) + *(dst - 1) = '\0'; + return n + strlen (src); +} + +#endif + +#define dbg() fprintf(stderr, "%s:%d\n", __FUNCTION__, __LINE__) + + +// TODO (kevinclark): This was here from the patch/python. Not sure +// If it's actually that big a pain. Need to look into pulling +// From the right place + +// Stolen out of TProtocol.h. +// It would be a huge pain to have both get this from one place. + +enum TType { + T_STOP = 0, + T_BOOL = 2, + T_BYTE = 3, + T_I16 = 6, + T_I32 = 8, + T_I64 = 10, + T_DBL = 4, + T_STR = 11, + T_STRCT = 12, + T_MAP = 13, + T_SET = 14, + T_LIST = 15 + // T_VOID = 1, + // T_I08 = 3, + // T_U64 = 9, + // T_UTF7 = 11, + // T_UTF8 = 16, + // T_UTF16 = 17 +}; + +#define IS_CONTAINER(x) (x == T_MAP || x == T_SET || x == T_LIST) + +// Same comment as the enum. Sorry. +#ifdef HAVE_ENDIAN_H +#include +#endif + +#ifndef __BYTE_ORDER +# if defined(BYTE_ORDER) && defined(LITTLE_ENDIAN) && defined(BIG_ENDIAN) +# define __BYTE_ORDER BYTE_ORDER +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __BIG_ENDIAN BIG_ENDIAN +# else +# error "Cannot determine endianness" +# endif +#endif + +#if __BYTE_ORDER == __BIG_ENDIAN +# define ntohll(n) (n) +# define htonll(n) (n) +#elif __BYTE_ORDER == __LITTLE_ENDIAN +# if defined(__GNUC__) && defined(__GLIBC__) +# include +# define ntohll(n) bswap_64(n) +# define htonll(n) bswap_64(n) +# else /* GNUC & GLIBC */ +# define ntohll(n) ( (((unsigned long long)ntohl(n)) << 32) + ntohl(n >> 32) ) +# define htonll(n) ( (((unsigned long long)htonl(n)) << 32) + htonl(n >> 32) ) +# endif /* GNUC & GLIBC */ +#else /* __BYTE_ORDER */ +# error "Can't define htonll or ntohll!" +#endif + + +// ----------------------------------------------------------------------------- +// Cached interned strings and such +// ----------------------------------------------------------------------------- + +static VALUE class_tbpa; +static VALUE m_thrift; +static VALUE rb_cSet; +static ID type_sym; +static ID class_sym; +static ID key_sym; +static ID value_sym; +static ID element_sym; +static ID name_sym; +static ID fields_id; +static ID consume_bang_id; +static ID string_buffer_id; +static ID borrow_id; +static ID keys_id; +static ID entries_id; + +static const uint32_t VERSION_MASK = 0xffff0000; +static const uint32_t VERSION_1 = 0x80010000; + +// ----------------------------------------------------------------------------- +// Structs so I don't have to keep calling rb_hash_aref +// ----------------------------------------------------------------------------- + +// { :type => field[:type], +// :class => field[:class], +// :key => field[:key], +// :value => field[:value], +// :element => field[:element] } + +struct _thrift_map; +struct _field_spec; + +typedef union { + VALUE class; + struct _thrift_map* map; + struct _field_spec* element; +} container_data; + +typedef struct _field_spec { + int type; + char* name; + container_data data; +} field_spec; + +typedef struct _thrift_map { + field_spec* key; + field_spec* value; +} thrift_map; + + +static void free_field_spec(field_spec* spec) { + switch(spec->type) { + case T_LIST: + case T_SET: + free_field_spec(spec->data.element); + break; + + case T_MAP: + free_field_spec(spec->data.map->key); + free_field_spec(spec->data.map->value); + free(spec->data.map); + break; + } + + free(spec); +} + +// Parses a ruby field spec into a C struct +// +// Simple fields look like: +// { :name => .., :type => .. } +// Structs add the :class attribute +// Maps adds :key and :value attributes, field specs +// Lists and Sets add an :element, a field spec +static field_spec* parse_field_spec(VALUE field_data) { + int type = NUM2INT(rb_hash_aref(field_data, type_sym)); + VALUE name = rb_hash_aref(field_data, name_sym); + field_spec* spec = (field_spec *) malloc(sizeof(field_spec)); + +#ifdef __DEBUG__ // No need for this in prod since I set all the fields + bzero(spec, sizeof(field_spec)); +#endif + + spec->type = type; + + if (Qnil != name) { + spec->name = StringValuePtr(name); + } else { + spec->name = NULL; + } + + switch(type) { + case T_STRCT: { + spec->data.class = rb_hash_aref(field_data, class_sym); + break; + } + + case T_MAP: { + VALUE key_fields = rb_hash_aref(field_data, key_sym); + VALUE value_fields = rb_hash_aref(field_data, value_sym); + thrift_map* map = (thrift_map *) malloc(sizeof(thrift_map)); + + map->key = parse_field_spec(key_fields); + map->value = parse_field_spec(value_fields); + spec->data.map = map; + + break; + } + + case T_LIST: + case T_SET: + { + VALUE list_fields = rb_hash_aref(field_data, element_sym); + spec->data.element = parse_field_spec(list_fields); + break; + } + } + + return spec; +} + + +// ----------------------------------------------------------------------------- +// Serialization routines +// ----------------------------------------------------------------------------- + + +// write_*(VALUE buf, ...) takes a value and adds it to a Ruby string buffer, +// in network order +static void write_byte(VALUE buf, int8_t val) { + rb_str_buf_cat(buf, (char*)&val, sizeof(int8_t)); +} + +static void write_i16(VALUE buf, int16_t val) { + int16_t net = (int16_t)htons(val); + rb_str_buf_cat(buf, (char*)&net, sizeof(int16_t)); +} + +static void write_i32(VALUE buf, int32_t val) { + int32_t net = (int32_t)htonl(val); + rb_str_buf_cat(buf, (char*)&net, sizeof(int32_t)); +} + +static void write_i64(VALUE buf, int64_t val) { + int64_t net = (int64_t)htonll(val); + rb_str_buf_cat(buf, (char*)&net, sizeof(int64_t)); +} + +static void write_double(VALUE buf, double dub) { + // Unfortunately, bitwise_cast doesn't work in C. Bad C! + union { + double f; + int64_t t; + } transfer; + transfer.f = dub; + write_i64(buf, transfer.t); +} + +static void write_string(VALUE buf, char* str) { + int32_t len = strlen(str); + write_i32(buf, len); + rb_str_buf_cat2(buf, str); +} + +// Some functions macro'd out because they're nops for the binary protocol +// but placeholders were desired in case things change +#define write_struct_begin(buf) +#define write_struct_end(buf) + +static void write_field_begin(VALUE buf, char* name, int type, int fid) { +#ifdef __DEBUG__ + fprintf(stderr, "Writing field beginning: %s %d %d\n", name, type, fid); +#endif + + write_byte(buf, (int8_t)type); + write_i16(buf, (int16_t)fid); +} + +#define write_field_end(buf) + +static void write_field_stop(VALUE buf) { + write_byte(buf, T_STOP); +} + +static void write_map_begin(VALUE buf, int8_t ktype, int8_t vtype, int32_t sz) { + write_byte(buf, ktype); + write_byte(buf, vtype); + write_i32(buf, sz); +} + +#define write_map_end(buf); + +static void write_list_begin(VALUE buf, int type, int sz) { + write_byte(buf, type); + write_i32(buf, sz); +} + +#define write_list_end(buf) + +static void write_set_begin(VALUE buf, int type, int sz) { + write_byte(buf, type); + write_i32(buf, sz); +} + +#define write_set_end(buf) + +static void binary_encoding(VALUE buf, VALUE obj, int type); + +// Handles container types: Map, Set, List +static void write_container(VALUE buf, VALUE value, field_spec* spec) { + int sz, i; + + switch(spec->type) { + case T_MAP: { + VALUE keys; + VALUE key; + VALUE val; + + keys = rb_funcall(value, keys_id, 0); + + sz = RARRAY(keys)->len; + + write_map_begin(buf, spec->data.map->key->type, spec->data.map->value->type, sz); + + for (i = 0; i < sz; i++) { + key = rb_ary_entry(keys, i); + val = rb_hash_aref(value, key); + + if (IS_CONTAINER(spec->data.map->key->type)) { + write_container(buf, key, spec->data.map->key); + } else { + binary_encoding(buf, key, spec->data.map->key->type); + } + + if (IS_CONTAINER(spec->data.map->value->type)) { + write_container(buf, val, spec->data.map->value); + } else { + binary_encoding(buf, val, spec->data.map->value->type); + } + } + + write_map_end(buf); + + break; + } + + case T_LIST: { + sz = RARRAY(value)->len; + + write_list_begin(buf, spec->data.element->type, sz); + for (i = 0; i < sz; ++i) { + if (IS_CONTAINER(spec->data.element->type)) { + write_container(buf, rb_ary_entry(value, i), spec->data.element); + } else { + binary_encoding(buf, rb_ary_entry(value, i), spec->data.element->type); + } + } + write_list_end(buf); + break; + } + + case T_SET: { + VALUE items; + + if (TYPE(value) == T_ARRAY) { + items = value; + } else { + if (rb_cSet == CLASS_OF(value)) { + items = rb_funcall(value, entries_id, 0); + } else { + Check_Type(value, T_HASH); + items = rb_funcall(value, keys_id, 0); + } + } + + sz = RARRAY(items)->len; + + write_set_begin(buf, spec->data.element->type, sz); + + for (i = 0; i < sz; i++) { + if (IS_CONTAINER(spec->data.element->type)) { + write_container(buf, rb_ary_entry(items, i), spec->data.element); + } else { + binary_encoding(buf, rb_ary_entry(items, i), spec->data.element->type); + } + } + + write_set_end(buf); + break; + } + } +} + +// Takes the field id, data to be encoded, buffer and enclosing object +// to be encoded. buf and obj passed as a ruby array for rb_hash_foreach. +// TODO(kevinclark): See if they can be passed individually to avoid object +// creation +static int encode_field(VALUE fid, VALUE data, VALUE ary) { + field_spec *spec = parse_field_spec(data); + + VALUE buf = rb_ary_entry(ary, 0); + VALUE obj = rb_ary_entry(ary, 1); + char name_buf[128]; + + name_buf[0] = '@'; + strlcpy(&name_buf[1], spec->name, sizeof(name_buf) - 1); + + VALUE value = rb_ivar_get(obj, rb_intern(name_buf)); + + if (Qnil == value) { + free_field_spec(spec); + return 0; + } + + write_field_begin(buf, spec->name, spec->type, NUM2INT(fid)); + + if (IS_CONTAINER(spec->type)) { + write_container(buf, value, spec); + } else { + binary_encoding(buf, value, spec->type); + } + write_field_end(buf); + + free_field_spec(spec); + + return 0; +} + +// ----------------------------------------------------------------------------- +// TFastBinaryProtocol's main encoding loop +// ----------------------------------------------------------------------------- + +static void binary_encoding(VALUE buf, VALUE obj, int type) { +#ifdef __DEBUG__ + rb_p(rb_str_new2("Encoding binary (buf, obj, type)")); + rb_p(rb_inspect(buf)); + rb_p(rb_inspect(obj)); + rb_p(rb_inspect(INT2FIX(type))); +#endif + + switch(type) { + case T_BOOL: + if RTEST(obj) { + write_byte(buf, 1); + } + else { + write_byte(buf, 0); + } + + break; + + case T_BYTE: + write_byte(buf, NUM2INT(obj)); + break; + + case T_I16: + write_i16(buf, NUM2INT(obj)); + break; + + case T_I32: + write_i32(buf, NUM2INT(obj)); + break; + + case T_I64: + write_i64(buf, rb_num2ll(obj)); + break; + + case T_DBL: + write_double(buf, NUM2DBL(obj)); + break; + + case T_STR: + write_string(buf, StringValuePtr(obj)); + break; + + case T_STRCT: { + // rb_hash_foreach has to take args as a ruby array + VALUE args = rb_ary_new3(2, buf, obj); + VALUE fields = rb_const_get(CLASS_OF(obj), fields_id); + + write_struct_begin(buf); + + rb_hash_foreach(fields, encode_field, args); + + write_field_stop(buf); + write_struct_end(buf); + break; + } + + default: { + rb_raise(rb_eNotImpError, "Unknown type for binary_encoding: %d", type); + } + } +} + +// obj is always going to be a TSTRCT +static VALUE tbpa_encode_binary(VALUE self, VALUE obj) { + VALUE buf = rb_str_buf_new(1024); + binary_encoding(buf, obj, T_STRCT); + return buf; +} + + +// ----------------------------------------------------------------------------- +// Read stuff +// ----------------------------------------------------------------------------- + +typedef struct { + char* name; + int8_t type; + int16_t id; +} field_header; + +typedef struct { + int8_t key_type; + int8_t val_type; + int num_entries; +} map_header; + +typedef struct { + int8_t type; + int num_elements; +} list_header; + +typedef list_header set_header; + +typedef struct { + char* data; + int pos; + int len; + VALUE trans; +} decode_buffer; + +typedef struct { + char* ptr; + int len; +} thrift_string; + +#define read_struct_begin(buf) +#define read_struct_end(buf) + +// This prototype is required to be able to run a call through rb_protect +// which rescues from ruby exceptions +static VALUE protectable_consume(VALUE args) { + VALUE trans = rb_ary_entry(args, 0); + VALUE size = rb_ary_entry(args, 1); + + return rb_funcall(trans, consume_bang_id, 1, size); +} + +// Clears size bytes from the transport's string buffer +static bool consume(decode_buffer* buf, int32_t size) { + if (size != 0) { + VALUE ret; + VALUE args = rb_ary_new3(2, buf->trans, INT2FIX(size)); + int status = 0; + + ret = rb_protect(protectable_consume, args, &status); + + if (status) { + return false; + } else { + return true; + } + } + + // Nothing to consume, we're all good + return true; +} + +// This prototype is required to be able to run a call through rb_protect +// which rescues from ruby exceptions +static VALUE protectable_borrow(VALUE args) { + VALUE trans = rb_ary_entry(args, 0); + + switch(RARRAY(args)->len) { + case 1: + return rb_funcall(trans, borrow_id, 0); + + case 2: { + VALUE size = rb_ary_entry(args, 1); + return rb_funcall(trans, borrow_id, 1, size); + } + } + + return Qnil; +} + +// Calls into the transport to get the available string buffer +static bool borrow(decode_buffer* buf, int32_t size, VALUE* dst) { + int status = 0; + VALUE args; + + if (size == 0) { + args = rb_ary_new3(1, buf->trans); + } else { + args = rb_ary_new3(2, buf->trans, INT2FIX(size)); + } + + *dst = rb_protect(protectable_borrow, args, &status); + + return (status == 0); +} + +// Refills the buffer by calling borrow. If buf->pos is nonzero that number of bytes +// is cleared through consume. +// +// returns: 0 on success, non-zero on failure. On error buf is unchanged. +static int fill_buffer(decode_buffer* buf, int32_t req_len) { + VALUE refill; + + if (!consume(buf, buf->pos)) { + return -1; + } + + if (!borrow(buf, req_len, &refill)) { + return -2; + } + + buf->data = StringValuePtr(refill); + buf->len = RSTRING(refill)->len; + buf->pos = 0; + + return 0; +} + + +// read_bytes pulls a number of bytes (size) from the buffer, refilling if needed, +// and places them in dst. This should _always_ be used used when reading from the buffer +// or buffered transports will be upset with you. +static bool read_bytes(decode_buffer* buf, void* dst, size_t size) { + int avail = (buf->len - buf->pos); + + if (size <= avail) { + memcpy(dst, buf->data + buf->pos, size); + buf->pos += size; + } else { + + if (avail > 0) { + // Copy what we can + memcpy(dst, buf->data + buf->pos, avail); + buf->pos += avail; + } + + if (fill_buffer(buf, size - avail) < 0) { + return false; + } + + memcpy(dst + avail, buf->data, size - avail); + buf->pos += size - avail; + } + + return true; +} + +// ----------------------------------------------------------------------------- +// Helpers for grabbing specific types from the buffer +// ----------------------------------------------------------------------------- + +static bool read_byte(decode_buffer* buf, int8_t* data) { + return read_bytes(buf, data, sizeof(int8_t)); +} + +static bool read_int16(decode_buffer* buf, int16_t* data) { + bool success = read_bytes(buf, data, sizeof(int16_t)); + *data = ntohs(*data); + + return success; +} + +static bool read_int32(decode_buffer* buf, int32_t* data) { + bool success = read_bytes(buf, data, sizeof(int32_t)); + *data = ntohl(*data); + + return success; +} + +static bool read_int64(decode_buffer* buf, int64_t* data) { + bool success = read_bytes(buf, data, sizeof(int64_t)); + *data = ntohll(*data); + + return success; +} + +static bool read_double(decode_buffer* buf, double* data) { + return read_int64(buf, (int64_t*)data); +} + +static bool read_string(decode_buffer* buf, VALUE* data) { + int len; + + if (!read_int32(buf, &len)) { + return false; + } + + if (buf->len - buf->pos >= len) { + *data = rb_str_new(buf->data + buf->pos, len); + buf->pos += len; + } + else { + char* str; + + if ((str = (char*) malloc(len)) == NULL) { + return false; + } + + if (!read_bytes(buf, str, len)) { + free(str); + return false; + } + + *data = rb_str_new(str, len); + + free(str); + } + + return true; +} + +static bool read_field_begin(decode_buffer* buf, field_header* header) { +#ifdef __DEBUG__ // No need for this in prod since I set all the fields + bzero(header, sizeof(field_header)); +#endif + + header->name = NULL; + + if (!read_byte(buf, &header->type)) { + return false; + } + + if (header->type == T_STOP) { + header->id = 0; + } else { + if (!read_int16(buf, &header->id)) { + return false; + } + } + + return true; +} + +#define read_field_end(buf) + +static bool read_map_begin(decode_buffer* buf, map_header* header) { +#ifdef __DEBUG__ // No need for this in prod since I set all the fields + bzero(header, sizeof(map_header)); +#endif + + return (read_byte(buf, &header->key_type) && + read_byte(buf, &header->val_type) && + read_int32(buf, &header->num_entries)); +} + +#define read_map_end(buf) + +static bool read_list_begin(decode_buffer* buf, list_header* header) { +#ifdef __DEBUG__ // No need for this in prod since I set all the fields + bzero(header, sizeof(list_header)); +#endif + + if (!read_byte(buf, &header->type) || !read_int32(buf, &header->num_elements)) { + return false; + } else { + return true; + } +} + +#define read_list_end(buf) + +#define read_set_begin read_list_begin +#define read_set_end read_list_end + + +// High level reader function with ruby type coercion +static bool read_type(int type, decode_buffer* buf, VALUE* dst) { + switch(type) { + case T_BOOL: { + int8_t byte; + + if (!read_byte(buf, &byte)) { + return false; + } + + if (0 == byte) { + *dst = Qfalse; + } else { + *dst = Qtrue; + } + + break; + } + + case T_BYTE: { + int8_t byte; + + if (!read_byte(buf, &byte)) { + return false; + } + + *dst = INT2FIX(byte); + break; + } + + case T_I16: { + int16_t i16; + + if (!read_int16(buf, &i16)) { + return false; + } + + *dst = INT2FIX(i16); + break; + } + + case T_I32: { + int32_t i32; + + if (!read_int32(buf, &i32)) { + return false; + } + + *dst = INT2NUM(i32); + break; + } + + case T_I64: { + int64_t i64; + + if (!read_int64(buf, &i64)) { + return false; + } + + *dst = rb_ll2inum(i64); + break; + } + + case T_DBL: { + double dbl; + + if (!read_double(buf, &dbl)) { + return false; + } + + *dst = rb_float_new(dbl); + break; + } + + case T_STR: { + VALUE str; + + if (!read_string(buf, &str)) { + return false; + } + + *dst = str; + break; + } + } + + return true; +} + +// TODO(kevinclark): Now that read_string does a malloc, +// This maybe could be modified to avoid that, and the type coercion + +// Read the bytes but don't do anything with the value +static bool skip_type(int type, decode_buffer* buf) { + switch (type) { + case T_STRCT: + read_struct_begin(buf); + while (true) { + field_header header; + + if (!read_field_begin(buf, &header)) { + return false; + } + + if (header.type == T_STOP) { + break; + } + + if (!skip_type(header.type, buf)) { + return false; + } + + read_field_end(buf); + } + read_struct_end(buf); + + break; + + case T_MAP: { + int i; + map_header header; + + if (!read_map_begin(buf, &header)) { + return false; + } + + for (i = 0; i < header.num_entries; ++i) { + if (!skip_type(header.key_type, buf)) { + return false; + } + if (!skip_type(header.val_type, buf)) { + return false; + } + } + + read_map_end(buf); + break; + } + + case T_SET: { + int i; + set_header header; + + if (!read_set_begin(buf, &header)) { + return false; + } + + for (i = 0; i < header.num_elements; ++i) { + if (!skip_type(header.type, buf)) { + return false; + } + } + + read_set_end(buf); + break; + } + + case T_LIST: { + int i; + list_header header; + + if (!read_list_begin(buf, &header)) { + return false; + } + + for (i = 0; i < header.num_elements; ++i) { + if (!skip_type(header.type, buf)) { + return false; + } + } + + read_list_end(buf); + break; + } + + default: { + VALUE v; + if (!read_type(type, buf, &v)) { + return false; + } + } + } + + return true; +} + + +static VALUE read_struct(VALUE obj, decode_buffer* buf); + +// Read the right thing from the buffer given the field spec +// and return the ruby object +static bool read_field(decode_buffer* buf, field_spec* spec, VALUE* dst) { + switch (spec->type) { + case T_STRCT: { + VALUE obj = rb_class_new_instance(0, NULL, spec->data.class); + + *dst = read_struct(obj, buf); + break; + } + + case T_MAP: { + map_header hdr; + VALUE hsh; + int i; + + read_map_begin(buf, &hdr); + hsh = rb_hash_new(); + + for (i = 0; i < hdr.num_entries; ++i) { + VALUE key, val; + + if (!read_field(buf, spec->data.map->key, &key)) { + return false; + } + + if (!read_field(buf, spec->data.map->value, &val)) { + return false; + } + + rb_hash_aset(hsh, key, val); + } + + read_map_end(buf); + + *dst = hsh; + break; + } + + case T_LIST: { + list_header hdr; + VALUE arr, element; + int i; + + read_list_begin(buf, &hdr); + arr = rb_ary_new2(hdr.num_elements); + + for (i = 0; i < hdr.num_elements; ++i) { + if (!read_field(buf, spec->data.element, &element)) { + return false; + } + + rb_ary_push(arr, element); + } + + read_list_end(buf); + + *dst = arr; + break; + } + + case T_SET: { + VALUE items, item; + set_header hdr; + int i; + + read_set_begin(buf, &hdr); + items = rb_ary_new2(hdr.num_elements); + + for (i = 0; i < hdr.num_elements; ++i) { + if (!read_field(buf, spec->data.element, &item)) { + return false; + } + + rb_ary_push(items, item); + } + + *dst = rb_class_new_instance(1, &items, rb_cSet); + break; + } + + + default: + return read_type(spec->type, buf, dst); + } + + return true; +} + +static void handle_read_error() { + // If it was an exception, reraise + if (!NIL_P(ruby_errinfo)) { + rb_exc_raise(ruby_errinfo); + } else { + // Something else went wrong, no idea what would call this yet + // So far, the only thing to cause failures underneath is ruby + // exceptions. Follow up on this regularly -- Kevin Clark (TODO) + rb_raise(rb_eStandardError, "[BUG] Something went wrong in the field reading, but not a ruby exception"); + } +} + +// Fill in the instance variables in an object (thrift struct) +// from the decode buffer +static VALUE read_struct(VALUE obj, decode_buffer* buf) { + VALUE field; + field_header f_header; + VALUE value = Qnil; + VALUE fields = rb_const_get(CLASS_OF(obj), fields_id); + field_spec* spec; + char name_buf[128]; + + read_struct_begin(buf); + + while(true) { + if (!read_field_begin(buf, &f_header)) { + handle_read_error(); + } + + if (T_STOP == f_header.type) { + break; + } + + field = rb_hash_aref(fields, INT2FIX(f_header.id)); + + if (NIL_P(field)) { + if (!skip_type(f_header.type, buf)) { + handle_read_error(); + return Qnil; + } + } + else { + spec = parse_field_spec(field); + + if (spec->type != f_header.type) { + if (!skip_type(spec->type, buf)) { + free_field_spec(spec); + handle_read_error(); + return Qnil; + } + } else { + // Read busted somewhere (probably borrow/consume), bail + if (!read_field(buf, spec, &value)) { + free_field_spec(spec); + handle_read_error(); + return Qnil; + } + + name_buf[0] = '@'; + strlcpy(&name_buf[1], spec->name, sizeof(name_buf) - 1); + + rb_iv_set(obj, name_buf, value); + } + + free_field_spec(spec); + } + + read_field_end(buf); + } + + read_struct_end(buf); + + return obj; +} + + +// Takes an object and transport, and decodes the values in the transport's +// buffer to fill the object. +static VALUE tbpa_decode_binary(VALUE self, VALUE obj, VALUE transport) { + decode_buffer buf; + VALUE ret_val; + + buf.pos = 0; // This needs to be set so an arbitrary number of bytes isn't consumed + buf.trans = transport; // We need to hold this so the buffer can be refilled + + if (fill_buffer(&buf, 0) < 0) { + handle_read_error(); + return Qnil; + } + +#ifdef __DEBUG__ + rb_p(rb_str_new2("Running decode binary with data:")); + rb_p(rb_inspect(rb_str_new2(buf.data))); +#endif + + ret_val = read_struct(obj, &buf); + + // Consume whatever was read + consume(&buf, buf.pos); + + return ret_val; +} + +// ----------------------------------------------------------------------------- +// These methods are used by the thrift library and need to handled +// seperately from encode and decode +// ----------------------------------------------------------------------------- + +// Read the message header and return it as a ruby array +static VALUE tbpa_read_message_begin(VALUE self) { + decode_buffer buf; + int32_t version, seqid; + int8_t type; + VALUE name; + + VALUE trans = rb_iv_get(self, "@trans"); + + buf.pos = 0; // This needs to be set so fill_buffer doesn't consume + buf.trans = trans; // We need to hold this so the buffer can be refilled + + + if (fill_buffer(&buf, 0) < 0 || !read_int32(&buf, &version)) { + // Consume whatever was read + consume(&buf, buf.pos); + handle_read_error(); + return Qnil; + } + + if ((version & VERSION_MASK) != VERSION_1) { + VALUE tprotocol_exception = rb_const_get(rb_cObject, rb_intern("TProtocolException")); + VALUE exception = rb_funcall(tprotocol_exception, rb_intern("new"), 2, rb_const_get(tprotocol_exception, rb_intern("BAD_VERSION")), rb_str_new2("Missing version identifier")); + rb_raise(exception, ""); + } + + type = version & 0x000000ff; + + if (!read_string(&buf, &name) || !read_int32(&buf, &seqid)) { + // Consume whatever was read + consume(&buf, buf.pos); + handle_read_error(); + return Qnil; + } + + // Consume whatever was read + if (consume(&buf, buf.pos) < 0) { + handle_read_error(); + return Qnil; + } + + return rb_ary_new3(3, name, INT2FIX(type), INT2FIX(seqid)); +} + +void Init_binaryprotocolaccelerated() +{ + m_thrift = rb_const_get(rb_cObject, rb_intern("Thrift")); + VALUE class_tbinproto = rb_const_get(m_thrift, rb_intern("BinaryProtocol")); + class_tbpa = rb_define_class_under(m_thrift, "BinaryProtocolAccelerated", class_tbinproto); + type_sym = ID2SYM(rb_intern("type")); + class_sym = ID2SYM(rb_intern("class")); + key_sym = ID2SYM(rb_intern("key")); + value_sym = ID2SYM(rb_intern("value")); + name_sym = ID2SYM(rb_intern("name")); + fields_id = rb_intern("FIELDS"); + element_sym = ID2SYM(rb_intern("element")); + consume_bang_id = rb_intern("consume!"); + string_buffer_id = rb_intern("string_buffer"); + borrow_id = rb_intern("borrow"); + keys_id = rb_intern("keys"); + entries_id = rb_intern("entries"); + rb_cSet = rb_const_get(rb_cObject, rb_intern("Set")); + + // For fast access + rb_define_method(class_tbpa, "encode_binary", tbpa_encode_binary, 1); + rb_define_method(class_tbpa, "decode_binary", tbpa_decode_binary, 2); + rb_define_method(class_tbpa, "read_message_begin", tbpa_read_message_begin, 0); + +} diff --git a/lib/rb/ext/extconf.rb b/lib/rb/ext/extconf.rb new file mode 100644 index 00000000..54ad5ed5 --- /dev/null +++ b/lib/rb/ext/extconf.rb @@ -0,0 +1,7 @@ +require 'mkmf' + +$CFLAGS = "-g -O2 -Wall -Werror" + +have_func("strlcpy", "string.h") + +create_makefile 'binaryprotocolaccelerated' diff --git a/lib/rb/lib/thrift/protocol/binaryprotocolaccelerated.rb b/lib/rb/lib/thrift/protocol/binaryprotocolaccelerated.rb new file mode 100644 index 00000000..fae98190 --- /dev/null +++ b/lib/rb/lib/thrift/protocol/binaryprotocolaccelerated.rb @@ -0,0 +1,19 @@ +require 'thrift/protocol/binaryprotocol' +require 'binaryprotocolaccelerated' + +=begin +The only change required for a transport to support TBinaryProtocolAccelerated is to implement 2 methods: + * borrow(size), which takes an optional argument and returns atleast _size_ bytes from the transport, + or the default buffer size if no argument is given + * consume!(size), which removes size bytes from the front of the buffer + +See TMemoryBuffer and TBufferedTransport for examples. +=end + +module Thrift + class BinaryProtocolAcceleratedFactory < ProtocolFactory + def get_protocol(trans) + BinaryProtocolAccelerated.new(trans) + end + end +end diff --git a/lib/rb/lib/thrift/server/nonblockingserver.rb b/lib/rb/lib/thrift/server/nonblockingserver.rb index dc0f646d..df199cc7 100644 --- a/lib/rb/lib/thrift/server/nonblockingserver.rb +++ b/lib/rb/lib/thrift/server/nonblockingserver.rb @@ -72,6 +72,8 @@ module Thrift end class IOManager # :nodoc: + DEFAULT_BUFFER = 2**20 + def initialize(processor, serverTransport, transportFactory, protocolFactory, num, logger) @processor = processor @serverTransport = serverTransport @@ -116,7 +118,7 @@ module Thrift end private - + def run spin_worker_threads @@ -139,7 +141,7 @@ module Thrift end def read_connection(fd) - @buffers[fd] << fd.readpartial(1048576) + @buffers[fd] << fd.read(DEFAULT_BUFFER) frame = slice_frame!(@buffers[fd]) if frame @logger.debug "#{self} is processing a frame" diff --git a/lib/rb/lib/thrift/struct.rb b/lib/rb/lib/thrift/struct.rb index b016f136..0535948f 100644 --- a/lib/rb/lib/thrift/struct.rb +++ b/lib/rb/lib/thrift/struct.rb @@ -23,31 +23,41 @@ module Thrift end def read(iprot) - iprot.read_struct_begin - loop do - fname, ftype, fid = iprot.read_field_begin - break if (ftype == Types::STOP) - handle_message(iprot, fid, ftype) - iprot.read_field_end + # TODO(kevinclark): Make sure transport is C readable + if iprot.respond_to?(:decode_binary) + iprot.decode_binary(self, iprot.trans) + else + iprot.read_struct_begin + loop do + fname, ftype, fid = iprot.read_field_begin + break if (ftype == Types::STOP) + handle_message(iprot, fid, ftype) + iprot.read_field_end + end + iprot.read_struct_end end - iprot.read_struct_end end def write(oprot) - oprot.write_struct_begin(self.class.name) - each_field do |fid, type, name| - unless (value = instance_variable_get("@#{name}")).nil? - if is_container? type - oprot.write_field_begin(name, type, fid) - write_container(oprot, value, struct_fields[fid]) - oprot.write_field_end - else - oprot.write_field(name, type, fid, value) + if oprot.respond_to?(:encode_binary) + # TODO(kevinclark): Clean this so I don't have to access the transport. + oprot.trans.write oprot.encode_binary(self) + else + oprot.write_struct_begin(self.class.name) + each_field do |fid, type, name| + unless (value = instance_variable_get("@#{name}")).nil? + if is_container? type + oprot.write_field_begin(name, type, fid) + write_container(oprot, value, struct_fields[fid]) + oprot.write_field_end + else + oprot.write_field(name, type, fid, value) + end end end + oprot.write_field_stop + oprot.write_struct_end end - oprot.write_field_stop - oprot.write_struct_end end def ==(other) diff --git a/lib/rb/lib/thrift/transport.rb b/lib/rb/lib/thrift/transport.rb index 36492dd7..d55adf0d 100644 --- a/lib/rb/lib/thrift/transport.rb +++ b/lib/rb/lib/thrift/transport.rb @@ -76,9 +76,12 @@ module Thrift deprecate_class! :TTransportFactory => TransportFactory class BufferedTransport < Transport + DEFAULT_BUFFER = 4096 + def initialize(transport) @transport = transport @wbuf = '' + @rbuf = '' end def open? @@ -95,7 +98,13 @@ module Thrift end def read(sz) - return @transport.read(sz) + ret = @rbuf.slice!(0...sz) + if ret.length == 0 + @rbuf = @transport.read([sz, DEFAULT_BUFFER].max) + @rbuf.slice!(0...sz) + else + ret + end end def write(buf) @@ -110,6 +119,25 @@ module Thrift @transport.flush end + + def borrow(requested_length = 0) + # $stderr.puts "#{Time.now.to_f} Have #{@rbuf.length} asking for #{requested_length.inspect}" + return @rbuf if @rbuf.length > requested_length + + if @rbuf.length < DEFAULT_BUFFER + @rbuf << @transport.read([requested_length, DEFAULT_BUFFER].max) + end + + if @rbuf.length < requested_length + @rbuf << @transport.read_all(requested_length - @rbuf.length) + end + + @rbuf + end + + def consume!(size) + @rbuf.slice!(0...size) + end end deprecate_class! :TBufferedTransport => BufferedTransport @@ -230,6 +258,23 @@ module Thrift def flush end + + # For fast binary protocol access + def borrow(size = nil) + if size.nil? + @buf[0..-1] + else + if size > @buf.length + raise EOFError # Memory buffers only get one shot. + else + @buf[0..size] + end + end + end + + def consume!(size) + @buf.slice!(0, size) + end end deprecate_class! :TMemoryBuffer => MemoryBuffer diff --git a/lib/rb/lib/thrift/transport/socket.rb b/lib/rb/lib/thrift/transport/socket.rb index e3981fe3..c7ed521b 100644 --- a/lib/rb/lib/thrift/transport/socket.rb +++ b/lib/rb/lib/thrift/transport/socket.rb @@ -43,17 +43,11 @@ module Thrift end end - def read(sz, partial=false) + def read(sz) raise IOError, "closed stream" unless open? + begin - if partial - data = @handle.readpartial(sz) - else - data = @handle.read(sz) - end - rescue Errno::EAGAIN => e - # let our parent know that the nonblock read failed - raise e + data = @handle.readpartial(sz) rescue StandardError => e @handle.close unless @handle.closed? @handle = nil @@ -65,10 +59,6 @@ module Thrift data end - def readpartial(sz) - read(sz, true) - end - def close @handle.close unless @handle.nil? or @handle.closed? @handle = nil diff --git a/lib/rb/setup.rb b/lib/rb/setup.rb new file mode 100644 index 00000000..9f0c8267 --- /dev/null +++ b/lib/rb/setup.rb @@ -0,0 +1,1585 @@ +# +# setup.rb +# +# Copyright (c) 2000-2005 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +unless Errno.const_defined?(:ENOTEMPTY) # Windows? + module Errno + class ENOTEMPTY + # We do not raise this exception, implementation is not needed. + end + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted Windows' stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class ConfigTable + + include Enumerable + + def initialize(rbconfig) + @rbconfig = rbconfig + @items = [] + @table = {} + # options + @install_prefix = nil + @config_opt = nil + @verbose = true + @no_harm = false + end + + attr_accessor :install_prefix + attr_accessor :config_opt + + attr_writer :verbose + + def verbose? + @verbose + end + + attr_writer :no_harm + + def no_harm? + @no_harm + end + + def [](key) + lookup(key).resolve(self) + end + + def []=(key, val) + lookup(key).set val + end + + def names + @items.map {|i| i.name } + end + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or setup_rb_error "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def load_script(path, inst = nil) + if File.file?(path) + MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path + end + end + + def savefile + '.config' + end + + def load_savefile + begin + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + self[k] = v.strip + end + rescue Errno::ENOENT + setup_rb_error $!.message + "\n#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value? and i.value + end + } + end + + def load_standard_entries + standard_entries(@rbconfig).each do |ent| + add ent + end + end + + def standard_entries(rbconfig) + c = rbconfig + + rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + if c['rubylibdir'] + # V > 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = c['rubylibdir'] + librubyverarch = c['archdir'] + siteruby = c['sitedir'] + siterubyver = c['sitelibdir'] + siterubyverarch = c['sitearchdir'] + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = c['sitedir'] + siterubyver = "$siteruby/#{version}" + siterubyverarch = "$siterubyver/#{c['arch']}" + else + # V < 1.4.4 + libruby = "#{c['prefix']}/lib/ruby" + librubyver = "#{c['prefix']}/lib/ruby/#{version}" + librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" + siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" + siterubyver = siteruby + siterubyverarch = "$siterubyver/#{c['arch']}" + end + parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') + } + + if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] + else + makeprog = 'make' + end + + [ + ExecItem.new('installdirs', 'std/site/home', + 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ + {|val, table| + case val + when 'std' + table['rbdir'] = '$librubyver' + table['sodir'] = '$librubyverarch' + when 'site' + table['rbdir'] = '$siterubyver' + table['sodir'] = '$siterubyverarch' + when 'home' + setup_rb_error '$HOME was not set' unless ENV['HOME'] + table['prefix'] = ENV['HOME'] + table['rbdir'] = '$libdir/ruby' + table['sodir'] = '$libdir/ruby' + end + }, + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', parameterize.call(c['libdir']), + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for system configuration files'), + PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), + 'the directory for local state data'), + PathItem.new('libruby', 'path', libruby, + 'the directory for ruby libraries'), + PathItem.new('librubyver', 'path', librubyver, + 'the directory for standard ruby libraries'), + PathItem.new('librubyverarch', 'path', librubyverarch, + 'the directory for standard ruby extensions'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') + ] + end + private :standard_entries + + def load_multipackage_entries + multipackage_entries().each do |ent| + add ent + end + end + + def multipackage_entries + [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') + ] + end + private :multipackage_entries + + ALIASES = { + 'std-ruby' => 'librubyver', + 'stdruby' => 'librubyver', + 'rubylibdir' => 'librubyver', + 'archdir' => 'librubyverarch', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } + + def fixup + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + @items.freeze + @table.freeze + @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ + end + + def parse_opt(opt) + m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" + m.to_a[1,2] + end + + def dllext + @rbconfig['DLEXT'] + end + + def value_config?(name) + lookup(name).value? + end + + class Item + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value? + true + end + + def value + @value + end + + def resolve(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end + end + + class BoolItem < Item + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + case val + when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' + when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' + else + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + end + end + + class PathItem < Item + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end + end + + class ProgramItem < Item + def config_type + 'program' + end + end + + class SelectItem < Item + def initialize(name, selection, default, desc) + super + @ok = selection.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end + end + + class ExecItem < Item + def initialize(name, selection, desc, &block) + super name, selection, nil, desc + @ok = selection.split('/') + @action = block + end + + def config_type + 'exec' + end + + def value? + false + end + + def resolve(table) + setup_rb_error "$#{name()} wrongly used as option value" + end + + undef set + + def evaluate(val, table) + v = val.strip.downcase + unless @ok.include?(v) + setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" + end + @action.call v, table + end + end + + class PackageSelectionItem < Item + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end + end + + class MetaConfigEnvironment + def initialize(config, installer) + @config = config + @installer = installer + end + + def config_names + @config.names + end + + def config?(name) + @config.key?(name) + end + + def bool_config?(name) + @config.lookup(name).config_type == 'bool' + end + + def path_config?(name) + @config.lookup(name).config_type == 'path' + end + + def value_config?(name) + @config.lookup(name).config_type != 'exec' + end + + def add_config(item) + @config.add item + end + + def add_bool_config(name, default, desc) + @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + @config.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + @config.lookup(name).default = default + end + + def remove_config(name) + @config.remove(name) + end + + # For only multipackage + def packages + raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer + @installer.packages + end + + # For only multipackage + def declare_packages(list) + raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer + @installer.packages = list + end + end + +end # class ConfigTable + + +# This module requires: #verbose?, #no_harm? +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # Does not check '/', it's too abnormal. + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(path) + $stderr.puts "rm -f #{path}" if verbose? + return if no_harm? + force_remove_file path + end + + def rm_rf(path) + $stderr.puts "rm -rf #{path}" if verbose? + return if no_harm? + remove_tree path + end + + def remove_tree(path) + if File.symlink?(path) + remove_file path + elsif File.dir?(path) + remove_tree0 path + else + force_remove_file path + end + end + + def remove_tree0(path) + Dir.foreach(path) do |ent| + next if ent == '.' + next if ent == '..' + entpath = "#{path}/#{ent}" + if File.symlink?(entpath) + remove_file entpath + elsif File.dir?(entpath) + remove_tree0 entpath + else + force_remove_file entpath + end + end + begin + Dir.rmdir path + rescue Errno::ENOTEMPTY + # directory may not be empty + end + end + + def move_file(src, dest) + force_remove_file dest + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| + f.write File.binread(src) + } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def force_remove_file(path) + begin + remove_file path + rescue + end + end + + def remove_file(path) + File.chmod 0777, path + File.unlink path + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(*args) + $stderr.puts args.join(' ') if verbose? + system(*args) or raise RuntimeError, + "system(#{args.map{|a| a.inspect }.join(' ')}) failed" + end + + def ruby(*args) + command config('rubyprog'), *args + end + + def make(task = nil) + command(*[config('makeprog'), task].compact) + end + + def extdir?(dir) + File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") + end + + def files_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.file?("#{dir}/#{ent}") } + } + end + + DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) + + def directories_of(dir) + Dir.open(dir) {|d| + return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT + } + end + +end + + +# This module requires: #srcdir_root, #objdir_root, #relpath +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + # obsolete: use metaconfig to change configuration + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file?(srcfile(path)) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.4.1' + Copyright = 'Copyright (c) 2000-2005 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'test', 'run all tests in test/' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + config = ConfigTable.new(load_rbconfig()) + config.load_standard_entries + config.load_multipackage_entries if multipackage? + config.fixup + klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) + klass.new(File.dirname($0), config).invoke + end + + def ToplevelInstaller.multipackage? + File.dir?(File.dirname($0) + '/packages') + end + + def ToplevelInstaller.load_rbconfig + if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + load File.expand_path(arg.split(/=/, 2)[1]) + $".push 'rbconfig.rb' + else + require 'rbconfig' + end + ::Config::CONFIG + end + + def initialize(ardir_root, config) + @ardir = File.expand_path(ardir_root) + @config = config + # cache + @valid_task_re = nil + end + + def config(key) + @config[key] + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + case task + when 'config', 'test' + ; + when 'clean', 'distclean' + @config.load_savefile if File.exist?(@config.savefile) + else + @config.load_savefile + end + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig" + end + + def init_installers + @installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) + return arg + when '-q', '--quiet' + @config.verbose = false + when '--verbose' + @config.verbose = true + when '--help' + print_usage $stdout + exit 0 + when '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + when '--copyright' + puts Copyright + exit 0 + else + setup_rb_error "unknown global option '#{arg}'" + end + end + nil + end + + def valid_task?(t) + valid_task_re() =~ t + end + + def valid_task_re + @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ + end + + def parsearg_no_options + unless ARGV.empty? + task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) + setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_test parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + evalopt = [] + set = [] + @config.config_opt = [] + while i = ARGV.shift + if /\A--?\z/ =~ i + @config.config_opt = ARGV.dup + break + end + name, value = *@config.parse_opt(i) + if @config.value_config?(name) + @config[name] = value + else + evalopt.push [name, value] + end + set.push name + end + evalopt.each do |name, value| + @config.lookup(name).evaluate value, @config + end + # Check if configuration is valid + set.each do |n| + @config[n] if @config.value_config?(n) + end + end + + def parsearg_install + @config.no_harm = false + @config.install_prefix = '' + while a = ARGV.shift + case a + when '--no-harm' + @config.no_harm = true + when /\A--prefix=/ + path = a.split(/=/, 2)[1] + path = File.expand_path(path) unless path[0,1] == '/' + @config.install_prefix = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, ' --help', 'print this message' + out.printf fmt, ' --version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + @config.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_test + @installer.exec_test + end + + def exec_show + @config.each do |i| + printf "%-20s %s\n", i.name, i.value if i.value? + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end # class ToplevelInstaller + + +class ToplevelInstallerMulti < ToplevelInstaller + + include FileOperations + + def initialize(ardir_root, config) + super + @packages = directories_of("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) + end + + def run_metaconfigs + @config.load_script "#{@ardir}/metaconfig", self + @packages.each do |name| + @config.load_script "#{@ardir}/packages/#{name}/metaconfig" + end + end + + attr_reader :packages + + def packages=(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_test + run_hook 'pre-test' + each_selected_installers {|inst| inst.exec_test } + run_hook 'post-test' + end + + def exec_clean + rm_f @config.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f @config.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if verbose? + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def run_hook(id) + @root_installer.run_hook id + end + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + +end # class ToplevelInstallerMulti + + +class Installer + + FILETYPES = %w( bin lib ext data conf man ) + + include FileOperations + include HookScriptAPI + + def initialize(config, srcroot, objroot) + @config = config + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + def noop(rel) + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # Config Access + # + + # module FileOperations requires this + def verbose? + @config.verbose? + end + + # module FileOperations requires this + def no_harm? + @config.no_harm? + end + + def verbose_off + begin + save, @config.verbose = @config.verbose?, false + yield + ensure + @config.verbose = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + alias config_dir_bin noop + alias config_dir_lib noop + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + alias config_dir_data noop + alias config_dir_conf noop + alias config_dir_man noop + + def extconf + ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + files_of(curr_srcdir()).each do |fname| + update_shebang_line "#{curr_srcdir()}/#{fname}" + end + end + + alias setup_dir_lib noop + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + alias setup_dir_data noop + alias setup_dir_conf noop + alias setup_dir_man noop + + def update_shebang_line(path) + return if no_harm? + return if config('shebang') == 'never' + old = Shebang.load(path) + if old + $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 + new = new_shebang(old) + return if new.to_s == old.to_s + else + return unless config('shebang') == 'all' + new = Shebang.new(config('rubypath')) + end + $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? + open_atomic_writer(path) {|output| + File.open(path, 'rb') {|f| + f.gets if old # discard + output.puts new.to_s + output.print f.read + } + } + end + + def new_shebang(old) + if /\Aruby/ =~ File.basename(old.cmd) + Shebang.new(config('rubypath'), old.args) + elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' + Shebang.new(config('rubypath'), old.args[1..-1]) + else + return old unless config('shebang') == 'all' + Shebang.new(config('rubypath')) + end + end + + def open_atomic_writer(path, &block) + tmpfile = File.basename(path) + '.tmp' + begin + File.open(tmpfile, 'wb', &block) + File.rename tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + class Shebang + def Shebang.load(path) + line = nil + File.open(path) {|f| + line = f.gets + } + return nil unless /\A#!/ =~ line + parse(line) + end + + def Shebang.parse(line) + cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') + new(cmd, args) + end + + def initialize(cmd, args = []) + @cmd = cmd + @args = args + end + + attr_reader :cmd + attr_reader :args + + def to_s + "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") + end + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files rubyextentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_dir_conf(rel) + # FIXME: should not remove current config files + # (rename previous file to .old/.org) + install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 + end + + def install_dir_man(rel) + install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @config.install_prefix + list.each do |fname| + install fname, dest, mode, @config.install_prefix + end + end + + def libfiles + glob_reject(%w(*.y *.output), targetfiles()) + end + + def rubyextentions(dir) + ents = glob_select("*.#{@config.dllext}", targetfiles()) + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + ents + end + + def targetfiles + mapdir(existfiles() - hookfiles()) + end + + def mapdir(ents) + ents.map {|ent| + if File.exist?(ent) + then ent # objdir + else "#{curr_srcdir()}/#{ent}" # srcdir + end + } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + JUNK_FILES = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + + def existfiles + glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def glob_select(pat, ents) + re = globs2re([pat]) + ents.select {|ent| re =~ ent } + end + + def glob_reject(pats, ents) + re = globs2re(pats) + ents.reject {|ent| re =~ ent } + end + + GLOB2REGEX = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + + def globs2re(pats) + /\A(?:#{ + pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') + })\z/ + end + + # + # TASK test + # + + TESTDIR = 'test' + + def exec_test + unless File.directory?('test') + $stderr.puts 'no test in this package' if verbose? + return + end + $stderr.puts 'Running tests...' if verbose? + begin + require 'test/unit' + rescue LoadError + setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' + end + runner = Test::Unit::AutoRunner.new(true) + runner.to_run << TESTDIR + runner.run + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias clean_dir_bin noop + alias clean_dir_lib noop + alias clean_dir_data noop + alias clean_dir_conf noop + alias clean_dir_man noop + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f @config.savefile + rm_f 'InstalledFiles' + end + + alias distclean_dir_bin noop + alias distclean_dir_lib noop + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + alias distclean_dir_data noop + alias distclean_dir_conf noop + alias distclean_dir_man noop + + # + # Traversing + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if type == 'ext' and config('without-ext') == 'yes' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + directories_of(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + + def run_hook(id) + path = [ "#{curr_srcdir()}/#{id}", + "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } + return unless path + begin + instance_eval File.read(path), path, 1 + rescue + raise if $DEBUG + setup_rb_error "hook #{path} failed:\n" + $!.message + end + end + +end # class Installer + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +if $0 == __FILE__ + begin + ToplevelInstaller.invoke + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/lib/rb/spec/socket_spec_shared.rb b/lib/rb/spec/socket_spec_shared.rb index a0092e18..b32ab44a 100644 --- a/lib/rb/spec/socket_spec_shared.rb +++ b/lib/rb/spec/socket_spec_shared.rb @@ -26,19 +26,13 @@ shared_examples_for "a socket" do it "should raise an error when it cannot read from the handle" do @socket.open - @handle.should_receive(:read).with(17).and_raise(StandardError) + @handle.should_receive(:readpartial).with(17).and_raise(StandardError) lambda { @socket.read(17) }.should raise_error(Thrift::TransportException) { |e| e.type.should == Thrift::TransportException::NOT_OPEN } end - it "should raise an error when it reads no data from the handle" do - @socket.open - @handle.should_receive(:read).with(17).and_return("") - lambda { @socket.read(17) }.should raise_error(Thrift::TransportException, "Socket: Could not read 17 bytes from #{@socket.instance_variable_get("@desc")}") - end - it "should return the data read when reading from the handle works" do @socket.open - @handle.should_receive(:read).with(17).and_return("test data") + @handle.should_receive(:readpartial).with(17).and_return("test data") @socket.read(17).should == "test data" end diff --git a/lib/rb/spec/transport_spec.rb b/lib/rb/spec/transport_spec.rb index 44d0508a..1fe8600f 100644 --- a/lib/rb/spec/transport_spec.rb +++ b/lib/rb/spec/transport_spec.rb @@ -48,18 +48,26 @@ class ThriftTransportSpec < Spec::ExampleGroup end describe BufferedTransport do - it "should pass through everything but write/flush" do + it "should pass through everything but write/flush/read" do trans = mock("Transport") trans.should_receive(:open?).ordered.and_return("+ open?") trans.should_receive(:open).ordered.and_return("+ open") trans.should_receive(:flush).ordered # from the close trans.should_receive(:close).ordered.and_return("+ close") - trans.should_receive(:read).with(217).ordered.and_return("+ read") btrans = BufferedTransport.new(trans) btrans.open?.should == "+ open?" btrans.open.should == "+ open" btrans.close.should == "+ close" - btrans.read(217).should == "+ read" + end + + it "should buffer reads in chunks of #{BufferedTransport::DEFAULT_BUFFER}" do + trans = mock("Transport") + trans.should_receive(:read).with(BufferedTransport::DEFAULT_BUFFER).and_return("lorum ipsum dolor emet") + btrans = BufferedTransport.new(trans) + btrans.read(6).should == "lorum " + btrans.read(6).should == "ipsum " + btrans.read(6).should == "dolor " + btrans.read(6).should == "emet" end it "should buffer writes and send them on flush" do diff --git a/test/Makefile.am b/test/Makefile.am index 47e6b761..bac5b06e 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -8,6 +8,10 @@ if WITH_PYTHON SUBDIRS += py endif +if ENABLE_RUBY +SUBDIRS += rb +endif + noinst_LTLIBRARIES = libtestgencpp.la libtestgencpp_la_SOURCES = \ gen-cpp/DebugProtoTest_types.cpp \ diff --git a/test/SmallTest.thrift b/test/SmallTest.thrift index 36ee3499..dd5634d6 100644 --- a/test/SmallTest.thrift +++ b/test/SmallTest.thrift @@ -11,6 +11,10 @@ senum Thinger { "ASDFLJASDF" } +struct BoolPasser { + 1: bool value = 1 +} + struct Hello { 1: i32 simple = 53, 2: map complex = {23:532, 6243:632, 2355:532}, diff --git a/test/rb/Makefile b/test/rb/Makefile deleted file mode 100644 index 5c1e61a8..00000000 --- a/test/rb/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# Makefile for Thrift test project. -# -# Author: -# Mark Slee - -# Default target is everything -target: all - -# Tools -THRIFT = ../../compiler/cpp/thrift - -all: stubs check - -stubs: ../ThriftTest.thrift ../SmallTest.thrift - $(THRIFT) --gen rb ../ThriftTest.thrift - $(THRIFT) --gen rb ../SmallTest.thrift - -check: stubs - ruby test_suite.rb - -clean: - $(RM) -r gen-rb diff --git a/test/rb/Makefile.am b/test/rb/Makefile.am new file mode 100644 index 00000000..b19fcde9 --- /dev/null +++ b/test/rb/Makefile.am @@ -0,0 +1,9 @@ +THRIFT = $(top_srcdir)/compiler/cpp/thrift + +stubs: ../ThriftTest.thrift ../SmallTest.thrift + $(THRIFT) --gen rb ../ThriftTest.thrift + $(THRIFT) --gen rb ../SmallTest.thrift + +check: stubs + $(RUBY) test_suite.rb + diff --git a/test/rb/benchmarks/protocol_benchmark.rb b/test/rb/benchmarks/protocol_benchmark.rb new file mode 100644 index 00000000..99f07601 --- /dev/null +++ b/test/rb/benchmarks/protocol_benchmark.rb @@ -0,0 +1,158 @@ +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. .. .. lib rb lib]) +$LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. .. .. lib rb ext]) + +require 'thrift' +require 'thrift/transport' +require 'thrift/protocol/binaryprotocol' +require 'thrift/protocol/binaryprotocolaccelerated' + +require 'benchmark' +require 'rubygems' +require 'set' +require 'pp' + +# require 'ruby-debug' +# require 'ruby-prof' + +require File.join(File.dirname(__FILE__), '../fixtures/structs') + +transport1 = Thrift::MemoryBuffer.new +ruby_binary_protocol = Thrift::BinaryProtocol.new(transport1) + +transport2 = Thrift::MemoryBuffer.new +c_fast_binary_protocol = Thrift::BinaryProtocolAccelerated.new(transport2) + + +ooe = Fixtures::Structs::OneOfEach.new +ooe.im_true = true +ooe.im_false = false +ooe.a_bite = -42 +ooe.integer16 = 27000 +ooe.integer32 = 1<<24 +ooe.integer64 = 6000 * 1000 * 1000 +ooe.double_precision = Math::PI +ooe.some_characters = "Debug THIS!" +ooe.zomg_unicode = "\xd7\n\a\t" + +n1 = Fixtures::Structs::Nested1.new +n1.a_list = [] +n1.a_list << ooe << ooe << ooe << ooe +n1.i32_map = {} +n1.i32_map[1234] = ooe +n1.i32_map[46345] = ooe +n1.i32_map[-34264] = ooe +n1.i64_map = {} +n1.i64_map[43534986783945] = ooe +n1.i64_map[-32434639875122] = ooe +n1.dbl_map = {} +n1.dbl_map[324.65469834] = ooe +n1.dbl_map[-9458672340.4986798345112] = ooe +n1.str_map = {} +n1.str_map['sdoperuix'] = ooe +n1.str_map['pwoerxclmn'] = ooe + +n2 = Fixtures::Structs::Nested2.new +n2.a_list = [] +n2.a_list << n1 << n1 << n1 << n1 << n1 +n2.i32_map = {} +n2.i32_map[398345] = n1 +n2.i32_map[-2345] = n1 +n2.i32_map[12312] = n1 +n2.i64_map = {} +n2.i64_map[2349843765934] = n1 +n2.i64_map[-123234985495] = n1 +n2.i64_map[0] = n1 +n2.dbl_map = {} +n2.dbl_map[23345345.38927834] = n1 +n2.dbl_map[-1232349.5489345] = n1 +n2.dbl_map[-234984574.23498725] = n1 +n2.str_map = {} +n2.str_map[''] = n1 +n2.str_map['sdflkertpioux'] = n1 +n2.str_map['sdfwepwdcjpoi'] = n1 + +n3 = Fixtures::Structs::Nested3.new +n3.a_list = [] +n3.a_list << n2 << n2 << n2 << n2 << n2 +n3.i32_map = {} +n3.i32_map[398345] = n2 +n3.i32_map[-2345] = n2 +n3.i32_map[12312] = n2 +n3.i64_map = {} +n3.i64_map[2349843765934] = n2 +n3.i64_map[-123234985495] = n2 +n3.i64_map[0] = n2 +n3.dbl_map = {} +n3.dbl_map[23345345.38927834] = n2 +n3.dbl_map[-1232349.5489345] = n2 +n3.dbl_map[-234984574.23498725] = n2 +n3.str_map = {} +n3.str_map[''] = n2 +n3.str_map['sdflkertpioux'] = n2 +n3.str_map['sdfwepwdcjpoi'] = n2 + +n4 = Fixtures::Structs::Nested4.new +n4.a_list = [] +n4.a_list << n3 +n4.i32_map = {} +n4.i32_map[-2345] = n3 +n4.i64_map = {} +n4.i64_map[2349843765934] = n3 +n4.dbl_map = {} +n4.dbl_map[-1232349.5489345] = n3 +n4.str_map = {} +n4.str_map[''] = n3 + + +# prof = RubyProf.profile do +# n4.write(c_fast_binary_protocol) +# Fixtures::Structs::Nested4.new.read(c_fast_binary_protocol) +# end +# +# printer = RubyProf::GraphHtmlPrinter.new(prof) +# printer.print(STDOUT, :min_percent=>0) + +Benchmark.bmbm do |x| + x.report("ruby write large (1MB) structure once") do + n4.write(ruby_binary_protocol) + end + + x.report("ruby read large (1MB) structure once") do + Fixtures::Structs::Nested4.new.read(ruby_binary_protocol) + end + + x.report("c write large (1MB) structure once") do + n4.write(c_fast_binary_protocol) + end + + x.report("c read large (1MB) structure once") do + Fixtures::Structs::Nested4.new.read(c_fast_binary_protocol) + end + + + + x.report("ruby write 10_000 small structures") do + 10_000.times do + ooe.write(ruby_binary_protocol) + end + end + + x.report("ruby read 10_000 small structures") do + 10_000.times do + Fixtures::Structs::OneOfEach.new.read(ruby_binary_protocol) + end + end + + x.report("c write 10_000 small structures") do + 10_000.times do + ooe.write(c_fast_binary_protocol) + end + end + + x.report("c read 10_000 small structures") do + 10_000.times do + Fixtures::Structs::OneOfEach.new.read(c_fast_binary_protocol) + end + end + +end diff --git a/test/rb/core/test_binary_protocol_accelerated.rb b/test/rb/core/test_binary_protocol_accelerated.rb new file mode 100644 index 00000000..15c033e1 --- /dev/null +++ b/test/rb/core/test_binary_protocol_accelerated.rb @@ -0,0 +1,275 @@ +require File.join(File.dirname(__FILE__), '../test_helper') +require File.join(File.dirname(__FILE__), '../fixtures/structs') + +require 'thrift' +require 'thrift/transport' +require 'thrift/protocol/binaryprotocol' +require 'thrift/protocol/binaryprotocolaccelerated' + +class BinaryProtocolAcceleratedTest < Test::Unit::TestCase + I8_MIN = -128 + I8_MAX = 127 + I16_MIN = -32768 + I16_MAX = 32767 + I32_MIN = -2147483648 + I32_MAX = 2147483647 + I64_MIN = -9223372036854775808 + I64_MAX = 9223372036854775807 + DBL_MIN = Float::MIN + DBL_MAX = Float::MAX + + # booleans might be read back differently, so we supply a list [write_value, read_value] + BOOL_VALUES = [[0,true], [14,true], [-14,true], [true,true], [false,false], ["",true]] + BYTE_VALUES = [14, -14, I8_MIN, I8_MAX] + I16_VALUES = [400, 0, -234, I16_MIN, I16_MAX] + I32_VALUES = [325, 0, -1, -1073741825, -278, -4352388, I32_MIN, I32_MAX] + I64_VALUES = [15, 0, -33, I64_MIN, I64_MAX] + DBL_VALUES = [DBL_MIN, -33.8755, 0, 3658.1279, DBL_MAX] + STR_VALUES = ["", "welcome to my test"] + + def setup + @trans = Thrift::MemoryBuffer.new + @fast_proto = Thrift::BinaryProtocolAccelerated.new(@trans) + @slow_proto = Thrift::BinaryProtocol.new(@trans) + end + + def assert_encodes_struct(obj) + obj.write(@slow_proto) + expected = @trans.read(@trans.available) # read it all baby + assert_equal expected, @fast_proto.encode_binary(obj) + end + + # Assumes encode works + def assert_decodes_struct(obj, eql_obj = nil) + data = @fast_proto.encode_binary(obj) + @trans.write data + assert_equal (eql_obj || obj), @fast_proto.decode_binary(obj.class.new, @trans) + end + + def test_encodes_and_decodes_bools + BOOL_VALUES.each do |(write_val, read_val)| + obj = Fixtures::Structs::OneBool.new(:bool => write_val) + assert_encodes_struct obj + assert_decodes_struct obj, Fixtures::Structs::OneBool.new(:bool => read_val) + end + end + + def test_encodes_and_decodes_bytes + BYTE_VALUES.each do |val| + obj = Fixtures::Structs::OneByte.new(:byte => val) + assert_encodes_struct obj + assert_decodes_struct obj + end + end + + def test_encodes_and_decodes_i16 + I16_VALUES.each do |val| + obj = Fixtures::Structs::OneI16.new(:i16 => val) + assert_encodes_struct obj + assert_decodes_struct obj + end + end + + def test_encodes_and_decodes_i32 + I32_VALUES.each do |val| + obj = Fixtures::Structs::OneI32.new(:i32 => val) + assert_encodes_struct obj + assert_decodes_struct obj + end + end + + def test_encodes_and_decodes_i64 + I64_VALUES.each do |val| + obj = Fixtures::Structs::OneI64.new(:i64 => val) + assert_encodes_struct obj + assert_decodes_struct obj + end + end + + def test_encodes_and_decodes_double + DBL_VALUES.each do |val| + obj = Fixtures::Structs::OneDouble.new(:double => val) + assert_encodes_struct obj + assert_decodes_struct obj + end + end + + def test_encodes_strings + STR_VALUES.each do |val| + obj = Fixtures::Structs::OneString.new(:string => val) + assert_encodes_struct obj + assert_decodes_struct obj + end + end + + def test_encodes_maps + obj = Fixtures::Structs::OneMap.new(:map => {"a" => "b", "c" => "d"}) + assert_encodes_struct obj + assert_decodes_struct obj + end + + def test_encodes_lists + obj = Fixtures::Structs::OneList.new(:list => ["a", "b", "c", "d"]) + assert_encodes_struct obj + assert_decodes_struct obj + end + + def test_encodes_sets + expected_return = Fixtures::Structs::OneSet.new(:set => Set.new(["a", "b", "c"])) + # support hashes + obj = Fixtures::Structs::OneSet.new(:set => {"a" => true, "b" => true, "c" => true}) + assert_encodes_struct obj + assert_decodes_struct obj, expected_return + + # We also support arrays as compatability bug from TBinaryProtocol.... + obj = Fixtures::Structs::OneSet.new(:set => ["a", "b", "c"]) + assert_encodes_struct obj + assert_decodes_struct obj, expected_return + + # We also support sets + obj = Fixtures::Structs::OneSet.new(:set => Set.new(["a", "b", "c"])) + assert_encodes_struct obj + assert_decodes_struct obj, expected_return + end + + def test_encodes_maps_of_maps + obj = Fixtures::Structs::NestedMap.new(:map => { 1 => { 2 => 3 }}) + assert_encodes_struct obj + assert_decodes_struct obj + end + + def test_encodes_list_of_lists + obj = Fixtures::Structs::NestedList.new(:list => [[1,2,3], [4,5,6]]) + assert_encodes_struct obj + assert_decodes_struct obj + end + + def test_encodes_set_of_sets + obj = Fixtures::Structs::NestedSet.new(:set => Set.new([Set.new('a')])) + assert_encodes_struct obj + + # Nested hashes won't work with ==, so we do it by hand + data = @fast_proto.encode_binary(obj) + @trans.write data + decoded = @fast_proto.decode_binary(obj.class.new, @trans) + assert_equal decoded.set.entries, Set.new([Set.new('a')]).entries + end + + if ENV['MEM_TEST'] + def test_for_memory_leaks_on_exceptions + ooe = Fixtures::Structs::OneOfEach.new + ooe.im_true = true + ooe.im_false = false + ooe.a_bite = -42 + ooe.integer16 = 27000 + ooe.integer32 = 1<<24 + ooe.integer64 = 6000 * 1000 * 1000 + ooe.double_precision = Math::PI + ooe.some_characters = "Debug THIS!" + ooe.zomg_unicode = "\xd7\n\a\t" + + 10_000.times do + data = @fast_proto.encode_binary(ooe) + bytes = [] + data.each_byte do |b| + bytes << b + transport = TMemoryBuffer.new + transport.write(bytes.pack("c#{bytes.length}")) + + begin + @fast_proto.decode_binary(Fixtures::Structs::OneOfEach.new, transport) + rescue Exception + end + end + end + + end + end + + unless ENV['FAST_TEST'] + def test_encodes_and_decodes_struct_of_structs + ooe = Fixtures::Structs::OneOfEach.new + ooe.im_true = true + ooe.im_false = false + ooe.a_bite = -42 + ooe.integer16 = 27000 + ooe.integer32 = 1<<24 + ooe.integer64 = 6000 * 1000 * 1000 + ooe.double_precision = Math::PI + ooe.some_characters = "Debug THIS!" + ooe.zomg_unicode = "\xd7\n\a\t" + + n1 = Fixtures::Structs::Nested1.new + n1.a_list = [] + n1.a_list << ooe << ooe << ooe << ooe + n1.i32_map = {} + n1.i32_map[1234] = ooe + n1.i32_map[46345] = ooe + n1.i32_map[-34264] = ooe + n1.i64_map = {} + n1.i64_map[43534986783945] = ooe + n1.i64_map[-32434639875122] = ooe + n1.dbl_map = {} + n1.dbl_map[324.65469834] = ooe + n1.dbl_map[-9458672340.4986798345112] = ooe + n1.str_map = {} + n1.str_map['sdoperuix'] = ooe + n1.str_map['pwoerxclmn'] = ooe + + n2 = Fixtures::Structs::Nested2.new + n2.a_list = [] + n2.a_list << n1 << n1 << n1 << n1 << n1 + n2.i32_map = {} + n2.i32_map[398345] = n1 + n2.i32_map[-2345] = n1 + n2.i32_map[12312] = n1 + n2.i64_map = {} + n2.i64_map[2349843765934] = n1 + n2.i64_map[-123234985495] = n1 + n2.i64_map[0] = n1 + n2.dbl_map = {} + n2.dbl_map[23345345.38927834] = n1 + n2.dbl_map[-1232349.5489345] = n1 + n2.dbl_map[-234984574.23498725] = n1 + n2.str_map = {} + n2.str_map[''] = n1 + n2.str_map['sdflkertpioux'] = n1 + n2.str_map['sdfwepwdcjpoi'] = n1 + + n3 = Fixtures::Structs::Nested3.new + n3.a_list = [] + n3.a_list << n2 << n2 << n2 << n2 << n2 + n3.i32_map = {} + n3.i32_map[398345] = n2 + n3.i32_map[-2345] = n2 + n3.i32_map[12312] = n2 + n3.i64_map = {} + n3.i64_map[2349843765934] = n2 + n3.i64_map[-123234985495] = n2 + n3.i64_map[0] = n2 + n3.dbl_map = {} + n3.dbl_map[23345345.38927834] = n2 + n3.dbl_map[-1232349.5489345] = n2 + n3.dbl_map[-234984574.23498725] = n2 + n3.str_map = {} + n3.str_map[''] = n2 + n3.str_map['sdflkertpioux'] = n2 + n3.str_map['sdfwepwdcjpoi'] = n2 + + n4 = Fixtures::Structs::Nested4.new + n4.a_list = [] + n4.a_list << n3 + n4.i32_map = {} + n4.i32_map[-2345] = n3 + n4.i64_map = {} + n4.i64_map[2349843765934] = n3 + n4.dbl_map = {} + n4.dbl_map[-1232349.5489345] = n3 + n4.str_map = {} + n4.str_map[''] = n3 + + assert_encodes_struct n4 + assert_decodes_struct n4 + end + end +end diff --git a/test/rb/fixtures/structs.rb b/test/rb/fixtures/structs.rb new file mode 100644 index 00000000..82c291ec --- /dev/null +++ b/test/rb/fixtures/structs.rb @@ -0,0 +1,225 @@ +require 'thrift' + +module Fixtures + module Structs + class OneBool + include Thrift::Struct + attr_accessor :bool + FIELDS = { + 1 => {:type => Thrift::Types::BOOL, :name => 'bool'} + } + end + + class OneByte + include Thrift::Struct + attr_accessor :byte + FIELDS = { + 1 => {:type => Thrift::Types::BYTE, :name => 'byte'} + } + end + + class OneI16 + include Thrift::Struct + attr_accessor :i16 + FIELDS = { + 1 => {:type => Thrift::Types::I16, :name => 'i16'} + } + end + + class OneI32 + include Thrift::Struct + attr_accessor :i32 + FIELDS = { + 1 => {:type => Thrift::Types::I32, :name => 'i32'} + } + end + + class OneI64 + include Thrift::Struct + attr_accessor :i64 + FIELDS = { + 1 => {:type => Thrift::Types::I64, :name => 'i64'} + } + end + + class OneDouble + include Thrift::Struct + attr_accessor :double + FIELDS = { + 1 => {:type => Thrift::Types::DOUBLE, :name => 'double'} + } + end + + class OneString + include Thrift::Struct + attr_accessor :string + FIELDS = { + 1 => {:type => Thrift::Types::STRING, :name => 'string'} + } + end + + class OneMap + include Thrift::Struct + attr_accessor :map + FIELDS = { + 1 => {:type => Thrift::Types::MAP, :name => 'map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRING}} + } + end + + class NestedMap + include Thrift::Struct + attr_accessor :map + FIELDS = { + 0 => {:type => Thrift::Types::MAP, :name => 'map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::MAP, :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::I32}}} + } + end + + class OneList + include Thrift::Struct + attr_accessor :list + FIELDS = { + 1 => {:type => Thrift::Types::LIST, :name => 'list', :element => {:type => Thrift::Types::STRING}} + } + end + + class NestedList + include Thrift::Struct + attr_accessor :list + FIELDS = { + 0 => {:type => Thrift::Types::LIST, :name => 'list', :element => {:type => Thrift::Types::LIST, :element => { :type => Thrift::Types::I32 } } } + } + end + + class OneSet + include Thrift::Struct + attr_accessor :set + FIELDS = { + 1 => {:type => Thrift::Types::SET, :name => 'set', :element => {:type => Thrift::Types::STRING}} + } + end + + class NestedSet + include Thrift::Struct + attr_accessor :set + FIELDS = { + 1 => {:type => Thrift::Types::SET, :name => 'set', :element => {:type => Thrift::Types::SET, :element => { :type => Thrift::Types::STRING } }} + } + end + + # struct OneOfEach { + # 1: bool im_true, + # 2: bool im_false, + # 3: byte a_bite, + # 4: i16 integer16, + # 5: i32 integer32, + # 6: i64 integer64, + # 7: double double_precision, + # 8: string some_characters, + # 9: string zomg_unicode, + # 10: bool what_who, + # 11: binary base64, + # } + class OneOfEach + include Thrift::Struct + attr_accessor :im_true, :im_false, :a_bite, :integer16, :integer32, :integer64, :double_precision, :some_characters, :zomg_unicode, :what_who, :base64 + FIELDS = { + 1 => {:type => Thrift::Types::BOOL, :name => 'im_true'}, + 2 => {:type => Thrift::Types::BOOL, :name => 'im_false'}, + 3 => {:type => Thrift::Types::BYTE, :name => 'a_bite'}, + 4 => {:type => Thrift::Types::I16, :name => 'integer16'}, + 5 => {:type => Thrift::Types::I32, :name => 'integer32'}, + 6 => {:type => Thrift::Types::I64, :name => 'integer64'}, + 7 => {:type => Thrift::Types::DOUBLE, :name => 'double_precision'}, + 8 => {:type => Thrift::Types::STRING, :name => 'some_characters'}, + 9 => {:type => Thrift::Types::STRING, :name => 'zomg_unicode'}, + 10 => {:type => Thrift::Types::BOOL, :name => 'what_who'}, + 11 => {:type => Thrift::Types::STRING, :name => 'base64'} + } + + # Added for assert_equal + def ==(other) + [:im_true, :im_false, :a_bite, :integer16, :integer32, :integer64, :double_precision, :some_characters, :zomg_unicode, :what_who, :base64].each do |f| + var = "@#{f}" + return false if instance_variable_get(var) != other.instance_variable_get(var) + end + true + end + end + + # struct Nested1 { + # 1: list a_list + # 2: map i32_map + # 3: map i64_map + # 4: map dbl_map + # 5: map str_map + # } + class Nested1 + include Thrift::Struct + attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + FIELDS = { + 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => OneOfEach}}, + 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => OneOfEach}}, + 3 => {:type => Thrift::Types::MAP, :name => 'i64_map', :key => {:type => Thrift::Types::I64}, :value => {:type => Thrift::Types::STRUCT, :class => OneOfEach}}, + 4 => {:type => Thrift::Types::MAP, :name => 'dbl_map', :key => {:type => Thrift::Types::DOUBLE}, :value => {:type => Thrift::Types::STRUCT, :class => OneOfEach}}, + 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => OneOfEach}} + } + end + + # struct Nested2 { + # 1: list a_list + # 2: map i32_map + # 3: map i64_map + # 4: map dbl_map + # 5: map str_map + # } + class Nested2 + include Thrift::Struct + attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + FIELDS = { + 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => Nested1}}, + 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => Nested1}}, + 3 => {:type => Thrift::Types::MAP, :name => 'i64_map', :key => {:type => Thrift::Types::I64}, :value => {:type => Thrift::Types::STRUCT, :class => Nested1}}, + 4 => {:type => Thrift::Types::MAP, :name => 'dbl_map', :key => {:type => Thrift::Types::DOUBLE}, :value => {:type => Thrift::Types::STRUCT, :class => Nested1}}, + 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => Nested1}} + } + end + + # struct Nested3 { + # 1: list a_list + # 2: map i32_map + # 3: map i64_map + # 4: map dbl_map + # 5: map str_map + # } + class Nested3 + include Thrift::Struct + attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + FIELDS = { + 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => Nested2}}, + 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => Nested2}}, + 3 => {:type => Thrift::Types::MAP, :name => 'i64_map', :key => {:type => Thrift::Types::I64}, :value => {:type => Thrift::Types::STRUCT, :class => Nested2}}, + 4 => {:type => Thrift::Types::MAP, :name => 'dbl_map', :key => {:type => Thrift::Types::DOUBLE}, :value => {:type => Thrift::Types::STRUCT, :class => Nested2}}, + 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => Nested2}} + } + end + + # struct Nested4 { + # 1: list a_list + # 2: map i32_map + # 3: map i64_map + # 4: map dbl_map + # 5: map str_map + # } + class Nested4 + include Thrift::Struct + attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + FIELDS = { + 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => Nested3}}, + 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => Nested3}}, + 3 => {:type => Thrift::Types::MAP, :name => 'i64_map', :key => {:type => Thrift::Types::I64}, :value => {:type => Thrift::Types::STRUCT, :class => Nested3}}, + 4 => {:type => Thrift::Types::MAP, :name => 'dbl_map', :key => {:type => Thrift::Types::DOUBLE}, :value => {:type => Thrift::Types::STRUCT, :class => Nested3}}, + 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => Nested3}} + } + end + end +end diff --git a/test/rb/generation/test_struct.rb b/test/rb/generation/test_struct.rb index 3af0df84..b4526ab2 100644 --- a/test/rb/generation/test_struct.rb +++ b/test/rb/generation/test_struct.rb @@ -17,6 +17,9 @@ class TestStructGeneration < Test::Unit::TestCase assert_kind_of(Hash, hello.complex) assert_equal(hello.complex, { 6243 => 632, 2355 => 532, 23 => 532}) + + bool_passer = TestNamespace::BoolPasser.new(:value => false) + assert_equal false, bool_passer.value end def test_goodbyez diff --git a/test/rb/integration/accelerated_buffered_client.rb b/test/rb/integration/accelerated_buffered_client.rb new file mode 100644 index 00000000..a27787ce --- /dev/null +++ b/test/rb/integration/accelerated_buffered_client.rb @@ -0,0 +1,145 @@ +require File.join(File.dirname(__FILE__), '../test_helper') + +require 'thrift' +require 'thrift/protocol/binaryprotocolaccelerated' +require 'ThriftTest' + +class AcceleratedBufferedClientTest < Test::Unit::TestCase + def setup + unless @socket + @socket = Thrift::Socket.new('localhost', 9090) + @protocol = Thrift::BinaryProtocolAccelerated.new(Thrift::BufferedTransport.new(@socket)) + @client = Thrift::Test::ThriftTest::Client.new(@protocol) + @socket.open + end + end + + def test_string + assert_equal(@client.testString('string'), 'string') + end + + def test_byte + val = 8 + assert_equal(@client.testByte(val), val) + assert_equal(@client.testByte(-val), -val) + end + + def test_i32 + val = 32 + assert_equal(@client.testI32(val), val) + assert_equal(@client.testI32(-val), -val) + end + + def test_i64 + val = 64 + assert_equal(@client.testI64(val), val) + assert_equal(@client.testI64(-val), -val) + end + + def test_double + val = 3.14 + assert_equal(@client.testDouble(val), val) + assert_equal(@client.testDouble(-val), -val) + assert_kind_of(Float, @client.testDouble(val)) + end + + def test_map + val = {1 => 1, 2 => 2, 3 => 3} + assert_equal(@client.testMap(val), val) + assert_kind_of(Hash, @client.testMap(val)) + end + + def test_list + val = [1,2,3,4,5] + assert_equal(@client.testList(val), val) + assert_kind_of(Array, @client.testList(val)) + end + + def test_enum + val = Thrift::Test::Numberz::SIX + ret = @client.testEnum(val) + + assert_equal(ret, 6) + assert_kind_of(Fixnum, ret) + end + + def test_typedef + #UserId testTypedef(1: UserId thing), + true + end + + def test_set + val = Set.new([1,2,3]) + assert_equal(@client.testSet(val), val) + assert_kind_of(Set, @client.testSet(val)) + end + + def get_struct + Thrift::Test::Xtruct.new({'string_thing' => 'hi!', 'i32_thing' => 4 }) + end + + def test_struct + ret = @client.testStruct(get_struct) + + assert_nil(ret.byte_thing, nil) + assert_nil(ret.i64_thing, nil) + assert_equal(ret.string_thing, 'hi!') + assert_equal(ret.i32_thing, 4) + assert_kind_of(Thrift::Test::Xtruct, ret) + end + + def test_nest + struct2 = Thrift::Test::Xtruct2.new({'struct_thing' => get_struct, 'i32_thing' => 10}) + + ret = @client.testNest(struct2) + + assert_nil(ret.struct_thing.byte_thing, nil) + assert_nil(ret.struct_thing.i64_thing, nil) + assert_equal(ret.struct_thing.string_thing, 'hi!') + assert_equal(ret.struct_thing.i32_thing, 4) + assert_equal(ret.i32_thing, 10) + + assert_kind_of(Thrift::Test::Xtruct, ret.struct_thing) + assert_kind_of(Thrift::Test::Xtruct2, ret) + end + + def test_insane + insane = Thrift::Test::Insanity.new({ + 'userMap' => { Thrift::Test::Numberz::ONE => 44 }, + 'xtructs' => [get_struct, + Thrift::Test::Xtruct.new({ + 'string_thing' => 'hi again', + 'i32_thing' => 12 + }) + ] + }) + + ret = @client.testInsanity(insane) + + assert_not_nil(ret[44]) + assert_not_nil(ret[44][1]) + + struct = ret[44][1] + + assert_equal(struct.userMap[Thrift::Test::Numberz::ONE], 44) + assert_equal(struct.xtructs[1].string_thing, 'hi again') + assert_equal(struct.xtructs[1].i32_thing, 12) + + assert_kind_of(Hash, struct.userMap) + assert_kind_of(Array, struct.xtructs) + assert_kind_of(Thrift::Test::Insanity, struct) + end + + def test_map_map + ret = @client.testMapMap(4) + assert_kind_of(Hash, ret) + assert_equal(ret, { 4 => { 4 => 4}}) + end + + def test_exception + assert_raise Thrift::Test::Xception do + @client.testException('foo') + end + end +end + diff --git a/test/rb/integration/accelerated_buffered_server.rb b/test/rb/integration/accelerated_buffered_server.rb new file mode 100644 index 00000000..d505e9f0 --- /dev/null +++ b/test/rb/integration/accelerated_buffered_server.rb @@ -0,0 +1,47 @@ +$:.push File.dirname(__FILE__) + '/../gen-rb' +$:.push File.join(File.dirname(__FILE__), '../../../lib/rb/lib') +$:.push File.join(File.dirname(__FILE__), '../../../lib/rb/ext') + +require 'thrift' +require 'thrift/protocol/binaryprotocolaccelerated' +require 'ThriftTest' + +class SimpleHandler + [:testString, :testByte, :testI32, :testI64, :testDouble, + :testStruct, :testMap, :testSet, :testList, :testNest, + :testEnum, :testTypedef].each do |meth| + + define_method(meth) do |thing| + thing + end + + end + + def testInsanity(thing) + num, uid = thing.userMap.find { true } + return {uid => {num => thing}} + end + + def testMapMap(thing) + return {thing => {thing => thing}} + end + + def testEnum(thing) + return thing + end + + def testTypedef(thing) + return thing + end + + def testException(thing) + raise Thrift::Test::Xception, :message => 'error' + end +end + +@handler = SimpleHandler.new +@processor = Thrift::Test::ThriftTest::Processor.new(@handler) +@transport = Thrift::ServerSocket.new(9090) +@server = Thrift::ThreadedServer.new(@processor, @transport, Thrift::BufferedTransportFactory.new, Thrift::BinaryProtocolAcceleratedFactory.new) + +@server.serve diff --git a/test/rb/integration/buffered_client.rb b/test/rb/integration/buffered_client.rb new file mode 100644 index 00000000..3548b18e --- /dev/null +++ b/test/rb/integration/buffered_client.rb @@ -0,0 +1,145 @@ +require File.join(File.dirname(__FILE__), '../test_helper') + +require 'thrift' +require 'thrift/protocol/binaryprotocol' +require 'ThriftTest' + +class BufferedClientTest < Test::Unit::TestCase + def setup + unless @socket + @socket = Thrift::Socket.new('localhost', 9090) + @protocol = Thrift::BinaryProtocol.new(Thrift::BufferedTransport.new(@socket)) + @client = Thrift::Test::ThriftTest::Client.new(@protocol) + @socket.open + end + end + + def test_string + assert_equal(@client.testString('string'), 'string') + end + + def test_byte + val = 8 + assert_equal(@client.testByte(val), val) + assert_equal(@client.testByte(-val), -val) + end + + def test_i32 + val = 32 + assert_equal(@client.testI32(val), val) + assert_equal(@client.testI32(-val), -val) + end + + def test_i64 + val = 64 + assert_equal(@client.testI64(val), val) + assert_equal(@client.testI64(-val), -val) + end + + def test_double + val = 3.14 + assert_equal(@client.testDouble(val), val) + assert_equal(@client.testDouble(-val), -val) + assert_kind_of(Float, @client.testDouble(val)) + end + + def test_map + val = {1 => 1, 2 => 2, 3 => 3} + assert_equal(@client.testMap(val), val) + assert_kind_of(Hash, @client.testMap(val)) + end + + def test_list + val = [1,2,3,4,5] + assert_equal(@client.testList(val), val) + assert_kind_of(Array, @client.testList(val)) + end + + def test_enum + val = Thrift::Test::Numberz::SIX + ret = @client.testEnum(val) + + assert_equal(ret, 6) + assert_kind_of(Fixnum, ret) + end + + def test_typedef + #UserId testTypedef(1: UserId thing), + true + end + + def test_set + val = Set.new([1,2,3]) + assert_equal(@client.testSet(val), val) + assert_kind_of(Set, @client.testSet(val)) + end + + def get_struct + Thrift::Test::Xtruct.new({'string_thing' => 'hi!', 'i32_thing' => 4 }) + end + + def test_struct + ret = @client.testStruct(get_struct) + + assert_nil(ret.byte_thing, nil) + assert_nil(ret.i64_thing, nil) + assert_equal(ret.string_thing, 'hi!') + assert_equal(ret.i32_thing, 4) + assert_kind_of(Thrift::Test::Xtruct, ret) + end + + def test_nest + struct2 = Thrift::Test::Xtruct2.new({'struct_thing' => get_struct, 'i32_thing' => 10}) + + ret = @client.testNest(struct2) + + assert_nil(ret.struct_thing.byte_thing, nil) + assert_nil(ret.struct_thing.i64_thing, nil) + assert_equal(ret.struct_thing.string_thing, 'hi!') + assert_equal(ret.struct_thing.i32_thing, 4) + assert_equal(ret.i32_thing, 10) + + assert_kind_of(Thrift::Test::Xtruct, ret.struct_thing) + assert_kind_of(Thrift::Test::Xtruct2, ret) + end + + def test_insane + insane = Thrift::Test::Insanity.new({ + 'userMap' => { Thrift::Test::Numberz::ONE => 44 }, + 'xtructs' => [get_struct, + Thrift::Test::Xtruct.new({ + 'string_thing' => 'hi again', + 'i32_thing' => 12 + }) + ] + }) + + ret = @client.testInsanity(insane) + + assert_not_nil(ret[44]) + assert_not_nil(ret[44][1]) + + struct = ret[44][1] + + assert_equal(struct.userMap[Thrift::Test::Numberz::ONE], 44) + assert_equal(struct.xtructs[1].string_thing, 'hi again') + assert_equal(struct.xtructs[1].i32_thing, 12) + + assert_kind_of(Hash, struct.userMap) + assert_kind_of(Array, struct.xtructs) + assert_kind_of(Thrift::Test::Insanity, struct) + end + + def test_map_map + ret = @client.testMapMap(4) + assert_kind_of(Hash, ret) + assert_equal(ret, { 4 => { 4 => 4}}) + end + + def test_exception + assert_raise Thrift::Test::Xception do + @client.testException('foo') + end + end +end + diff --git a/test/rb/integration/simple_client.rb b/test/rb/integration/simple_client.rb new file mode 100644 index 00000000..fe2ed821 --- /dev/null +++ b/test/rb/integration/simple_client.rb @@ -0,0 +1,145 @@ +require File.join(File.dirname(__FILE__), '../test_helper') + +require 'thrift' +require 'thrift/protocol/binaryprotocol' +require 'ThriftTest' + +class SimpleClientTest < Test::Unit::TestCase + def setup + unless @socket + @socket = Thrift::Socket.new('localhost', 9090) + @protocol = Thrift::BinaryProtocol.new(@socket) + @client = Thrift::Test::ThriftTest::Client.new(@protocol) + @socket.open + end + end + + def test_string + assert_equal(@client.testString('string'), 'string') + end + + def test_byte + val = 8 + assert_equal(@client.testByte(val), val) + assert_equal(@client.testByte(-val), -val) + end + + def test_i32 + val = 32 + assert_equal(@client.testI32(val), val) + assert_equal(@client.testI32(-val), -val) + end + + def test_i64 + val = 64 + assert_equal(@client.testI64(val), val) + assert_equal(@client.testI64(-val), -val) + end + + def test_double + val = 3.14 + assert_equal(@client.testDouble(val), val) + assert_equal(@client.testDouble(-val), -val) + assert_kind_of(Float, @client.testDouble(val)) + end + + def test_map + val = {1 => 1, 2 => 2, 3 => 3} + assert_equal(@client.testMap(val), val) + assert_kind_of(Hash, @client.testMap(val)) + end + + def test_list + val = [1,2,3,4,5] + assert_equal(@client.testList(val), val) + assert_kind_of(Array, @client.testList(val)) + end + + def test_enum + val = Thrift::Test::Numberz::SIX + ret = @client.testEnum(val) + + assert_equal(ret, 6) + assert_kind_of(Fixnum, ret) + end + + def test_typedef + #UserId testTypedef(1: UserId thing), + true + end + + def test_set + val = Set.new([1,2,3]) + assert_equal(@client.testSet(val), val) + assert_kind_of(Set, @client.testSet(val)) + end + + def get_struct + Thrift::Test::Xtruct.new({'string_thing' => 'hi!', 'i32_thing' => 4 }) + end + + def test_struct + ret = @client.testStruct(get_struct) + + assert_nil(ret.byte_thing, nil) + assert_nil(ret.i64_thing, nil) + assert_equal(ret.string_thing, 'hi!') + assert_equal(ret.i32_thing, 4) + assert_kind_of(Thrift::Test::Xtruct, ret) + end + + def test_nest + struct2 = Thrift::Test::Xtruct2.new({'struct_thing' => get_struct, 'i32_thing' => 10}) + + ret = @client.testNest(struct2) + + assert_nil(ret.struct_thing.byte_thing, nil) + assert_nil(ret.struct_thing.i64_thing, nil) + assert_equal(ret.struct_thing.string_thing, 'hi!') + assert_equal(ret.struct_thing.i32_thing, 4) + assert_equal(ret.i32_thing, 10) + + assert_kind_of(Thrift::Test::Xtruct, ret.struct_thing) + assert_kind_of(Thrift::Test::Xtruct2, ret) + end + + def test_insane + insane = Thrift::Test::Insanity.new({ + 'userMap' => { Thrift::Test::Numberz::ONE => 44 }, + 'xtructs' => [get_struct, + Thrift::Test::Xtruct.new({ + 'string_thing' => 'hi again', + 'i32_thing' => 12 + }) + ] + }) + + ret = @client.testInsanity(insane) + + assert_not_nil(ret[44]) + assert_not_nil(ret[44][1]) + + struct = ret[44][1] + + assert_equal(struct.userMap[Thrift::Test::Numberz::ONE], 44) + assert_equal(struct.xtructs[1].string_thing, 'hi again') + assert_equal(struct.xtructs[1].i32_thing, 12) + + assert_kind_of(Hash, struct.userMap) + assert_kind_of(Array, struct.xtructs) + assert_kind_of(Thrift::Test::Insanity, struct) + end + + def test_map_map + ret = @client.testMapMap(4) + assert_kind_of(Hash, ret) + assert_equal(ret, { 4 => { 4 => 4}}) + end + + def test_exception + assert_raise Thrift::Test::Xception do + @client.testException('foo') + end + end +end + diff --git a/test/rb/integration/simple_server.rb b/test/rb/integration/simple_server.rb new file mode 100644 index 00000000..6b029ba3 --- /dev/null +++ b/test/rb/integration/simple_server.rb @@ -0,0 +1,46 @@ +$:.push File.dirname(__FILE__) + '/../gen-rb' +$:.push File.join(File.dirname(__FILE__), '../../../lib/rb/lib') + +require 'thrift' +require 'thrift/protocol/binaryprotocol' +require 'ThriftTest' + +class SimpleHandler + [:testString, :testByte, :testI32, :testI64, :testDouble, + :testStruct, :testMap, :testSet, :testList, :testNest, + :testEnum, :testTypedef].each do |meth| + + define_method(meth) do |thing| + thing + end + + end + + def testInsanity(thing) + num, uid = thing.userMap.find { true } + return {uid => {num => thing}} + end + + def testMapMap(thing) + return {thing => {thing => thing}} + end + + def testEnum(thing) + return thing + end + + def testTypedef(thing) + return thing + end + + def testException(thing) + raise Thrift::Test::Xception, :message => 'error' + end +end + +@handler = SimpleHandler.new +@processor = Thrift::Test::ThriftTest::Processor.new(@handler) +@transport = Thrift::ServerSocket.new(9090) +@server = Thrift::ThreadedServer.new(@processor, @transport) + +@server.serve diff --git a/test/rb/test_helper.rb b/test/rb/test_helper.rb index 125f52f8..51f72b16 100644 --- a/test/rb/test_helper.rb +++ b/test/rb/test_helper.rb @@ -1,4 +1,16 @@ $:.unshift File.dirname(__FILE__) + '/gen-rb' $:.unshift File.join(File.dirname(__FILE__), '../../lib/rb/lib') +$:.unshift File.join(File.dirname(__FILE__), '../../lib/rb/ext') require 'test/unit' + +module Thrift + module Struct + def ==(other) + return false unless other.is_a? self.class + self.class.const_get(:FIELDS).collect {|fid, data| data[:name] }.all? do |field| + send(field) == other.send(field) + end + end + end +end diff --git a/test/rb/test_suite.rb b/test/rb/test_suite.rb index af686a82..36119633 100644 --- a/test/rb/test_suite.rb +++ b/test/rb/test_suite.rb @@ -1 +1 @@ -Dir["{core,generation,integration}/**/*.rb"].each {|f| require f } \ No newline at end of file +Dir["{core,generation}/**/*.rb"].each {|f| require f } \ No newline at end of file -- 2.17.1