diff --git a/ext/-test-/memory_view/memory_view.c b/ext/-test-/memory_view/memory_view.c index 4dee65dd3b9203..a61652eefc77c9 100644 --- a/ext/-test-/memory_view/memory_view.c +++ b/ext/-test-/memory_view/memory_view.c @@ -114,51 +114,85 @@ memory_view_parse_item_format(VALUE mod, VALUE format) } static VALUE -memory_view_get_memory_view_info(VALUE mod, VALUE obj) +memory_view_release_ensure(VALUE data) { - rb_memory_view_t view; + rb_memory_view_t *view = (rb_memory_view_t *)data; + rb_memory_view_release(view); + return Qnil; +} - if (!rb_memory_view_get(obj, &view, 0)) { - return Qnil; - } +static VALUE +memory_view_get_memory_view_info_body(VALUE args) +{ + rb_memory_view_t *view = (rb_memory_view_t *)args; VALUE hash = rb_hash_new(); - rb_hash_aset(hash, sym_obj, view.obj); - rb_hash_aset(hash, sym_byte_size, SSIZET2NUM(view.byte_size)); - rb_hash_aset(hash, sym_readonly, view.readonly ? Qtrue : Qfalse); - rb_hash_aset(hash, sym_format, view.format ? rb_str_new_cstr(view.format) : Qnil); - rb_hash_aset(hash, sym_item_size, SSIZET2NUM(view.item_size)); - rb_hash_aset(hash, sym_ndim, SSIZET2NUM(view.ndim)); - - if (view.shape) { - VALUE shape = rb_ary_new_capa(view.ndim); + rb_hash_aset(hash, sym_obj, view->obj); + rb_hash_aset(hash, sym_byte_size, SSIZET2NUM(view->byte_size)); + rb_hash_aset(hash, sym_readonly, view->readonly ? Qtrue : Qfalse); + rb_hash_aset(hash, sym_format, view->format ? rb_str_new_cstr(view->format) : Qnil); + rb_hash_aset(hash, sym_item_size, SSIZET2NUM(view->item_size)); + rb_hash_aset(hash, sym_ndim, SSIZET2NUM(view->ndim)); + + if (view->shape) { + VALUE shape = rb_ary_new_capa(view->ndim); + ssize_t i; + for (i = 0; i < view->ndim; i++) { + rb_ary_push(shape, SSIZET2NUM(view->shape[i])); + } rb_hash_aset(hash, sym_shape, shape); } else { rb_hash_aset(hash, sym_shape, Qnil); } - if (view.strides) { - VALUE strides = rb_ary_new_capa(view.ndim); + if (view->strides) { + VALUE strides = rb_ary_new_capa(view->ndim); + ssize_t i; + for (i = 0; i < view->ndim; i++) { + rb_ary_push(strides, SSIZET2NUM(view->strides[i])); + } rb_hash_aset(hash, sym_strides, strides); } else { rb_hash_aset(hash, sym_strides, Qnil); } - if (view.sub_offsets) { - VALUE sub_offsets = rb_ary_new_capa(view.ndim); + if (view->sub_offsets) { + VALUE sub_offsets = rb_ary_new_capa(view->ndim); rb_hash_aset(hash, sym_sub_offsets, sub_offsets); } else { rb_hash_aset(hash, sym_sub_offsets, Qnil); } - rb_memory_view_release(&view); - return hash; } +static VALUE +memory_view_get_memory_view_info(int argc, VALUE *args, VALUE mod) +{ + rb_memory_view_t view; + VALUE obj; + VALUE flags; + enum ruby_memory_view_flags view_flags; + + rb_scan_args(argc, args, "11", &obj, &flags); + if (NIL_P(flags)) { + view_flags = RUBY_MEMORY_VIEW_SIMPLE; + } + else { + view_flags = NUM2UINT(flags); + } + + if (!rb_memory_view_get(obj, &view, view_flags)) { + return Qnil; + } + + return rb_ensure(memory_view_get_memory_view_info_body, (VALUE)&view, + memory_view_release_ensure, (VALUE)&view); +} + static VALUE memory_view_is_row_major_contiguous(VALUE mod, VALUE obj) { @@ -281,6 +315,108 @@ memory_view_extract_item_members(VALUE mod, VALUE str, VALUE format) return item; } +typedef struct { + VALUE location; + rb_memory_view_t view; +} memory_view_get_data_args; + +static VALUE +memory_view_get_data_body(VALUE _args) +{ + memory_view_get_data_args *args = (memory_view_get_data_args *)_args; + long beg, len; + + if (RTEST(rb_range_beg_len(args->location, + &beg, + &len, + args->view.byte_size, + 0))) { + const char *content = ((const char *)(args->view.data)) + beg; + return rb_str_new(content, len); + } + else { + const char *content = + ((const char *)(args->view.data)) + NUM2LONG(args->location); + return rb_str_new(content, 1); + } +} + +static VALUE +memory_view_get_data(VALUE mod, VALUE obj, VALUE location) +{ + memory_view_get_data_args args; + args.location = location; + + if (!rb_memory_view_get(obj, &(args.view), 0)) { + return Qnil; + } + + return rb_ensure(memory_view_get_data_body, (VALUE)&args, + memory_view_release_ensure, (VALUE)&(args.view)); +} + +typedef struct { + VALUE offset; + VALUE data; + rb_memory_view_t view; +} memory_view_set_data_args; + +static VALUE +memory_view_set_data_body(VALUE _args) +{ + memory_view_set_data_args *args = (memory_view_set_data_args *)_args; + + if (FIXNUM_P(args->data)) { + ((char *)(args->view.data))[NUM2LONG(args->offset)] = + NUM2LONG(args->data); + } + else { + StringValue(args->data); + memcpy(((char *)(args->view.data)) + NUM2LONG(args->offset), + RSTRING_PTR(args->data), + RSTRING_LEN(args->data)); + } + + return Qtrue; +} + +static VALUE +memory_view_set_data(VALUE mod, VALUE obj, VALUE offset, VALUE data) +{ + memory_view_set_data_args args; + args.offset = offset; + args.data = data; + + if (!rb_memory_view_get(obj, &(args.view), RUBY_MEMORY_VIEW_WRITABLE)) { + return Qnil; + } + + return rb_ensure(memory_view_set_data_body, (VALUE)&args, + memory_view_release_ensure, (VALUE)&(args.view)); +} + +static VALUE +memory_view_get_body(VALUE data) +{ + return rb_yield_values(0); +} + +static VALUE +memory_view_get(VALUE mod, VALUE obj, VALUE flags) +{ + rb_memory_view_t view; + + if (!rb_memory_view_get(obj, &view, NUM2UINT(flags))) { + rb_raise(rb_eArgError, + "Unable to get memory view: " + "object=%+" PRIsVALUE " flags=%+" PRIsVALUE, + obj, flags); + } + + return rb_ensure(memory_view_get_body, Qnil, + memory_view_release_ensure, (VALUE)&view); +} + static VALUE expstr_initialize(VALUE obj, VALUE s) { @@ -416,16 +552,38 @@ Init_memory_view(void) #ifdef HAVE_RUBY_MEMORY_VIEW_H VALUE mMemoryViewTestUtils = rb_define_module("MemoryViewTestUtils"); + rb_define_const(mMemoryViewTestUtils, "SIMPLE", + UINT2NUM(RUBY_MEMORY_VIEW_SIMPLE)); + rb_define_const(mMemoryViewTestUtils, "WRITABLE", + UINT2NUM(RUBY_MEMORY_VIEW_WRITABLE)); + rb_define_const(mMemoryViewTestUtils, "FORMAT", + UINT2NUM(RUBY_MEMORY_VIEW_FORMAT)); + rb_define_const(mMemoryViewTestUtils, "MULTI_DIMENSIONAL", + UINT2NUM(RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL)); + rb_define_const(mMemoryViewTestUtils, "STRIDES", + UINT2NUM(RUBY_MEMORY_VIEW_STRIDES)); + rb_define_const(mMemoryViewTestUtils, "ROW_MAJOR", + UINT2NUM(RUBY_MEMORY_VIEW_ROW_MAJOR)); + rb_define_const(mMemoryViewTestUtils, "COLUMN_MAJOR", + UINT2NUM(RUBY_MEMORY_VIEW_COLUMN_MAJOR)); + rb_define_const(mMemoryViewTestUtils, "ANY_CONTIGUOUS", + UINT2NUM(RUBY_MEMORY_VIEW_ANY_CONTIGUOUS)); + rb_define_const(mMemoryViewTestUtils, "INDIRECT", + UINT2NUM(RUBY_MEMORY_VIEW_INDIRECT)); + rb_define_module_function(mMemoryViewTestUtils, "available?", memory_view_available_p, 1); rb_define_module_function(mMemoryViewTestUtils, "register", memory_view_register, 1); rb_define_module_function(mMemoryViewTestUtils, "item_size_from_format", memory_view_item_size_from_format, 1); rb_define_module_function(mMemoryViewTestUtils, "parse_item_format", memory_view_parse_item_format, 1); - rb_define_module_function(mMemoryViewTestUtils, "get_memory_view_info", memory_view_get_memory_view_info, 1); + rb_define_module_function(mMemoryViewTestUtils, "get_memory_view_info", memory_view_get_memory_view_info, -1); rb_define_module_function(mMemoryViewTestUtils, "is_row_major_contiguous", memory_view_is_row_major_contiguous, 1); rb_define_module_function(mMemoryViewTestUtils, "is_column_major_contiguous", memory_view_is_column_major_contiguous, 1); rb_define_module_function(mMemoryViewTestUtils, "fill_contiguous_strides", memory_view_fill_contiguous_strides, 4); rb_define_module_function(mMemoryViewTestUtils, "ref_count_while_exporting", memory_view_ref_count_while_exporting, 2); rb_define_module_function(mMemoryViewTestUtils, "extract_item_members", memory_view_extract_item_members, 2); + rb_define_module_function(mMemoryViewTestUtils, "get_data", memory_view_get_data, 2); + rb_define_module_function(mMemoryViewTestUtils, "set_data", memory_view_set_data, 3); + rb_define_module_function(mMemoryViewTestUtils, "get", memory_view_get, 2); VALUE cExportableString = rb_define_class_under(mMemoryViewTestUtils, "ExportableString", rb_cObject); rb_define_method(cExportableString, "initialize", expstr_initialize, 1); diff --git a/file.c b/file.c index d173cfe1858890..0f2cbfc9e64749 100644 --- a/file.c +++ b/file.c @@ -5536,21 +5536,22 @@ static VALUE rb_file_dirname_n(VALUE fname, int n); /* * call-seq: - * File.dirname(file_name, level = 1) -> dir_name + * File.dirname(path, count = 1) -> string * - * Returns all components of the filename given in file_name - * except the last one (after first stripping trailing separators). - * The filename can be formed using both File::SEPARATOR and - * File::ALT_SEPARATOR as the separator when File::ALT_SEPARATOR is - * not nil. + * Returns a string path containing all but the last +count+ components + * of the given +path+: * - * File.dirname("/home/gumby/work/ruby.rb") #=> "/home/gumby/work" + * File.dirname('/usr/lib/linux') # => "/usr/lib" + * File.dirname('/usr') # => "/" + * File.dirname('/') # => "/" + * File.dirname('lib/') # => "." + * File.dirname('nosuch') # => "." + * File.dirname('/usr/lib/linux', 2) # => "/usr" + * File.dirname('/usr/lib/linux', 20) # => "/" + * File.dirname('/usr/lib/linux', 0) # => "/usr/lib/linux" * - * If +level+ is given, removes the last +level+ components, not only - * one. + * Components are delimited by File::SEPARATOR and, if non-+nil+, File::ALT_SEPARATOR. * - * File.dirname("/home/gumby/work/ruby.rb", 2) #=> "/home/gumby" - * File.dirname("/home/gumby/work/ruby.rb", 4) #=> "/" */ static VALUE diff --git a/hash.c b/hash.c index eede4d192108be..471d46049bc1eb 100644 --- a/hash.c +++ b/hash.c @@ -1497,7 +1497,7 @@ hash_slot_size(size_t capa, bool frozen) } static VALUE -hash_alloc_capa(VALUE klass, VALUE flags, VALUE ifnone, size_t size, bool frozen) +hash_alloc(VALUE klass, VALUE flags, VALUE ifnone, size_t size, bool frozen) { VALUE hash = rb_newobj_of(klass, T_HASH | flags, hash_slot_size(size, frozen)); rb_hash_set_ifnone(hash, ifnone); @@ -1524,19 +1524,19 @@ hash_init_capa(VALUE hash, size_t size) static VALUE hash_hidden_new(size_t size) { - return hash_init_capa(hash_alloc_capa(0, 0, Qnil, size, false), size); + return hash_init_capa(hash_alloc(0, 0, Qnil, size, false), size); } -VALUE -rb_hash_alloc_copy(VALUE klass, VALUE src) +static VALUE +hash_alloc_capa(VALUE klass, size_t size) { - return hash_alloc_capa(klass, 0, Qnil, RHASH_SIZE(src), false); + return hash_alloc(klass, 0, Qnil, size, false); } -static VALUE -hash_alloc(VALUE klass) +VALUE +rb_hash_alloc_copy(VALUE klass, VALUE src) { - return hash_alloc_capa(klass, 0, Qnil, 0, false); + return hash_alloc_capa(klass, RHASH_SIZE(src)); } #if USE_ZJIT @@ -1553,7 +1553,7 @@ empty_hash_alloc(VALUE klass) { RUBY_DTRACE_CREATE_HOOK(HASH, 0); - return hash_alloc(klass); + return hash_alloc_capa(klass, 0); } static VALUE @@ -1568,7 +1568,7 @@ copy_compare_by_id(VALUE hash, VALUE basis) static VALUE hash_new_capa(VALUE klass, size_t capa) { - return hash_init_capa(hash_alloc_capa(klass, 0, Qnil, capa, false), capa); + return hash_init_capa(hash_alloc_capa(klass, capa), capa); } VALUE @@ -1589,7 +1589,7 @@ rb_hash_new(void) VALUE rb_hash_alloc_fixed_size(VALUE klass, st_index_t size) { - return hash_init_capa(hash_alloc_capa(klass, 0, Qnil, size, true), size); + return hash_init_capa(hash_alloc(klass, 0, Qnil, size, true), size); } static VALUE @@ -1635,7 +1635,7 @@ hash_copy(VALUE ret, VALUE hash) static VALUE hash_dup_with_compare_by_id(VALUE hash) { - VALUE dup = hash_alloc_capa(rb_cHash, 0, Qnil, RHASH_SIZE(hash), false); + VALUE dup = hash_alloc_capa(rb_cHash, RHASH_SIZE(hash)); if (RHASH_ST_TABLE_P(hash)) { RHASH_SET_ST_FLAG(dup); } @@ -1646,14 +1646,14 @@ hash_dup_with_compare_by_id(VALUE hash) static VALUE hash_dup(VALUE hash, VALUE klass, VALUE flags) { - VALUE dup = hash_alloc_capa(klass, flags, RHASH_IFNONE(hash), RHASH_SIZE(hash), false); + VALUE dup = hash_alloc(klass, flags, RHASH_IFNONE(hash), RHASH_SIZE(hash), false); return hash_copy(dup, hash); } static VALUE hash_dup_capa(VALUE hash, size_t capa) { - VALUE ret = hash_alloc_capa(rb_cHash, 0, Qnil, capa, false); + VALUE ret = hash_alloc_capa(rb_cHash, capa); if (capa > RHASH_AR_TABLE_MAX_SIZE) { RHASH_SET_ST_FLAG(ret); } @@ -1933,7 +1933,7 @@ rb_hash_s_create(int argc, VALUE *argv, VALUE klass) tmp = rb_hash_to_a(tmp); } else { - hash = hash_alloc_capa(klass, 0, Qnil, RHASH_SIZE(tmp), false); + hash = hash_alloc_capa(klass, RHASH_SIZE(tmp)); return hash_copy(hash, tmp); } } @@ -2123,7 +2123,7 @@ rb_hash_rehash(VALUE hash) } rb_hash_modify_check(hash); if (RHASH_AR_TABLE_P(hash)) { - tmp = hash_alloc(0); + tmp = hash_alloc_capa(0, RHASH_SIZE(hash)); rb_hash_foreach(hash, rb_hash_rehash_i, (VALUE)tmp); hash_ar_free_and_clear_table(hash); @@ -2131,7 +2131,7 @@ rb_hash_rehash(VALUE hash) } else if (RHASH_ST_TABLE_P(hash)) { st_table *old_tab = RHASH_ST_TABLE(hash); - tmp = hash_alloc(0); + tmp = hash_alloc_capa(0, 0); hash_st_table_init(tmp, old_tab->type, old_tab->num_entries); tbl = RHASH_ST_TABLE(tmp); @@ -4813,7 +4813,7 @@ rb_hash_compare_by_id(VALUE hash) else { // Slow path: Need to rehash the members of `self` into a new // `tmp` table using the new `identhash` compare/hash functions. - tmp = hash_alloc(0); + tmp = hash_alloc_capa(0, 0); hash_st_table_init(tmp, &identhash, RHASH_SIZE(hash)); identtable = RHASH_ST_TABLE(tmp); diff --git a/inits.c b/inits.c index 1be4916e87b357..e4323cc4a847b6 100644 --- a/inits.c +++ b/inits.c @@ -47,6 +47,7 @@ rb_call_inits(void) CALL(marshal); CALL(Range); CALL(IO); + CALL(MemoryView); /* Must precede IO_Buffer */ CALL(IO_Buffer) CALL(Dir); CALL(Time); @@ -70,7 +71,6 @@ rb_call_inits(void) CALL(process); CALL(Rational); CALL(Complex); - CALL(MemoryView); CALL(pathname); CALL(version); CALL(vm_trace); diff --git a/io.c b/io.c index 5812aa4497d536..f4dcc9883d2f6b 100644 --- a/io.c +++ b/io.c @@ -8966,7 +8966,7 @@ io_puts_ary(VALUE ary, VALUE out, int recur) * If called without arguments, writes a newline. * See {Line IO}[rdoc-ref:IO@Line+IO]. * - * Note that each added newline is the character "\n", + * Note that each added newline is the character "\n", * not the output record separator ($\\). * * Treatment for each object: diff --git a/io_buffer.c b/io_buffer.c index 5a5128aa1dd949..8099c23a37e6fe 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -8,6 +8,7 @@ #include "ruby/io/buffer.h" #include "ruby/fiber/scheduler.h" +#include "ruby/memory_view.h" // For `rb_nogvl`. #include "ruby/thread.h" @@ -4252,6 +4253,81 @@ io_buffer_bit_count(int argc, VALUE *argv, VALUE self) return SIZET2NUM(count); } +static bool +io_buffer_memory_view_get(VALUE self, rb_memory_view_t *view, int flags) +{ + struct rb_io_buffer *buffer = get_io_buffer(self); + + if (buffer->base == NULL || !io_buffer_validate(buffer)) { + return false; + } + + bool readonly = true; + if (flags & RUBY_MEMORY_VIEW_WRITABLE) { + if (io_buffer_readonly_p(buffer)) { + return false; + } else { + readonly = false; + } + } + rb_memory_view_init_as_byte_array(view, self, buffer->base, buffer->size, readonly); + if (flags & RUBY_MEMORY_VIEW_FORMAT) { + view->format = "C"; + } + bool request_multi_dimensional = flags & RUBY_MEMORY_VIEW_MULTI_DIMENSIONAL; + bool request_strides = + (flags & RUBY_MEMORY_VIEW_STRIDES) == RUBY_MEMORY_VIEW_STRIDES; + if (request_multi_dimensional || request_strides) { + size_t n_metadata = 0; + if (request_multi_dimensional) + n_metadata++; + if (request_strides) + n_metadata++; + ssize_t *metadata_buffer = ALLOC_N(ssize_t, n_metadata); + size_t i = 0; + if (request_multi_dimensional) { + ssize_t *shape = &metadata_buffer[i]; + shape[0] = buffer->size; + view->shape = shape; + i++; + } + if (request_strides) { + ssize_t *strides = &metadata_buffer[i]; + strides[0] = 1; + view->strides = strides; + i++; + } + view->private_data = metadata_buffer; + } + io_buffer_lock(buffer); + + return true; +} + +static bool +io_buffer_memory_view_release(VALUE self, rb_memory_view_t *view) +{ + rb_io_buffer_unlock(self); + if (view->private_data) { + xfree(view->private_data); + } + return true; +} + +static bool +io_buffer_memory_view_available_p(VALUE self) +{ + struct rb_io_buffer *buffer = get_io_buffer(self); + + return buffer->base != NULL && io_buffer_validate(buffer); +} + +static const rb_memory_view_entry_t io_buffer_memory_view_entry = { + .get_func = io_buffer_memory_view_get, + .release_func = io_buffer_memory_view_release, + .available_p_func = io_buffer_memory_view_available_p, +}; + /* * Document-class: IO::Buffer * @@ -4276,6 +4352,16 @@ io_buffer_bit_count(int argc, VALUE *argv, VALUE self) * like Fiber::Scheduler#io_read and Fiber::Scheduler#io_write and parsing binary * protocols. * + * == MemoryView Support + * + * IO::Buffer supports the C-level MemoryView protocol, so C + * extensions can use +rb_memory_view_get()+ to access the buffer's + * memory directly (zero-copy) as a 1-dimensional contiguous array of + * bytes. The memory view is writable if the buffer is not + * #readonly? and +RUBY_MEMORY_VIEW_WRITABLE+ is specified. + * + * While a MemoryView is exported, the buffer is locked. + * * == Examples of Usage * * Empty buffer: @@ -4516,4 +4602,7 @@ Init_IO_Buffer(void) rb_define_method(rb_cIOBuffer, "pread", io_buffer_pread, -1); rb_define_method(rb_cIOBuffer, "write", io_buffer_write, -1); rb_define_method(rb_cIOBuffer, "pwrite", io_buffer_pwrite, -1); + + // MemoryView: + rb_memory_view_register(rb_cIOBuffer, &io_buffer_memory_view_entry); } diff --git a/lib/resolv.rb b/lib/resolv.rb index 6b58f92813b435..03e5e9a75dfa72 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -562,7 +562,21 @@ def fetch_resource(name, typeclass) next if !sender senders[[candidate, requester, nameserver, port]] = sender end - reply, reply_name = requester.request(sender, tout) + begin + reply, reply_name = requester.request(sender, tout) + rescue ResolvTimeout + # Giving up part way through a frame loses stream sync, and a peer + # seen going away leaves the socket dead. Either way the requester + # says so, and the next attempt has to open a fresh connection. A + # timeout with the stream still on a frame boundary keeps it; a + # peer that leaves while nothing is being read goes unnoticed here + # and only shows up when the next request is written. + unless requester.reusable? + requesters.delete([nameserver, port]) + requester.close + end + raise + end case reply.rcode when RCode::NoError if reply.tc == 1 and not Requester::TCP === requester @@ -698,6 +712,12 @@ def initialize @socks = nil end + # Whether another request may be sent over the same transport. Only a + # stream transport can end up in a state that rules this out. + def reusable? + true + end + def request(sender, tout) start = Process.clock_gettime(Process::CLOCK_MONOTONIC) timelimit = start + tout @@ -724,7 +744,7 @@ def request(sender, tout) raise ResolvTimeout end begin - reply, from = recv_reply(select_result[0]) + reply, from = recv_reply(select_result[0], timelimit) rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD Errno::ECONNRESET, # Windows EOFError @@ -801,7 +821,7 @@ def lazy_initialize self end - def recv_reply(readable_socks) + def recv_reply(readable_socks, timelimit = nil) lazy_initialize reply, from = readable_socks[0].recvfrom(UDPSize) return reply, [from[3],from[1]] @@ -870,7 +890,7 @@ def lazy_initialize self end - def recv_reply(readable_socks) + def recv_reply(readable_socks, timelimit = nil) lazy_initialize reply = readable_socks[0].recv(UDPSize) return reply, nil @@ -933,15 +953,28 @@ def initialize(host, port=Port) sock = TCPSocket.new(@host, @port) @socks = [sock] @senders = {} + @reusable = true + end + + def reusable? + @reusable end - def recv_reply(readable_socks) - len_data = readable_socks[0].read(2) + def recv_reply(readable_socks, timelimit = nil) + sock = readable_socks[0] + len_data = read_exactly(sock, 2, timelimit) raise EOFError if len_data.nil? || len_data.bytesize != 2 len = len_data.unpack('n')[0] - reply = @socks[0].read(len) + reply = read_exactly(sock, len, timelimit) raise EOFError if reply.nil? || reply.bytesize != len + @reusable = true return reply, nil + rescue EOFError, SystemCallError + # Whatever the kernel reported, this socket cannot be trusted for + # another frame. In practice that is the peer closing or resetting; + # the rest is rare enough that erring towards reconnecting is right. + @reusable = false + raise end def sender(msg, data, host=@host, port=@port) @@ -963,11 +996,40 @@ def send end def close + @reusable = false super @senders.each_key {|from,id| DNS.free_request_id(@host, @port, id) } end + + private + + # Read +len+ bytes, giving up with ResolvTimeout once +timelimit+ (a + # CLOCK_MONOTONIC value) has passed. A shorter result means the peer + # closed the connection, which the caller turns into an EOFError. + # Consuming any byte marks the requester unusable until the whole frame + # has been read, since giving up in between loses frame sync. Without a + # +timelimit+ the read blocks instead and keeps no such mark; that is + # only for a caller still using the one argument form of #recv_reply. + def read_exactly(sock, len, timelimit) + return sock.read(len) unless timelimit + buf = String.new + while buf.bytesize < len + case chunk = sock.read_nonblock(len - buf.bytesize, exception: false) + when :wait_readable + remaining = timelimit - Process.clock_gettime(Process::CLOCK_MONOTONIC) + raise ResolvTimeout if remaining <= 0 + sock.wait_readable(remaining) or raise ResolvTimeout + when nil + break + else + @reusable = false + buf << chunk + end + end + buf + end end ## diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 1b90b4f63ae706..10996374218c50 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -7,9 +7,6 @@ # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command - HISTORY_HEADER = %r{^##\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$} - VERSION_MATCHER = %r{^##\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$} - ENV_PATHS = %w[/usr/bin/env /bin/env].freeze def initialize @@ -23,7 +20,7 @@ def initialize add_option "--previous-version=VERSION", "Previous version of RubyGems", - "Used for changelog processing" do |version, options| + "Used for the release notes link" do |version, options| options[:previous_version] = version end @@ -183,12 +180,6 @@ def execute say end - if options[:previous_version].empty? - options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, "0") - end - - options[:previous_version] = Gem::Version.new(options[:previous_version]) - show_release_notes say @@ -549,34 +540,16 @@ def remove_old_man_files(old_man_dir) end def show_release_notes - release_notes = File.join Dir.pwd, "CHANGELOG.md" - - release_notes = - if File.exist? release_notes - history = File.read release_notes - - history.force_encoding Encoding::UTF_8 - - text = history.split(HISTORY_HEADER) - text.shift # correct an off-by-one generated by split - version_lines = history.scan(HISTORY_HEADER) - versions = history.scan(VERSION_MATCHER).flatten.map do |x| - Gem::Version.new(x) - end + ref = Gem::VERSION.include?(".dev") ? "master" : "v#{Gem::VERSION}" + link = "https://github.com/ruby/rubygems/blob/#{ref}/CHANGELOG.md" - history_string = "" - - until versions.length == 0 || - versions.shift <= options[:previous_version] do - history_string += version_lines.shift + text.shift - end - - history_string - else - "Oh-no! Unable to find release notes!" - end - - say release_notes + previous = options[:previous_version].to_s.strip + if previous.empty? || !Gem::Version.correct?(previous) || + Gem::Version.new(previous) >= Gem::Version.new(Gem::VERSION) + say "See #{link} for the changes." + else + say "See #{link} for the changes since #{previous}." + end end def uninstall_old_gemcutter diff --git a/lib/rubygems/update_suggestion.rb b/lib/rubygems/update_suggestion.rb index 6f3ec5f4936747..563f569c2475db 100644 --- a/lib/rubygems/update_suggestion.rb +++ b/lib/rubygems/update_suggestion.rb @@ -10,10 +10,14 @@ module Gem::UpdateSuggestion # Message to promote available RubyGems update with related gem update command. def update_suggestion + current = Gem.rubygems_version + latest = Gem.latest_rubygems_version + <<-MESSAGE -A new release of RubyGems is available: #{Gem.rubygems_version} → #{Gem.latest_rubygems_version}! -Run `gem update --system #{Gem.latest_rubygems_version}` to update your installation. +A new release of RubyGems is available: #{current} → #{latest}! +See https://github.com/ruby/rubygems/blob/v#{latest}/CHANGELOG.md for the changes since #{current}. +Run `gem update --system #{latest}` to update your installation. MESSAGE end diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 1471350488409d..6b8108ce0871cc 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2045,6 +2045,8 @@ def basename(...) self.class.new(File.basename(@path, ...)) end # Pathname('nosuch').basename # => # # ``` # + # Components are delimited by File::SEPARATOR and, if non-+nil+, File::ALT_SEPARATOR. + def dirname() self.class.new(File.dirname(@path)) end # :markup: markdown diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index 0b81118c8c4a48..b4ef92b63845f9 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -843,6 +843,8 @@ def test_tcp_connection_closed_before_length assert_raise(Resolv::ResolvTimeout) do requester.request(sender, 2) end + # The peer went away, so this connection cannot carry another request. + assert_equal(false, requester.reusable?) ensure requester.close end @@ -943,4 +945,314 @@ def test_tcp_connection_closed_with_partial_message_body client_thread.join end end + + def accept_within_timeout(t) + Timeout.timeout(EnvUtil.apply_timeout_scale(10)) { t.accept } + end + + # Reads one length prefixed DNS message from +sock+. + def read_framed_query(sock) + len_data = sock.read(2) + flunk('the client closed the connection before sending a query') unless len_data&.bytesize == 2 + Resolv::DNS::Message.decode(sock.read(len_data.unpack('n')[0])) + end + + # Builds the encoded reply answering +query+ with a single A record. + def reply_for_query(query, address) + reply = Resolv::DNS::Message.new(query.id) + reply.qr = 1 + reply.rd = query.rd + reply.ra = 1 + query.each_question do |name, typeclass| + reply.add_question(name, typeclass) + reply.add_answer(name, 3600, Resolv::DNS::Resource::IN::A.new(address)) + end + reply.encode + end + + def framed(encoded) + [encoded.bytesize].pack('n') << encoded + end + + # Runs a TCP server which replies with +reply+ and then keeps the connection + # open until the client side is done, so that only the timeout can end the + # request. + def with_tcp_server_keeping_connection_open(reply) + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + done = Thread::Queue.new + + server_thread = Thread.new do + ct = accept_within_timeout(t) + begin + ct.recv(512) + ct.write(reply) + done.pop + ensure + ct.close + end + end + + client_thread = Thread.new do + begin + yield server_address, server_port + ensure + done.push(true) + end + end + + assert_join_threads([client_thread, server_thread]) + end + end + + def request_over_tcp(server_address, server_port, tout) + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + Timeout.timeout(EnvUtil.apply_timeout_scale(10)) do + requester.request(sender, EnvUtil.apply_timeout_scale(tout)) + end + ensure + requester.close + end + end + + def test_tcp_partial_length_prefix_kept_open + with_tcp_server_keeping_connection_open("\x00") do |server_address, server_port| + assert_raise(Resolv::ResolvTimeout) do + request_over_tcp(server_address, server_port, 0.5) + end + end + end + + def test_tcp_partial_message_body_kept_open + reply = [10].pack('n') << '12345' # 5 bytes of a 10 byte message + with_tcp_server_keeping_connection_open(reply) do |server_address, server_port| + assert_raise(Resolv::ResolvTimeout) do + request_over_tcp(server_address, server_port, 0.5) + end + end + end + + def test_tcp_complete_reply_kept_open + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + done = Thread::Queue.new + + server_thread = Thread.new do + ct = accept_within_timeout(t) + begin + ct.write(framed(reply_for_query(read_framed_query(ct), '192.0.2.1'))) + done.pop + ensure + ct.close + end + end + + client_thread = Thread.new do + begin + reply, = request_over_tcp(server_address, server_port, 2) + assert_equal(1, reply.answer.length) + assert_equal('192.0.2.1', reply.answer[0][2].address.to_s) + ensure + done.push(true) + end + end + + assert_join_threads([client_thread, server_thread]) + end + end + + def test_tcp_reply_arriving_in_two_chunks + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + done = Thread::Queue.new + + server_thread = Thread.new do + ct = accept_within_timeout(t) + begin + reply = framed(reply_for_query(read_framed_query(ct), '192.0.2.1')) + ct.write(reply.byteslice(0, 6)) + sleep EnvUtil.apply_timeout_scale(0.2) + ct.write(reply.byteslice(6..-1)) + done.pop + ensure + ct.close + end + end + + client_thread = Thread.new do + begin + reply, = request_over_tcp(server_address, server_port, 2) + assert_equal(1, reply.answer.length) + assert_equal('192.0.2.1', reply.answer[0][2].address.to_s) + ensure + done.push(true) + end + end + + assert_join_threads([client_thread, server_thread]) + end + end + + def test_truncated_tcp_fallback_with_partial_message_body_kept_open + with_udp_and_tcp('127.0.0.1', 0) do |u, t| + _, server_port, _, server_address = u.addr + done = Thread::Queue.new + + client_thread = Thread.new do + begin + dns = Resolv::DNS.new(nameserver_port: [[server_address, server_port]], + raise_timeout_errors: true) + begin + dns.timeouts = EnvUtil.apply_timeout_scale(0.5) + assert_raise(Resolv::ResolvError) do + Timeout.timeout(EnvUtil.apply_timeout_scale(10)) do + dns.getresources('foo.example.org', Resolv::DNS::Resource::IN::A) + end + end + ensure + dns.close + end + ensure + done.push(true) + end + end + + udp_server_thread = Thread.new do + msg, (_, client_port, _, client_address) = + Timeout.timeout(EnvUtil.apply_timeout_scale(10)) { u.recvfrom(4096) } + id, word2, = msg.unpack('nnnnnn') + opcode = (word2 & 0x7800) >> 11 + rd = (word2 & 0x0100) >> 8 + qr = 1 + tc = 1 # ask the client to retry over TCP + ra = 1 + word2 = (qr << 15) | (opcode << 11) | (tc << 9) | (rd << 8) | (ra << 7) + u.send([id, word2, 0, 0, 0, 0].pack('nnnnnn'), 0, client_address, client_port) + end + + tcp_server_thread = Thread.new do + ct = accept_within_timeout(t) + begin + ct.recv(512) + ct.write([10].pack('n') << '12345') # 5 bytes of a 10 byte message + done.pop + ensure + ct.close + end + end + + assert_join_threads([client_thread, udp_server_thread, tcp_server_thread]) + end + end + + # A frame read that gives up part way through a frame leaves the stream + # between frame boundaries, so the retry has to start from a new connection. + def test_truncated_tcp_fallback_retries_on_a_new_connection + with_udp_and_tcp('127.0.0.1', 0) do |u, t| + _, server_port, _, server_address = u.addr + + client_thread = Thread.new do + Resolv::DNS.open(nameserver_port: [[server_address, server_port]], + raise_timeout_errors: true) do |dns| + dns.timeouts = [EnvUtil.apply_timeout_scale(1), + EnvUtil.apply_timeout_scale(3)] + Timeout.timeout(EnvUtil.apply_timeout_scale(20)) do + dns.getresources('foo.example.org', Resolv::DNS::Resource::IN::A) + end + end + end + + udp_server_thread = Thread.new do + msg, (_, client_port, _, client_address) = + Timeout.timeout(EnvUtil.apply_timeout_scale(10)) { u.recvfrom(4096) } + id, word2, = msg.unpack('nnnnnn') + opcode = (word2 & 0x7800) >> 11 + rd = (word2 & 0x0100) >> 8 + qr = 1 + tc = 1 # ask the client to retry over TCP + ra = 1 + word2 = (qr << 15) | (opcode << 11) | (tc << 9) | (rd << 8) | (ra << 7) + u.send([id, word2, 0, 0, 0, 0].pack('nnnnnn'), 0, client_address, client_port) + end + + tcp_server_thread = Thread.new do + partial = accept_within_timeout(t) + begin + read_framed_query(partial) + partial.write([45].pack('n') << 'abcdef') # 6 bytes of a 45 byte message + complete = accept_within_timeout(t) + begin + complete.write(framed(reply_for_query(read_framed_query(complete), '192.0.2.1'))) + ensure + complete.close + end + ensure + partial.close + end + end + + result, = assert_join_threads([client_thread, udp_server_thread, tcp_server_thread]) + assert_equal(['192.0.2.1'], result.map {|rr| rr.address.to_s }) + end + end + + # A timeout with no bytes read leaves the stream on a frame boundary, so the + # retry keeps the connection. The reply below only ever reaches the client if + # the second attempt reuses the socket the first one opened. + def test_truncated_tcp_fallback_keeps_the_connection_when_nothing_arrived + with_udp_and_tcp('127.0.0.1', 0) do |u, t| + _, server_port, _, server_address = u.addr + done = Thread::Queue.new + + client_thread = Thread.new do + begin + Resolv::DNS.open(nameserver_port: [[server_address, server_port]], + raise_timeout_errors: true) do |dns| + dns.timeouts = [EnvUtil.apply_timeout_scale(0.5), + EnvUtil.apply_timeout_scale(5)] + Timeout.timeout(EnvUtil.apply_timeout_scale(20)) do + dns.getresources('foo.example.org', Resolv::DNS::Resource::IN::A) + end + end + ensure + done.push(true) + end + end + + udp_server_thread = Thread.new do + msg, (_, client_port, _, client_address) = + Timeout.timeout(EnvUtil.apply_timeout_scale(10)) { u.recvfrom(4096) } + id, word2, = msg.unpack('nnnnnn') + opcode = (word2 & 0x7800) >> 11 + rd = (word2 & 0x0100) >> 8 + qr = 1 + tc = 1 # ask the client to retry over TCP + ra = 1 + word2 = (qr << 15) | (opcode << 11) | (tc << 9) | (rd << 8) | (ra << 7) + u.send([id, word2, 0, 0, 0, 0].pack('nnnnnn'), 0, client_address, client_port) + end + + tcp_server_thread = Thread.new do + ct = accept_within_timeout(t) + begin + query = read_framed_query(ct) + # Stay silent past the first interval, then answer on this connection. + # It has to stay open until the client is done: the retry resends the + # query, and closing with that still unread would discard the reply. + sleep EnvUtil.apply_timeout_scale(1) + ct.write(framed(reply_for_query(query, '192.0.2.1'))) + done.pop + ensure + ct.close + end + end + + result, = assert_join_threads([client_thread, udp_server_thread, tcp_server_thread]) + assert_equal(['192.0.2.1'], result.map {|rr| rr.address.to_s }) + end + end end diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 500ca5b7eb0b56..6e0c858fff52a6 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -4,6 +4,7 @@ require 'rbconfig/sizeof' require 'io/nonblock' require '-test-/io_buffer' +require "-test-/memory_view" class TestIOBuffer < Test::Unit::TestCase experimental = Warning[:experimental] @@ -1703,4 +1704,249 @@ def test_hexdump_width_zero buffer.hexdump(0, 1, 0) end end + + def test_memory_view_null + buffer = IO::Buffer.new(0) + assert_false(MemoryViewTestUtils.available?(buffer)) + end + + def test_memory_view_available + buffer = IO::Buffer.new(8) + assert_true(MemoryViewTestUtils.available?(buffer)) + end + + def test_memory_view_get_simple + data = +"\x00\x01\x02\x03" + IO::Buffer.for(data) do |buffer| + info = MemoryViewTestUtils.get_memory_view_info(buffer) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: nil, + strides: nil, + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_writable + data = +"\x00\x01\x02\x03" + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::WRITABLE + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: false, + format: nil, + item_size: 1, + ndim: 1, + shape: nil, + strides: nil, + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_format + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::FORMAT + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: "C", + item_size: 1, + ndim: 1, + shape: nil, + strides: nil, + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_multi_dimensional + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::MULTI_DIMENSIONAL + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: [data.bytesize], + strides: nil, + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_strides + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::STRIDES + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: [data.bytesize], + strides: [1], + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_row_major + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::ROW_MAJOR + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: [data.bytesize], + strides: [1], + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_column_major + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::COLUMN_MAJOR + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: [data.bytesize], + strides: [1], + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_any_contiguous + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::ANY_CONTIGUOUS + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: [data.bytesize], + strides: [1], + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_get_indirect + data = "\x00\x01\x02\x03".freeze + IO::Buffer.for(data) do |buffer| + flags = MemoryViewTestUtils::INDIRECT + info = MemoryViewTestUtils.get_memory_view_info(buffer, flags) + assert_equal({ + obj: buffer, + byte_size: data.bytesize, + readonly: true, + format: nil, + item_size: 1, + ndim: 1, + shape: [data.bytesize], + strides: [1], + sub_offsets: nil, + }, + info) + end + end + + def test_memory_view_readonly + data = "\x00\x01\x02\x03".freeze + buffer = IO::Buffer.for(data) + # rb_memory_view_get(RUBY_MEMORY_VIEW_WRITABLE) is failed with + # readonly IO::Buffer. + assert_nil(MemoryViewTestUtils.set_data(buffer, 1, 0x11)) + assert_equal(data, MemoryViewTestUtils.get_data(buffer, 0..(data.bytesize))) + end + + def test_memory_view_writable + IO::Buffer.for(+"\x00\x01\x02\x03") do |buffer| + assert_true(MemoryViewTestUtils.set_data(buffer, 1, 0x11)) + assert_equal(0x11, buffer.get_value(:U8, 1)) + end + end + + def test_memory_view_locked + IO::Buffer.for("\x00\x01\x02\x03".freeze) do |buffer| + flags = MemoryViewTestUtils::SIMPLE + MemoryViewTestUtils.get(buffer, flags) do + assert_predicate buffer, :locked? + end + refute_predicate buffer, :locked? + end + end + + def test_memory_view_nested_locked + IO::Buffer.for("\x00\x01\x02\x03".freeze) do |buffer| + flags = MemoryViewTestUtils::SIMPLE + MemoryViewTestUtils.get(buffer, flags) do + MemoryViewTestUtils.get(buffer, flags) do + assert_predicate buffer, :locked? + end + assert_predicate buffer, :locked? + end + refute_predicate buffer, :locked? + end + end + + def test_memory_view_sliced_nested_locked + IO::Buffer.for("\x00\x01\x02\x03".freeze) do |buffer| + sliced = buffer.slice(1, 2) + flags = MemoryViewTestUtils::SIMPLE + MemoryViewTestUtils.get(sliced, flags) do + MemoryViewTestUtils.get(sliced, flags) do + assert_predicate sliced, :locked? + assert_predicate buffer, :locked? + end + assert_predicate sliced, :locked? + assert_predicate buffer, :locked? + end + refute_predicate sliced, :locked? + refute_predicate buffer, :locked? + end + end end diff --git a/test/rubygems/test_gem_commands_setup_command.rb b/test/rubygems/test_gem_commands_setup_command.rb index ba410d64e635cd..484a802fe536e4 100644 --- a/test/rubygems/test_gem_commands_setup_command.rb +++ b/test/rubygems/test_gem_commands_setup_command.rb @@ -407,56 +407,129 @@ def test_remove_old_man_files end def test_show_release_notes - @default_external = @ui.outs.external_encoding - @ui.outs.set_encoding Encoding::US_ASCII + @cmd.options[:previous_version] = "2.0.2" - @cmd.options[:previous_version] = Gem::Version.new "2.0.2" + with_gem_version "4.0.19" do + use_ui @ui do + @cmd.show_release_notes + end + end - File.open "CHANGELOG.md", "w" do |io| - io.puts <<-HISTORY_TXT -# Changelog + expected = "See https://github.com/ruby/rubygems/blob/v4.0.19/CHANGELOG.md for the changes since 2.0.2.\n" -## #{Gem::VERSION} / 2013-03-26 + assert_equal expected, @ui.output + end -### Bug fixes: - * Fixed release note display for LANG=C when installing rubygems - * π is tasty + def test_show_release_notes_dev_version + @cmd.options[:previous_version] = "2.0.2" -## 2.0.2 / 2013-03-06 + with_gem_version "4.1.0.dev" do + use_ui @ui do + @cmd.show_release_notes + end + end -### Bug fixes: - * Other bugs fixed + expected = "See https://github.com/ruby/rubygems/blob/master/CHANGELOG.md for the changes since 2.0.2.\n" -## 2.0.1 / 2013-03-05 + assert_equal expected, @ui.output + end -### Bug fixes: - * Yet more bugs fixed - HISTORY_TXT + def test_show_release_notes_released_prerelease + @cmd.options[:previous_version] = "2.0.2" + + with_gem_version "4.1.0.beta1" do + use_ui @ui do + @cmd.show_release_notes + end end - use_ui @ui do - @cmd.show_release_notes + expected = "See https://github.com/ruby/rubygems/blob/v4.1.0.beta1/CHANGELOG.md for the changes since 2.0.2.\n" + + assert_equal expected, @ui.output + end + + def test_show_release_notes_downgrade + @cmd.handle_options ["--previous-version", "4.0.19"] + + with_gem_version "4.0.10" do + use_ui @ui do + @cmd.show_release_notes + end end - expected = <<-EXPECTED -## #{Gem::VERSION} / 2013-03-26 + expected = "See https://github.com/ruby/rubygems/blob/v4.0.10/CHANGELOG.md for the changes.\n" -### Bug fixes: - * Fixed release note display for LANG=C when installing rubygems - * π is tasty + assert_equal expected, @ui.output + end - EXPECTED + def test_show_release_notes_same_version + @cmd.handle_options ["--previous-version", "4.0.19"] - output = @ui.output - output.force_encoding Encoding::UTF_8 + with_gem_version "4.0.19" do + use_ui @ui do + @cmd.show_release_notes + end + end - assert_equal expected, output - ensure - @ui.outs.set_encoding @default_external if @default_external + expected = "See https://github.com/ruby/rubygems/blob/v4.0.19/CHANGELOG.md for the changes.\n" + + assert_equal expected, @ui.output + end + + def test_show_release_notes_whitespace_previous_version + @cmd.handle_options ["--previous-version", " 2.0.2\r"] + + with_gem_version "4.0.19" do + use_ui @ui do + @cmd.show_release_notes + end + end + + expected = "See https://github.com/ruby/rubygems/blob/v4.0.19/CHANGELOG.md for the changes since 2.0.2.\n" + + assert_equal expected, @ui.output + end + + def test_show_release_notes_invalid_previous_version + @cmd.handle_options ["--previous-version", "\e[31mnot-a-version"] + + with_gem_version "4.0.19" do + use_ui @ui do + @cmd.show_release_notes + end + end + + expected = "See https://github.com/ruby/rubygems/blob/v4.0.19/CHANGELOG.md for the changes.\n" + + assert_equal expected, @ui.output + end + + def test_show_release_notes_without_previous_version + @cmd.options[:previous_version] = "" + + with_gem_version "4.0.19" do + use_ui @ui do + @cmd.show_release_notes + end + end + + expected = "See https://github.com/ruby/rubygems/blob/v4.0.19/CHANGELOG.md for the changes.\n" + + assert_equal expected, @ui.output end private + def with_gem_version(version) + original = Gem::VERSION + Gem.send :remove_const, :VERSION + Gem.const_set :VERSION, version + yield + ensure + Gem.send :remove_const, :VERSION + Gem.const_set :VERSION, original + end + def create_dummy_files(list) list.each do |file| FileUtils.mkdir_p File.dirname(file) diff --git a/test/rubygems/test_gem_update_suggestion.rb b/test/rubygems/test_gem_update_suggestion.rb index 8cb8ee57ff90be..dc5395f8344b79 100644 --- a/test/rubygems/test_gem_update_suggestion.rb +++ b/test/rubygems/test_gem_update_suggestion.rb @@ -55,9 +55,10 @@ def self.with_eligible_environment( def test_update_suggestion Gem.stub :rubygems_version, Gem::Version.new("1.2.3") do Gem.stub :latest_rubygems_version, Gem::Version.new("2.0.0") do - assert_equal @cmd.update_suggestion, <<~SUGGESTION + assert_equal <<~SUGGESTION, @cmd.update_suggestion A new release of RubyGems is available: 1.2.3 → 2.0.0! + See https://github.com/ruby/rubygems/blob/v2.0.0/CHANGELOG.md for the changes since 1.2.3. Run `gem update --system 2.0.0` to update your installation. SUGGESTION